Я понимаю лямбда и делегаты Func
и Action
. Но выражения пеньют меня. В каких обстоятельствах вы использовали бы Expression<Func<T>>
, а не простой старый Func<T>
?
Если вы хотите рассматривать лямбда-выражения как деревья выражений и заглядывать внутрь них вместо их выполнения. Например, 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:
увеличенное изображение
В то время как оба они выглядят одинаково во время компиляции, то, что генерирует компилятор, полностью отличается.
Expression
содержит метаинформацию об определенном делегате.
Expression<Func<...>>
вместо просто Func<...>
.
Я добавляю ответ для noobs, потому что эти ответы казались мне над головой, пока я не понял, насколько это просто. Иногда вы ожидаете, что это сложно, что не позволяет вам "обвести вокруг себя голову".
Мне не нужно было понимать разницу, пока я не вошел в действительно раздражающую "ошибку", пытающуюся использовать LINQ-to-SQL в целом:
public IEnumerable<T> Get(Func<T, bool> conditionLambda){
using(var db = new DbContext()){
return db.Set<T>.Where(conditionLambda);
}
}
Это сработало отлично, пока я не начал получать OutofMemoryExceptions в больших наборах данных. Установка контрольных точек внутри лямбда заставила меня понять, что она повторяется через каждую строку в моем столе один за другим, ища совпадения с моим условием лямбда. Это натолкнуло меня на некоторое время, потому что, черт возьми, он обрабатывает мою таблицу данных как гигантский IEnumerable, а не делает LINQ-to-SQL, как он предполагал? Он также выполнял то же самое в моей копии LINQ-to-MongoDb.
Исправление было просто превратить Func<T, bool>
в Expression<Func<T, bool>>
, поэтому я искал google, зачем ему Expression
вместо Func
, заканчивая здесь.
Выражение просто превращает делегат в данные о себе. Итак, a => a + 1
становится чем-то вроде "На левой стороне есть int a
. С правой стороны вы добавляете 1 к нему." Что это. Теперь вы можете вернуться домой. Это, очевидно, более структурированное, чем это, но, по сути, все дерево выражений на самом деле - ничто не обертывает вашу голову.
Понимая это, становится ясно, почему LINQ-to-SQL нуждается в Expression
, а Func
не является адекватным. Func
не несет с собой способ проникнуть в себя, чтобы увидеть, как перевести его в SQL/MongoDb/другой запрос. Вы не можете видеть, делает ли это добавление или умножение при вычитании. Все, что вы можете сделать, это запустить его. Expression
, с другой стороны, позволяет вам заглянуть внутрь делегата и увидеть все, что он хочет сделать, предоставив вам возможность перевести его в нужное вам, например SQL-запрос. Func
не работал, потому что мой DbContext был слеп к тому, что было на самом деле в выражении лямбда, чтобы превратить его в SQL, поэтому он сделал следующее лучшее и повторил это условно через каждую строку в моей таблице.
Изменить: излагая мое последнее предложение в просьбе Джона Питера:
IQueryable расширяет IEnumerable, поэтому методы IEnumerable, такие как Where()
, получают перегрузки, которые принимают Expression
. Когда вы передаете Expression
к этому, вы сохраняете IQueryable в результате, но когда вы передаете Func
, вы возвращаетесь к базе IEnumerable, и в результате вы получите IEnumerable. Другими словами, не заметив, что вы превратили свой набор данных в список, который нужно повторить, а не что-то запрашивать. Трудно заметить разницу, пока вы действительно не посмотрите под капот на подписи.
Чрезвычайно важным соображением при выборе Expression vs Func является то, что провайдеры IQueryable, такие как LINQ to Entities, могут "переваривать" то, что вы передаете в выражении, но будете игнорировать то, что вы передаете в Func. У меня есть две записи в блогах по теме:
Подробнее о Expression vs Func с платформой Entity Framework и Влюбленность в LINQ - Часть 7: Выражения и Funcs (последний раздел)
Я хотел бы добавить некоторые примечания о различиях между Func<T>
и Expression<Func<T>>
:
Func<T>
- это обычная старая школа MulticastDelegate;Expression<Func<T>>
является представлением лямбда-выражения в форме дерева выражений;Func<T>
;ExpressionVisitor
;Func<T>
;Expression<Func<T>>
.Вот статья, в которой описываются детали с образцами кода:
LINQ: Func <T> vs. Expression < Func <T → .
Надеюсь, это будет полезно.
LINQ - это канонический пример (например, разговор с базой данных), но, по правде говоря, в любое время вам больше нужно выражать, что делать, а не делать это. Например, я использую этот подход в стеке RPC protobuf-net (чтобы избежать генерации кода и т.д.) - поэтому вы вызываете метод с:
string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));
Это деконструирует дерево выражений для разрешения SomeMethod
(и значение каждого аргумента), выполняет вызов RPC, обновляет любые аргументы ref
/out
и возвращает результат из удаленного вызова. Это возможно только через дерево выражений. Я расскажу об этом более здесь.
Другим примером является создание дерева выражений вручную для компиляции в лямбда, как это сделано с помощью общих операторов.
Это более философское объяснение из книги Кшиштофа Квалины ( "Руководства по дизайну рамок: условности, идиомы и шаблоны для многоразовых библиотек .NET" );
Изменить для версии без изображения:
В большинстве случаев вам понадобится Func или Действие, если все, что должно произойти, - запустить некоторый код. Вам нужно Expression, когда код нужно анализировать, сериализовать или оптимизировать до его запуска. Выражение для размышлений о коде, 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.
Вы должны использовать выражение, если хотите обрабатывать свою функцию как данные, а не как код. Вы можете сделать это, если хотите манипулировать кодом (как данными). В большинстве случаев, если вы не видите необходимости в выражениях, вам, вероятно, не нужно его использовать.
Основная причина заключается в том, что вы не хотите запускать код напрямую, а скорее хотите его проверить. Это может быть по нескольким причинам:
Я еще не вижу ответов, говорящих о производительности. Передача Func<>
в Where()
или Count()
плоха. Действительно плохо. Если вы используете Func<>
, тогда он вызывает IEnumerable
материал LINQ вместо IQueryable
, что означает, что целые таблицы втягиваются и затем фильтруются. Expression<Func<>>
значительно быстрее, особенно если вы запрашиваете базу данных, которая живет на другом сервере.