Почему вы используете выражение <Func <T>>, а не Func <T>? - PullRequest
865 голосов
/ 27 апреля 2009

Я понимаю лямбды и делегаты Func и Action. Но выражения озадачивают меня. При каких обстоятельствах вы бы использовали Expression<Func<T>> вместо простого старого Func<T>?

Ответы [ 9 ]

1057 голосов
/ 27 апреля 2009

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

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

283 голосов
/ 05 января 2016

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

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

96 голосов
/ 11 января 2012

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

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

70 голосов
/ 11 июня 2013

Я хотел бы добавить некоторые заметки о различиях между Func<T> и Expression<Func<T>>:

  • Func<T> - это обычный MulticastDelegate старой школы;
  • Expression<Func<T>> - представление лямбда-выражения в виде дерева выражений;
  • дерево выражений может быть построено через синтаксис лямбда-выражений или через синтаксис API;
  • дерево выражений может быть скомпилировано с делегатом Func<T>;
  • обратное преобразование теоретически возможно, но это своего рода декомпиляция, для этого нет встроенной функциональности, поскольку это не простой процесс;
  • дерево выражений можно наблюдать / переводить / изменять через ExpressionVisitor;
  • методы расширения для IEnumerable работают с Func<T>;
  • методы расширения для IQueryable работают с Expression<Func<T>>.

Есть статья, которая описывает детали с примерами кода:
LINQ: Func против выражения > .

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

61 голосов
/ 11 марта 2016

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

Rico Mariani

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

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

36 голосов
/ 27 апреля 2009

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

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

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

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

19 голосов
/ 27 апреля 2009

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

16 голосов
/ 26 марта 2014

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

  • Отображение кода в другую среду (например, код C # на SQL в Entity Framework)
  • Замена частей кода во время выполнения (динамическое программирование или даже простые методы СУХОЙ)
  • Проверка кода (очень полезно при эмуляции сценариев или при анализе)
  • Сериализация - выражения можно сериализовать довольно легко и безопасно, делегаты не могут
  • Строго типизированная безопасность для вещей, которые по своей природе не являются строго типизированными, и использование проверок компилятором, даже если вы выполняете динамические вызовы во время выполнения (хороший пример - ASP.NET MVC 5 с Razor)
9 голосов
/ 16 июня 2017

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

...