Я розумію лямбдів і своїх 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<>>значно швидше, особливо якщо ви запитуєте базу даних, в якій живе інший сервер.