Почему вы используете Expression <Func <T >>, а не Func <T>?

949

Я понимаю лямбды Funcи Actionделегаты. Но выражения озадачивают меня.

При каких обстоятельствах вы бы использовали Expression<Func<T>>скорее старый, чем обычный Func<T>?

Ричард Нэгл
источник
14
Func <> будет преобразован в метод на уровне компилятора c #, Expression <Func <>> будет выполнен на уровне MSIL после компиляции кода напрямую, поэтому он быстрее
Waleed AK
1
в дополнение к ответам полезно использовать перекрестную ссылку на спецификацию языка csharp "4.6. Типы дерева выражений"
djeikyb

Ответы:

1133

Когда вы хотите трактовать лямбда-выражения как деревья выражений и смотреть в них, а не выполнять их. Например, 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:

Выражение против Func увеличенное изображение

Хотя они оба выглядят одинаково во время компиляции, то, что генерирует компилятор, совершенно разное .

Мехрдад Афшари
источник
96
Другими словами, an Expressionсодержит метаинформацию об определенном делегате.
Бертл
40
@bertl На самом деле нет. Делегат не участвует вообще. Причина, по которой есть какая-либо связь с делегатом, заключается в том, что вы можете скомпилировать выражение для делегата - или, если быть более точным, скомпилировать его в метод и получить делегат этого метода в качестве возвращаемого значения. Но само дерево выражений - это просто данные. Делегат не существует, когда вы используете Expression<Func<...>>вместо просто Func<...>.
Луаан
5
@Kyle Delaney (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }такое выражение является ExpressionTree, ветки создаются для оператора If.
Маттео Марчиано - MSCP
3
@bertl Delegate - это то, что видит процессор (исполняемый код одной архитектуры), Expression - это то, что видит компилятор (просто другой формат исходного кода, но все же исходный код).
Codewarrior
5
@bertl: Можно было бы точнее подытожить, сказав, что выражение для функции является тем же, чем строитель строки для строки. Это не строка / функция, но она содержит данные, необходимые для ее создания, когда об этом попросят.
Flater
337

Я добавляю ответ для новичков, потому что эти ответы казались мне над головой, пока я не понял, насколько это просто. Иногда вы ожидаете, что это сложно, что делает вас неспособным «обернуть голову вокруг этого».

Мне не нужно было понимать разницу, пока я не столкнулся с действительно раздражающей «ошибкой», пытаясь использовать 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. Другими словами, не замечая, что вы превратили свой набор данных в список для повторения, а не для запроса. Трудно заметить разницу, пока вы действительно не заглянете под капотом на подписи.

Чед Хедккок
источник
2
Чад; Пожалуйста, объясните этот комментарий немного больше: «Func не работал, потому что мой DbContext был слеп к тому, что на самом деле было в лямбда-выражении, чтобы превратить его в SQL, поэтому он сделал следующую лучшую вещь и повторил это условие через каждую строку в моей таблице «.
Джон Питерс
2
>> Func ... Все, что вы можете сделать, это запустить его. Это не совсем так, но я думаю, что именно этот момент следует подчеркнуть. Должны выполняться функции / действия, выражения должны анализироваться (перед запуском или даже вместо запуска).
Константин
@Chad Проблема в том, что здесь ?: db.Set <T> запросил всю таблицу базы данных, и после, потому что .Where (conditionLambda) использовал метод расширения Where (IEnumerable), который перечисляет всю таблицу в памяти , Я думаю, что вы получаете OutOfMemoryException, потому что этот код пытался загрузить всю таблицу в память (и, конечно, создал объекты). Я прав? Спасибо :)
Бенс Вегерт
104

Чрезвычайно важным соображением при выборе Expression vs Func является то, что провайдеры IQueryable, такие как LINQ to Entities, могут «переваривать» то, что вы передаете в Expression, но игнорируют то, что вы передаете в Func. У меня есть два сообщения в блоге на эту тему:

Подробнее о Expression vs Func с Entity Framework и влюблении в LINQ - Часть 7. Выражения и функции (последний раздел)

LSpencer777
источник
+1 для объяснения. Однако я получаю «Тип узла выражения LINQ« Invoke »не поддерживается в LINQ to Entities». и должен был использовать ForEach после получения результатов.
tymtam
77

Я хотел бы добавить некоторые заметки о различиях между 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> против Expression <Func <T >> .

Надеюсь, это будет полезно.

Александр Иваницкий
источник
Хороший список, одна небольшая заметка - вы упоминаете, что обратное преобразование возможно, однако точное обратное - нет. Некоторые метаданные теряются в процессе преобразования. Однако вы можете декомпилировать его в дерево выражений, которое при повторной компиляции выдает тот же результат.
Aidiakapi
76

