Навіщо використовувати вираз <Func <T>>, а не Func <T>?


948

Я розумію лямбдів і своїх Funcта Actionделегатів. Але вирази спотикають мене.

За яких обставин ви б скористалися Expression<Func<T>>скоріше не простою старою Func<T>?


14
Func <> буде перетворений у метод на рівні компілятора c #, Вираз <Func <>> буде виконано на рівні MSIL після безпосереднього компіляції коду, тому це швидше
Waleed AK

1
крім відповідей, для перехресного посилання корисна специфікація мови csharp "4.6 типи виразів"
djeikyb

Відповіді:


1133

Коли ви хочете ставитися до лямбда-виразів як до дерев виразів, загляньте всередину, а не виконуючи їх. Наприклад, LINQ в SQL отримує вираз і перетворює його в еквівалентний оператор SQL і подає його на сервер (а не виконувати лямбда).

Концептуально, Expression<Func<T>>це абсолютно відрізняється від Func<T>. Func<T>позначає a, delegateякий в значній мірі є вказівником на метод і Expression<Func<T>>позначає структуру даних дерева для вираження лямбда. Ця структура дерева описує, що робить лямбда-вираз, а не робити фактичну річ. В основному він містить дані про склад виразів, змінних, викликів методів, ... (наприклад, він містить інформацію, таку як ця лямбда - деяка константа + деякий параметр). Ви можете використовувати цей опис, щоб перетворити його у фактичний метод (з Expression.Compile) або робити з ним інші речі (наприклад, LINQ у приклад SQL). Акт трактування лямбдаз як анонімних методів та дерев виразів є суто справою часу складання.

Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }

ефективно компілюється в метод IL, який нічого не отримує, і повертає 10.

Expression<Func<int>> myExpression = () => 10;

буде перетворено в структуру даних, яка описує вираз, який не має параметрів, і повертає значення 10:

Вираз проти Функ збільшене зображення

Хоча вони обидва виглядають однаково під час компіляції, те, що створює компілятор, зовсім інше .


95
Отже, іншими словами, a Expressionмістить метаінформацію про певного делегата.
bertl

39
@bertl Власне, ні. Делегат взагалі не бере участь. Причина, що взагалі є будь-яка асоціація з делегатом, полягає в тому, що ви можете скласти вираз до делегата - або якщо бути точнішим, скомпілювати його в метод і отримати делегата до цього методу як зворотне значення. Але саме дерево виразу - це лише дані. Делегат не існує, коли ви використовуєте Expression<Func<...>>замість просто Func<...>.
Луань

5
@Kyle Delaney (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }такий вираз є ExpressionTree, гілки створюються для оператора If.
Маттео Марчіано - MSCP

3
@bertl Delegate - це те, що бачить процесор (виконуваний код однієї архітектури), Expression - це те, що бачить компілятор (просто інший формат вихідного коду, але все ще вихідний код).
кодове воїнство

5
@bertl: Це може бути більш точно узагальнено, сказавши, що вираз - це функція, що таке стробоутворювач. Це не рядок / func, але він містить необхідні дані, щоб створити їх, коли його просять.
Flater

336

Я додаю відповідь на нобу, тому що ці відповіді здавалися мені в голові, поки я не зрозумів, наскільки це просто. Іноді ви очікуєте, що це складно, що не дає змоги «обернути голову навколо цього».

Мені не потрібно було розуміти різницю, поки я не потрапив у дійсно дратуючий «помилка», намагаючись загалом використовувати LINQ-SQL:

public IEnumerable<T> Get(Func<T, bool> conditionLambda){
  using(var db = new DbContext()){
    return db.Set<T>.Where(conditionLambda);
  }
}

Це спрацювало чудово, поки я не почав отримувати OutofMemoryExceptions на великих наборах даних. Встановлення точок перериву всередині лямбда змусило мене зрозуміти, що воно повторюється через кожен рядок в моїй таблиці по одному, шукаючи відповідність моєму лямбда-умові. Це наштовхнуло мене на деякий час, тому що, до біса, це трактування моєї таблиці даних як гігантського IEnumerable замість того, щоб робити LINQ-SQL так, як це належить? Це було те саме, що було зроблено в моєму колезі LINQ до MongoDb.

Виправлення було просто перетворити Func<T, bool>на Expression<Func<T, bool>>, тому я погуглив, чому це потрібно Expressionзамість цього Func, закінчившись тут.

Вираз просто перетворює делегата в дані про себе. Так a => a + 1стає щось на кшталт "На лівій стороні є" int a. На правій стороні ви додасте 1 ". Це воно. Ви можете піти додому зараз. Це, очевидно, більш структуровано, ніж це, але це по суті все виражене дерево насправді - нічого, щоб обернути голову.

Зрозумівши це, стає зрозуміло, для чого потрібен LINQ-SQL Expression, а Funcне адекватний. Funcне несе із собою спосіб зайнятися самим собою, побачити дріб'язкість, як перекласти його в SQL / MongoDb / інший запит. Ви не бачите, чи робить це додавання чи множення чи віднімання. Все, що ви можете зробити, це запустити його. Expressionз іншого боку, дозволяє заглянути все до делегата і побачити все, що він хоче зробити. Це дає вам змогу перевести делегата на все, що завгодно, як SQL-запит. Funcне спрацювало, оскільки мій DbContext був сліпим до вмісту лямбдаського виразу. Через це він не міг перетворити лямбда-вираз у SQL; однак вона зробила наступне найкраще і повторила це умовно через кожен рядок моєї таблиці.

Редагувати: викладати моє останнє речення на прохання Івана Петра:

