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

687

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

  • 8
    Func <> будет преобразован в метод на уровне компилятора c #, Expression <Func <>> будет выполнен на уровне MSIL после компиляции кода напрямую, поэтому он быстрее
  • 0
    в дополнение к ответам, для перекрестных ссылок полезна спецификация языка csharp «Типы дерева выражений 4.6».
Теги:
delegates
lambda
expression-trees

9 ответов

866
Лучший ответ

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

Изображение 1010 увеличенное изображение

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

  • 78
    Другими словами, Expression содержит метаинформацию об определенном делегате.
  • 35
    @bertl На самом деле нет. Делегат не участвует вообще. Причина, по которой есть какая-либо связь с делегатом, заключается в том, что вы можете скомпилировать выражение для делегата - или, если быть более точным, скомпилировать его в метод и получить делегат этого метода в качестве возвращаемого значения. Но само дерево выражений - это просто данные. Делегат не существует, когда вы используете Expression<Func<...>> вместо просто Func<...> .
Показать ещё 6 комментариев
131

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

  • 2
    Чад; Пожалуйста, объясните этот комментарий немного больше: «Func не работал, потому что мой DbContext был слеп к тому, что на самом деле было в лямбда-выражении, чтобы превратить его в SQL, поэтому он сделал следующее лучшее и повторил это условие через каждую строку в моей таблице «.
  • 7
    Это самый лучший ответ, который я прочитал в этой теме. Спасибо, сэр!
Показать ещё 6 комментариев
86

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

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

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

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

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

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

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

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

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

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

27

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

Изображение 1011

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

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

  • 8
    Хорошо сказано. то есть. Вы нуждаетесь в выражении, когда ожидаете, что ваш Func будет преобразован в какой-то запрос. То есть. вам нужно, чтобы database.data.Where(i => i.Id > 0) выполнялся как SELECT FROM [data] WHERE [id] > 0 . Если вы просто передаете Func, вы ставите блайнды на свой драйвер, и все, что он может сделать, это SELECT * а затем, как только он загрузит все эти данные в память, перебирает все и отфильтровывает все с id> 0. Func в Expression позволяет драйверу анализировать Func и превращать его в запрос Sql / MongoDb / other.
  • 0
    ссылка не работает
Показать ещё 3 комментария
16

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

14

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

  • Сопоставление кода с другой средой (например, код С# для SQL в платформе Entity Framework)
  • Замена частей кода во время выполнения (динамическое программирование или даже простые методы СУХОЙ)
  • Проверка кода (очень полезно при эмуляции скриптов или при анализе)
  • Сериализация - выражения могут быть сериализованы довольно легко и безопасно, делегаты не могут
  • Сильно-типизированная безопасность на вещах, которые по своей сути строго не типизированы, и использование проверок компилятора, даже если вы выполняете динамические вызовы во время выполнения (ASP.NET MVC 5 с Razor - хороший пример)
  • 0
    не могли бы вы подробнее рассказать о № 5
  • 0
    @ uowzd01 Просто посмотрите на Razor - он широко использует этот подход.
Показать ещё 3 комментария
3

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

  • 0
    Это относится и к запросу в памяти?
  • 0
    @ stt106 Наверное, нет.
Показать ещё 2 комментария

Ещё вопросы

Сообщество Overcoder
Наверх
Меню