Я розумію лямбдів і своїх Func
та Action
делегатів. Але вирази спотикають мене.
За яких обставин ви б скористалися Expression<Func<T>>
скоріше не простою старою Func<T>
?
Я розумію лямбдів і своїх Func
та Action
делегатів. Але вирази спотикають мене.
За яких обставин ви б скористалися Expression<Func<T>>
скоріше не простою старою Func<T>
?
Відповіді:
Коли ви хочете ставитися до лямбда-виразів як до дерев виразів, загляньте всередину, а не виконуючи їх. Наприклад, 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:
Хоча вони обидва виглядають однаково під час компіляції, те, що створює компілятор, зовсім інше .
Expression
містить метаінформацію про певного делегата.
Expression<Func<...>>
замість просто Func<...>
.
(isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }
такий вираз є ExpressionTree, гілки створюються для оператора If.
Я додаю відповідь на нобу, тому що ці відповіді здавалися мені в голові, поки я не зрозумів, наскільки це просто. Іноді ви очікуєте, що це складно, що не дає змоги «обернути голову навколо цього».
Мені не потрібно було розуміти різницю, поки я не потрапив у дійсно дратуючий «помилка», намагаючись загалом використовувати 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 як результат. Іншими словами, не помічаючи, ви перетворили свій набір даних у список, який слід повторити, на відміну від запиту. Важко помітити різницю, поки ти справді не заглянеш під капот на підписи.
Надзвичайно важливим питанням у виборі Expression vs Func є те, що постачальники послуг IQueryable, такі як LINQ для Entities, можуть «переварити» те, що ви передаєте в Expression, але ігноруватимете те, що передаєте у Func. У мене є дві публікації на цю тему:
Детальніше про Expression vs Func з Entity Framework та закохатися в LINQ - Частина 7: Вираження та функції (останній розділ)
Я хочу додати кілька приміток про відмінності між Func<T>
та Expression<Func<T>>
:
Func<T>
це просто звичайний старокалійський MulticastDelegate;Expression<Func<T>>
- представлення лямбда-експресії у вигляді дерева експресії;Func<T>
;ExpressionVisitor
;Func<T>
;Expression<Func<T>>
.Там є стаття, яка описує деталі із зразками коду:
LINQ: Func <T> vs. Expression <Func <T>> .
Сподіваюся, це буде корисним.
Про це є більш філософське пояснення з книги Кшиштофа Кваліна ( Рамкові рекомендації щодо дизайну: Конвенції, ідіоми та зразки для багаторазових бібліотек .NET );
Редагувати для необразованої версії:
У більшості випадків ви будете хотіти Func або дій , якщо все , що повинно статися, щоб запустити код. Вам потрібен вираз, коли код потрібно проаналізувати, серіалізувати або оптимізувати перед його запуском. Вираз - це думка про код, Func / Action - для його запуску.
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
;)
LINQ - канонічний приклад (наприклад, розмова з базою даних), але по правді кажучи, будь-коли вам більше важливо висловити, що робити, а не робити це. Наприклад, я використовую такий підхід у степі RPC протобуф-мережі (щоб уникнути генерації коду тощо) - тому ви викликаєте метод із:
string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));
Це деконструює дерево виразів для вирішення SomeMethod
(і значення кожного аргументу), виконує виклик RPC, оновлює будь-які ref
/ out
аргументи та повертає результат від віддаленого виклику. Це можливо лише через дерево виразів. Я висвітлюю це більше тут .
Ще один приклад - коли ви будуєте дерева виразів вручну для компіляції до лямбда, як це робиться за допомогою загального коду операторів .
Ви б використовували вираз, коли хочете розглянути свою функцію як дані, а не як код. Це можна зробити, якщо ви хочете маніпулювати кодом (як дані). Більшість випадків, якщо ви не бачите потреби в виразах, вам, ймовірно, не потрібно використовувати його.
Основна причина полягає в тому, що ви не хочете запускати код безпосередньо, а навпаки, хочете його перевірити. Це може бути з будь-якої кількості причин:
Expression
може бути так само неможливо серіалізувати, як делегат, оскільки будь-який вираз може містити виклик довільного посилання делегата / методу. "Легко" відносно, звичайно.
Я ще не бачу відповідей, які б згадували про ефективність. Перехід Func<>
s Where()
або Count()
поганий. Справді погано. Якщо ви використовуєте a, Func<>
тоді він викликає IEnumerable
речі LINQ замість IQueryable
, а це означає, що цілі таблиці потрапляють і потім фільтруються. Expression<Func<>>
значно швидше, особливо якщо ви запитуєте базу даних, в якій живе інший сервер.