IQueryable розширює IEnumerable, тому методи IEnumerable на зразок Where()отримання перевантажень, які приймають Expression. Коли ви переходите Expressionдо цього, ви зберігаєте IQueryable як результат, але коли ви проходите a Func, ви падаєте назад на базу IEnumerable і ви отримаєте IEnumerable як результат. Іншими словами, не помічаючи, ви перетворили свій набір даних у список, який слід повторити, на відміну від запиту. Важко помітити різницю, поки ти справді не заглянеш під капот на підписи.


2
Чад; Будь ласка, поясніть цей коментар трохи більше: "Func не працював, тому що мій DbContext був сліпий до того, що насправді було в лямбда-виразі, щоб перетворити його на SQL, тому він зробив наступне найкраще і повторив це умовно через кожен рядок моєї таблиці . "
Джон Петерс

2
>> Функц ... Все, що ви можете зробити, це запустити його. Це не зовсім так, але я вважаю, що це саме слід підкреслити. Функції / дії повинні бути запущені, вирази повинні бути проаналізовані (перед запуском або навіть замість запуску).
Костянтин

@Chad Чи проблема тут полягала в тому, що: db.Set <T> запитав усю таблицю бази даних, і після, тому що .Where (conditionLambda) використовував метод розширення Where (IEnumerable), який перераховується на всю таблицю в пам'яті . Я думаю, ви отримуєте OutOfMemoryException, оскільки цей код намагався завантажити всю таблицю в пам'ять (і, звичайно, створив об'єкти). Чи правий я? Дякую :)
Бенс Вегерт

104

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

Детальніше про Expression vs Func з Entity Framework та закохатися в LINQ - Частина 7: Вираження та функції (останній розділ)


+ l для пояснення. Однак я отримую: "Вузол виразу LINQ типу" Invoke "не підтримується в LINQ для сутностей." і довелося використовувати 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> vs. Expression <Func <T>> .

Сподіваюся, це буде корисним.


Хороший список, одна невелика примітка - ви згадаєте, що зворотне перетворення можливе, однак точне зворотне - ні. Деякі метадані втрачаються в процесі перетворення. Однак ви можете декомпілювати його до дерева Expression, яке створює той самий результат при компіляції заново.
Айдіакапі

76

Про це є більш філософське пояснення з книги Кшиштофа Кваліна ( Рамкові рекомендації щодо дизайну: Конвенції, ідіоми та зразки для багаторазових бібліотек .NET );

Ріко Маріані

Редагувати для необразованої версії:

У більшості випадків ви будете хотіти Func або дій , якщо все , що повинно статися, щоб запустити код. Вам потрібен вираз, коли код потрібно проаналізувати, серіалізувати або оптимізувати перед його запуском. Вираз - це думка про код, 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 / інший запит.
Чад Хеджкок

Тож коли я планую відпустку, я б скористався, Expressionале коли я перебуваю у відпустці, це буде Func/Action;)
GoldBishop

1
@ChadHedgcock Це був останній твір, який мені знадобився. Дякую. Я вже дещо дивився на це, і ваш коментар тут змусив усіх натиснути на дослідження.
джонни

37

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

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

Це деконструює дерево виразів для вирішення SomeMethod(і значення кожного аргументу), виконує виклик RPC, оновлює будь-які ref/ outаргументи та повертає результат від віддаленого виклику. Це можливо лише через дерево виразів. Я висвітлюю це більше тут .

Ще один приклад - коли ви будуєте дерева виразів вручну для компіляції до лямбда, як це робиться за допомогою загального коду операторів .


20

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


19

Основна причина полягає в тому, що ви не хочете запускати код безпосередньо, а навпаки, хочете його перевірити. Це може бути з будь-якої кількості причин:

  • Зіставлення коду в інше середовище (тобто код C # для SQL в Entity Framework)
  • Заміна частин коду під час виконання (динамічне програмування або навіть звичайні технології DRY)
  • Перевірка коду (дуже корисно при емуляції сценаріїв або при аналізі)
  • Серіалізація - вирази можна серіалізувати досить легко та безпечно, делегати не можуть
  • Сильно набрана безпека для речей, які за своєю суттю не сильно набрані, та використання перевірок компілятора, навіть якщо ви робите динамічні дзвінки під час виконання (ASP.NET MVC 5 з Razor - хороший приклад)

ви можете детальніше розібратися на no.5
uowzd01

@ uowzd01 Подивіться на Razor - він широко використовує цей підхід.
Луань

@Luaan Я шукаю серіалізації виразів, але не можу знайти нічого без обмеженого використання сторонніми сторонами. Чи підтримує .Net 4.5 підтримка серіалізації дерева експресій?
vabii

@vabii Не те, що я знаю, - і це було б не дуже хорошою ідеєю для загального випадку. Моя думка стосувалася того, що ви могли написати досить просту серіалізацію для конкретних випадків, які ви хочете підтримати, проти інтерфейсів, розроблених достроково - я це робив лише кілька разів. У загальному випадку, se Expressionможе бути так само неможливо серіалізувати, як делегат, оскільки будь-який вираз може містити виклик довільного посилання делегата / методу. "Легко" відносно, звичайно.
Луань

15

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


Чи стосується це і запиту в пам'яті?
stt106

@ stt106 Напевно, ні.
mhenry1384

Це справедливо лише в тому випадку, якщо ви перераховуєте список. Якщо ви використовуєте GetEnumerator або foreach, ви не завантажуєте цілі численні в пам'ять.
nelsontruran

1
@ stt106 При переході до пункту .Where () списку <>, вираз <Func <>> отримує виклик .Compile (), тому Func <> майже напевно швидше. Див. Посилання referenceource.microsoft.com/#System.Core/System/Linq/…
NStuke
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.