Я понимаю лямбды Func
и Action
делегаты. Но выражения озадачивают меня.
При каких обстоятельствах вы бы использовали Expression<Func<T>>
скорее старый, чем обычный Func<T>
?
Я понимаю лямбды Func
и Action
делегаты. Но выражения озадачивают меня.
При каких обстоятельствах вы бы использовали Expression<Func<T>>
скорее старый, чем обычный Func<T>
?
Ответы:
Когда вы хотите трактовать лямбда-выражения как деревья выражений и смотреть в них, а не выполнять их. Например, LINQ to SQL получает выражение и преобразует его в эквивалентный оператор SQL и отправляет его на сервер (а не выполняет лямбда-выражение).
Концептуально, Expression<Func<T>>
это совершенно отличается от Func<T>
. Func<T>
обозначает a, delegate
который в значительной степени является указателем на метод, и Expression<Func<T>>
обозначает древовидную структуру данных для лямбда-выражения. Эта древовидная структура описывает то, что делает лямбда-выражение, а не делает реальную вещь. Он в основном содержит данные о составе выражений, переменных, вызовов методов, ... (например, он хранит такую информацию, как эта лямбда - некоторая константа + некоторый параметр). Вы можете использовать это описание, чтобы преобразовать его в реальный метод (с Expression.Compile
) или сделать другие вещи (например, пример LINQ to SQL) с ним. Обращение с лямбдами как с анонимными методами и деревьями выражений - это просто время компиляции.
Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }
будет эффективно компилироваться в метод IL, который ничего не получает и возвращает 10.
Expression<Func<int>> myExpression = () => 10;
будет преобразован в структуру данных, которая описывает выражение, которое не получает параметров и возвращает значение 10:
Хотя они оба выглядят одинаково во время компиляции, то, что генерирует компилятор, совершенно разное .
Expression
содержит метаинформацию об определенном делегате.
Expression<Func<...>>
вместо просто Func<...>
.
(isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }
такое выражение является ExpressionTree, ветки создаются для оператора If.
Я добавляю ответ для новичков, потому что эти ответы казались мне над головой, пока я не понял, насколько это просто. Иногда вы ожидаете, что это сложно, что делает вас неспособным «обернуть голову вокруг этого».
Мне не нужно было понимать разницу, пока я не столкнулся с действительно раздражающей «ошибкой», пытаясь использовать LINQ-to-SQL в общем:
public IEnumerable<T> Get(Func<T, bool> conditionLambda){
using(var db = new DbContext()){
return db.Set<T>.Where(conditionLambda);
}
}
Это прекрасно работало, пока я не начал получать исключения OutofMemoryException для больших наборов данных. Установка точек останова внутри лямбды заставила меня понять, что он перебирает каждую строку в моей таблице один за другим в поисках совпадений с моим состоянием лямбды. Это поставило меня в тупик на некоторое время, потому что, черт возьми, он обрабатывает мою таблицу данных как гигантский IEnumerable вместо того, чтобы выполнять LINQ-to-SQL, как положено? То же самое делалось и с моим коллегой из LINQ-to-MongoDb.
Исправление было просто превратиться Func<T, bool>
в Expression<Func<T, bool>>
, так что я гуглил, зачем это нужно Expression
вместо того Func
, чтобы оказаться здесь.
Выражение просто превращает делегата в данные о себе. Так a => a + 1
что-то вроде: «С левой стороны есть int a
. На правой стороне вы добавляете 1 к нему». Вот и все. Вы можете идти домой сейчас. Это, очевидно, более структурированный, чем это, но это, по сути, все дерево выражений на самом деле - ничего, чтобы обернуть голову.
Понимая это, становится ясно, почему LINQ-to-SQL нуждается Expression
, а Func
не адекватен. Func
не влечет за собой способ проникнуть в себя, понять, как перевести его в запрос SQL / MongoDb / other. Вы не можете видеть, делает ли он сложение, умножение или вычитание. Все, что вы можете сделать, это запустить его. Expression
с другой стороны, позволяет заглянуть внутрь делегата и увидеть все, что он хочет сделать. Это позволяет вам перевести делегата во что угодно, например, в SQL-запрос. Func
не работал, потому что мой DbContext был слеп к содержанию лямбда-выражения. Из-за этого он не мог превратить лямбда-выражение в SQL; однако, он сделал следующее лучшее и повторил это условие по каждой строке в моей таблице.
Изменить: изложив мое последнее предложение по просьбе Джона Питера:
IQueryable расширяет IEnumerable, поэтому методы IEnumerable, такие как Where()
получение перегрузок, которые принимают Expression
. Когда вы передаете это Expression
, вы сохраняете IQueryable как результат, но когда вы передаете a Func
, вы возвращаетесь к базовому IEnumerable, и в результате вы получаете IEnumerable. Другими словами, не замечая, что вы превратили свой набор данных в список для повторения, а не для запроса. Трудно заметить разницу, пока вы действительно не заглянете под капотом на подписи.
Чрезвычайно важным соображением при выборе Expression vs Func является то, что провайдеры IQueryable, такие как LINQ to Entities, могут «переваривать» то, что вы передаете в Expression, но игнорируют то, что вы передаете в Func. У меня есть два сообщения в блоге на эту тему:
Подробнее о Expression vs Func с Entity Framework и влюблении в LINQ - Часть 7. Выражения и функции (последний раздел)
Я хотел бы добавить некоторые заметки о различиях между Func<T>
и Expression<Func<T>>
:
Func<T>
просто обычный MulticastDelegate старой школы;Expression<Func<T>>
является представлением лямбда-выражения в виде дерева выражений;Func<T>
;ExpressionVisitor
;Func<T>
;Expression<Func<T>>
.Есть статья, которая описывает детали с примерами кода:
LINQ: Func <T> против Expression <Func <T >> .
Надеюсь, это будет полезно.
Об этом есть более философское объяснение из книги Кшиштофа Квалины ( Руководство по разработке структуры: условные обозначения , идиомы и шаблоны для многократно используемых библиотек .NET );
Изменить для версии без изображения:
В большинстве случаев вам понадобится Func или Action, если все, что нужно, это запустить какой-то код. Вы должны использовать выражение, когда код должен быть проанализирован, сериализован или оптимизирован перед его запуском. Выражение для размышления о коде, Func / Action для его запуска.
database.data.Where(i => i.Id > 0)
быть выполнены как SELECT FROM [data] WHERE [id] > 0
. Если вы просто передать в Func, вы положили шоры на драйвере , и все это может сделать SELECT *
и то , как только он будет загружен все эти данные в память, итерацию по каждому и отфильтровывать все с ID> 0. оборачивать ваши Func
в Expression
расширяющего драйвер для анализа Func
и преобразования его в запрос Sql / MongoDb / other.
Expression
но когда я в отпуске, это будет Func/Action
;)
LINQ - это канонический пример (например, общение с базой данных), но, по правде говоря, каждый раз, когда вы больше заботитесь о том, чтобы выразить, что делать, чем о том, что вы делаете. Например, я использую этот подход в стеке RPC protobuf-net (чтобы избежать генерации кода и т. Д.) - поэтому вы вызываете метод с помощью:
string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));
Это деконструирует дерево выражений для разрешения SomeMethod
(и значение каждого аргумента), выполняет вызов RPC, обновляет любойref
/ out
args и возвращает результат удаленного вызова. Это возможно только через дерево выражений. Я расскажу об этом подробнее здесь .
Другой пример - когда вы вручную строите деревья выражений с целью компиляции в лямбду, как это делается с помощью универсального кода операторов .
Вы бы использовали выражение, когда хотите рассматривать свою функцию как данные, а не как код. Вы можете сделать это, если хотите манипулировать кодом (как данными). В большинстве случаев, если вы не видите необходимости в выражениях, вам, вероятно, не нужно их использовать.
Основная причина в том, что вы не хотите запускать код напрямую, а хотите его проверить. Это может быть по ряду причин:
Expression
может быть также невозможно сериализовать как делегат, так как любое выражение может содержать вызов произвольной ссылки на делегат / метод. «Легко» относительно, конечно.
Я пока не вижу ответов, в которых упоминается производительность. Передача Func<>
с Where()
или Count()
плохо. Очень плохо Если вы используете, Func<>
тогда он вызывает IEnumerable
LINQ, а не IQueryable
, что означает, что целые таблицы извлекаются, а затем фильтруются. Expression<Func<>>
значительно быстрее, особенно если вы запрашиваете базу данных, которая живет на другом сервере.