Об этом есть более философское объяснение из книги Кшиштофа Квалины ( Руководство по разработке структуры: условные обозначения , идиомы и шаблоны для многократно используемых библиотек .NET );

Рико Мариани

Изменить для версии без изображения:

В большинстве случаев вам понадобится Func или Action, если все, что нужно, это запустить какой-то код. Вы должны использовать выражение, когда код должен быть проанализирован, сериализован или оптимизирован перед его запуском. Выражение для размышления о коде, Func / Action для его запуска.

Огужан Сойкан
источник
10
Хорошо сказано. то есть. Вы нуждаетесь в выражении, когда ожидаете, что ваш Func будет преобразован в какой-то запрос. То есть. Вы должны 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;)
GoldBishop
1
@ChadHedgcock Это был последний кусок, который мне был нужен. Спасибо. Я смотрел на это некоторое время, и ваш комментарий заставил все исследование щелкнуть.
джонни
37

LINQ - это канонический пример (например, общение с базой данных), но, по правде говоря, каждый раз, когда вы больше заботитесь о том, чтобы выразить, что делать, чем о том, что вы делаете. Например, я использую этот подход в стеке RPC protobuf-net (чтобы избежать генерации кода и т. Д.) - поэтому вы вызываете метод с помощью:

string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));

Это деконструирует дерево выражений для разрешения SomeMethod(и значение каждого аргумента), выполняет вызов RPC, обновляет любойref / outargs и возвращает результат удаленного вызова. Это возможно только через дерево выражений. Я расскажу об этом подробнее здесь .

Другой пример - когда вы вручную строите деревья выражений с целью компиляции в лямбду, как это делается с помощью универсального кода операторов .

Марк Гравелл
источник
20

Вы бы использовали выражение, когда хотите рассматривать свою функцию как данные, а не как код. Вы можете сделать это, если хотите манипулировать кодом (как данными). В большинстве случаев, если вы не видите необходимости в выражениях, вам, вероятно, не нужно их использовать.

Эндрю Хэйр
источник
19

Основная причина в том, что вы не хотите запускать код напрямую, а хотите его проверить. Это может быть по ряду причин:

  • Отображение кода в другую среду (т. Е. Код C # на SQL в Entity Framework)
  • Замена частей кода во время выполнения (динамическое программирование или даже простые методы СУХОЙ)
  • Проверка кода (очень полезно при эмуляции сценариев или при анализе)
  • Сериализация - выражения можно сериализовать довольно легко и безопасно, делегаты не могут
  • Строго типизированная безопасность для вещей, которые по своей природе не являются строго типизированными, и использование проверок компилятора, даже если вы выполняете динамические вызовы во время выполнения (хороший пример - ASP.NET MVC 5 с Razor)
Luaan
источник
не могли бы вы подробнее
рассказать
@ uowzd01 Просто посмотрите на Razor - он широко использует этот подход.
Луаан
@Luaan Я ищу сериализацию выражений, но не могу ничего найти без ограниченного использования третьей стороной. Поддерживает ли .Net 4.5 сериализацию дерева выражений?
Вабий
@vabii Не то, чтобы я знал - и это не будет хорошей идеей для общего случая. Моя точка зрения была больше о том, что вы можете написать довольно простую сериализацию для конкретных случаев, которые вы хотите поддерживать, с интерфейсами, разработанными заранее - я делал это несколько раз. В общем случае Expressionможет быть также невозможно сериализовать как делегат, так как любое выражение может содержать вызов произвольной ссылки на делегат / метод. «Легко» относительно, конечно.
Луаан
15

Я пока не вижу ответов, в которых упоминается производительность. Передача Func<>с Where()или Count()плохо. Очень плохо Если вы используете, Func<>тогда он вызывает IEnumerableLINQ, а не IQueryable, что означает, что целые таблицы извлекаются, а затем фильтруются. Expression<Func<>>значительно быстрее, особенно если вы запрашиваете базу данных, которая живет на другом сервере.

mhenry1384
источник
Это относится и к запросу в памяти?
stt106
@ stt106 Наверное, нет.
mhenry1384
Это верно только в том случае, если вы перечислите список. Если вы используете GetEnumerator или foreach, вы не сможете полностью загрузить ienumerable в память.
nelsontruran
1
@ stt106 При передаче в .Where () предложения List <> Expression <Func <>> получает вызов .Compile (), поэтому Func <> почти наверняка быстрее. См. Referenceource.microsoft.com/#System.Core/System/Linq/…
NStuke