Я понимаю лямбда и делегаты Func
и Action
. Но выражения пеньют меня. В каких обстоятельствах вы использовали бы Expression<Func<T>>
, а не простой старый Func<T>
?
Почему вы используете Expression <Func <T>>, а не Func <T>?
Ответ 1
Если вы хотите рассматривать лямбда-выражения как деревья выражений и заглядывать внутрь них вместо их выполнения. Например, LINQ to SQL получает выражение и преобразует его в эквивалентный оператор SQL и отправляет его на сервер (вместо выполнения лямбда).
Концептуально Expression<Func<T>>
полностью отличается от Func<T>
. Func<T>
обозначает 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:
В то время как оба они выглядят одинаково во время компиляции, то, что генерирует компилятор, полностью отличается.
Ответ 2
Я добавляю ответ для новичков, потому что эти ответы казались мне над головой, пока я не понял, насколько это просто. Иногда ваше ожидание того, что это сложно, делает вас неспособным "обернуть голову вокруг этого".
Мне не нужно было понимать разницу, пока я не наткнулся на действительно раздражающую "ошибку", пытаясь использовать 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
другой стороны, Expression
позволяет вам заглянуть внутрь делегата и увидеть все, что он хочет сделать. Это позволяет вам перевести делегата во что угодно, например, в SQL-запрос. Func
не работал, потому что мой DbContext был слеп к содержанию лямбда-выражения. Из-за этого он не мог превратить лямбда-выражение в SQL; тем не менее, он сделал следующую лучшую вещь и повторил это условие по каждой строке в моей таблице.
Изменение: изложив мое последнее предложение по просьбе Джона Питера:
IQueryable расширяет IEnumerable, поэтому методы IEnumerable, такие как Where()
получают перегрузки, которые принимают Expression
. Когда вы передаете Expression
этому, вы сохраняете IQueryable в результате, но когда вы передаете Func
, вы отбрасываете базовый IEnumerable, и в результате вы получаете IEnumerable. Другими словами, не замечая, что вы превратили свой набор данных в список для повторения, а не для запроса. Трудно заметить разницу, пока вы действительно не заглянете под капот на подписи.
Ответ 3
Чрезвычайно важным соображением при выборе Expression vs Func является то, что провайдеры IQueryable, такие как LINQ to Entities, могут "переваривать" то, что вы передаете в выражении, но будете игнорировать то, что вы передаете в Func. У меня есть две записи в блогах по теме:
Подробнее о Expression vs Func с платформой Entity Framework и Влюбленность в LINQ - Часть 7: Выражения и Funcs (последний раздел)
Ответ 4
Я хотел бы добавить некоторые примечания о различиях между Func<T>
и Expression<Func<T>>
:
-
Func<T>
- это обычная старая школа MulticastDelegate; -
Expression<Func<T>>
является представлением лямбда-выражения в форме дерева выражений; - дерево выражений может быть построено с помощью синтаксиса выражения лямбда или с помощью синтаксиса API;
- дерево выражений может быть скомпилировано делегату
Func<T>
; - теоретически возможно обратное преобразование, но это своего рода декомпиляция, для этого нет встроенных функций, поскольку это не простой процесс;
- Дерево выражений можно наблюдать/переводить/модифицировать с помощью
ExpressionVisitor
; - методы расширения для IEnumerable работают с
Func<T>
; - методы расширения для IQueryable работают с
Expression<Func<T>>
.
Вот статья, в которой описываются детали с образцами кода:
LINQ: Func <T> vs. Expression < Func <T → .
Надеюсь, это будет полезно.
Ответ 5
Об этом есть более философское объяснение из книги Кшиштофа Квалины (Руководство по разработке структуры: соглашения, идиомы и шаблоны для многократно используемых библиотек .NET);
Изменить для версии без изображения:
В большинстве случаев вам понадобится Func или Action, если все, что нужно, это запустить какой-то код. Вы должны использовать выражение, когда код должен быть проанализирован, сериализован или оптимизирован перед его запуском. Выражение для размышления о коде, Func/Action для его запуска.
Ответ 6
LINQ - это канонический пример (например, общение с базой данных), но, по правде говоря, каждый раз, когда вы больше заботитесь о том, чтобы выразить, что делать, чем о том, что вы делаете. Например, я использую этот подход в стеке RPC protobuf-net (чтобы избежать генерации кода и т.д.) - поэтому вы вызываете метод с помощью:
string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));
Это деконструирует дерево выражений для разрешения SomeMethod
(и значения каждого аргумента), выполняет вызов RPC, обновляет любые аргументы ref
/out
и возвращает результат удаленного вызова. Это возможно только через дерево выражений. Я расскажу об этом подробнее здесь.
Другой пример - когда вы вручную строите деревья выражений с целью компиляции в лямбду, как это делается с помощью универсального кода операторов.
Ответ 7
Вы должны использовать выражение, если хотите обрабатывать свою функцию как данные, а не как код. Вы можете сделать это, если хотите манипулировать кодом (как данными). В большинстве случаев, если вы не видите необходимости в выражениях, вам, вероятно, не нужно его использовать.
Ответ 8
Основная причина заключается в том, что вы не хотите запускать код напрямую, а скорее хотите его проверить. Это может быть по нескольким причинам:
- Сопоставление кода с другой средой (например, код С# для SQL в платформе Entity Framework)
- Замена частей кода во время выполнения (динамическое программирование или даже простые методы СУХОЙ)
- Проверка кода (очень полезно при эмуляции скриптов или при анализе)
- Сериализация - выражения могут быть сериализованы довольно легко и безопасно, делегаты не могут
- Сильно-типизированная безопасность на вещах, которые по своей сути строго не типизированы, и использование проверок компилятора, даже если вы выполняете динамические вызовы во время выполнения (ASP.NET MVC 5 с Razor - хороший пример)
Ответ 9
Я еще не вижу ответов, говорящих о производительности. Передача Func<>
в Where()
или Count()
плоха. Действительно плохо. Если вы используете Func<>
, тогда он вызывает IEnumerable
материал LINQ вместо IQueryable
, что означает, что целые таблицы втягиваются и затем фильтруются. Expression<Func<>>
значительно быстрее, особенно если вы запрашиваете базу данных, которая живет на другом сервере.