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

Я понимаю лямбда и делегаты Func и Action. Но выражения пеньют меня. В каких обстоятельствах вы использовали бы 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:

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

В то время как оба они выглядят одинаково во время компиляции, то, что генерирует компилятор, полностью отличается.

Ответ 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);

Rico Mariani

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

В большинстве случаев вам понадобится 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<>> значительно быстрее, особенно если вы запрашиваете базу данных, которая живет на другом сервере.