Rozumiem lambda Func
i Action
delegatów. Ale wyrażenia mnie zaskakują.
W jakich okolicznościach użyłbyś Expression<Func<T>>
raczej zwykłego niż starego Func<T>
?
c#
delegates
lambda
expression-trees
Richard Nagle
źródło
źródło
Odpowiedzi:
Gdy chcesz traktować wyrażenia lambda jako drzewa wyrażeń i zajrzeć do nich zamiast je wykonywać. Na przykład LINQ na SQL pobiera wyrażenie i konwertuje je na równoważną instrukcję SQL i przesyła je na serwer (zamiast wykonywania lambda).
Konceptualnie,
Expression<Func<T>>
jest zupełnie inna odFunc<T>
.Func<T>
oznacza wskaźnik,delegate
który jest właściwie wskaźnikiem do metody iExpression<Func<T>>
oznacza strukturę danych drzewa dla wyrażenia lambda. Ta struktura drzewa opisuje, co robi wyrażenie lambda, zamiast robić rzeczywistą rzecz. Zasadniczo przechowuje dane o składzie wyrażeń, zmiennych, wywołań metod, ... (na przykład przechowuje informacje takie jak ta lambda to jakaś stała + jakiś parametr). Możesz użyć tego opisu, aby przekonwertować go na rzeczywistą metodę (za pomocąExpression.Compile
) lub wykonać inne czynności (na przykład LINQ na SQL). Traktowanie lambdas jako anonimowych metod i drzewek ekspresji jest wyłącznie kwestią czasu kompilacji.skutecznie skompiluje się do metody IL, która nic nie otrzymuje i zwraca 10.
zostanie przekonwertowany na strukturę danych opisującą wyrażenie, które nie otrzymuje parametrów i zwraca wartość 10:
większy obraz
Podczas gdy oba wyglądają tak samo w czasie kompilacji, to co generuje kompilator jest zupełnie inne .
źródło
Expression
zawiera meta-informacje o pewnym delegacie.Expression<Func<...>>
zamiast po prostuFunc<...>
.(isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }
takie wyrażenie jest ExpressionTree, gałęzie są tworzone dla instrukcji If.Dodałem odpowiedź za nooba, ponieważ te odpowiedzi wydawały mi się nad głową, dopóki nie zdałem sobie sprawy, jak to jest proste. Czasami twoje oczekiwania, że są skomplikowane, sprawiają, że nie możesz „owinąć głowy”.
Nie musiałem rozumieć różnicy, dopóki nie wpadłem w naprawdę irytujący „błąd”, próbując ogólnie użyć LINQ-to-SQL:
Działa to świetnie, dopóki nie zacząłem uzyskiwać wyjątków OutofMemory na większych zestawach danych. Ustawienie punktów przerwania w lambda uświadomiło mi, że iteruje się po każdym rzędzie w mojej tabeli, jeden po drugim, szukając dopasowania do mojego stanu lambda. To mnie zaskoczyło przez chwilę, bo dlaczego, do cholery, traktuje moją tabelę danych jako gigantyczną IEnumerable zamiast wykonywać LINQ-SQL tak, jak powinna? Robił też dokładnie to samo w moim odpowiedniku LINQ-to-MongoDb.
Rozwiązaniem było po prostu zamienić się
Func<T, bool>
wExpression<Func<T, bool>>
, więc wyszukałem w Google, dlaczegoExpression
zamiast tego potrzebujęFunc
, kończąc tutaj.Wyrażenie po prostu zamienia delegata w dane o sobie.
a => a + 1
Staje się więc czymś w rodzaju „Po lewej stronie jestint a
. Po prawej stronie dodajesz 1”. Otóż to. Możesz już iść do domu. Ma oczywiście bardziej uporządkowaną strukturę, ale w gruncie rzeczy jest to całe drzewo wyrażeń - nic, co mogłoby zawijać głowę.Zrozumienie tego staje się jasne, dlaczego LINQ-to-SQL potrzebuje
Expression
iFunc
nie jest wystarczające.Func
nie niesie ze sobą sposobu na dostanie się do samego siebie, zobaczenie drobiazgowego sposobu na przetłumaczenie go na zapytanie SQL / MongoDb / inne. Nie możesz sprawdzić, czy dokonuje dodawania, mnożenia czy odejmowania. Wszystko, co możesz zrobić, to uruchomić.Expression
, z drugiej strony, pozwala zajrzeć do wnętrza delegata i zobaczyć wszystko, co chce zrobić. To pozwala ci przetłumaczyć delegata na cokolwiek chcesz, na przykład zapytanie SQL.Func
nie działało, ponieważ mój DbContext był ślepy na treść wyrażenia lambda. Z tego powodu nie mógł przekształcić wyrażenia lambda w SQL; zrobiła jednak następną najlepszą rzecz i powtórzyła to warunkowe przez każdy wiersz w mojej tabeli.Edycja: wyjaśnienie mojego ostatniego zdania na prośbę Jana Piotra:
IQueryable rozszerza IEnumerable, więc metody IEnumerable, takie jak
Where()
uzyskiwanie przeciążeń, które akceptująExpression
. Kiedy przekazujeszExpression
do tego, zachowujesz IQueryable, w wyniku czego, przechodzisz zFunc
powrotem do podstawowej IEnumerable, w wyniku czego otrzymujesz IEnumerable. Innymi słowy, nie zauważając, że zmieniłeś swój zestaw danych w listę, która ma być iterowana, a nie w zapytaniu. Trudno zauważyć różnicę, dopóki nie spojrzy się pod maską na podpisy.źródło
Niezwykle ważną kwestią przy wyborze Expression vs. Func jest to, że dostawcy IQueryable, tacy jak LINQ to Entities, mogą „przetrawić” to, co przekazujesz w wyrażeniu, ale zignorują to, co przekazujesz w Func. Mam dwa posty na blogu na ten temat:
Więcej na temat Expression vs Func z Entity Framework i zakochania się w LINQ - Część 7: Expressions and Funcs (ostatnia sekcja)
źródło
Chciałbym dodać kilka uwag na temat różnic między
Func<T>
iExpression<Func<T>>
:Func<T>
jest po prostu zwykłą oldskulową MulticastDelegate;Expression<Func<T>>
jest reprezentacją wyrażenia lambda w formie drzewa wyrażeń;Func<T>
;ExpressionVisitor
;Func<T>
;Expression<Func<T>>
.Jest artykuł opisujący szczegóły z przykładowymi kodami:
LINQ: Func <T> vs. Expression <Func <T>> .
Mam nadzieję, że to będzie pomocne.
źródło
Istnieje bardziej filozoficzne wyjaśnienie na ten temat w książce Krzysztofa Cwaliny ( Wytyczne dotyczące projektowania ram: konwencje, idiomy i wzory dla bibliotek .NET wielokrotnego użytku );
Edycja dla wersji innej niż obraz:
źródło
database.data.Where(i => i.Id > 0)
zostać stracony jakoSELECT FROM [data] WHERE [id] > 0
. Jeśli po prostu przejść w Func, musisz umieścić blinders na sterowniku i wszystko może zrobić toSELECT *
, a następnie po jej załadowaniu wszystkich tych danych do pamięci, iterację każdego i odfiltrować wszystko z id> 0. Owijanie swojejFunc
wExpression
upoważnia sterownik do analizyFunc
i przekształcenia go w zapytanie Sql / MongoDb / inne.Expression
ale kiedy będę na wakacjach, będzieFunc/Action
;)LINQ jest przykładem kanonicznym (na przykład rozmowa z bazą danych), ale tak naprawdę, za każdym razem, gdy bardziej zależy ci na wyrażeniu tego, co robić, niż na tym, co faktycznie robisz. Na przykład używam tego podejścia w stosie RPC protobuf-net (aby uniknąć generowania kodu itp.) - więc wywołujesz metodę z:
To dekonstruuje drzewo wyrażeń do rozwiązania
SomeMethod
(i wartość każdego argumentu), wykonuje wywołanie RPC, aktualizuje dowolnyref
/out
args i zwraca wynik ze zdalnego wywołania. Jest to możliwe tylko za pośrednictwem drzewa wyrażeń. Omawiam to bardziej tutaj .Innym przykładem jest ręczne budowanie drzew wyrażeń w celu kompilacji do lambda, tak jak dzieje się to w kodzie operatorów ogólnych .
źródło
Użyłbyś wyrażenia, gdy chcesz traktować swoją funkcję jako dane, a nie kod. Możesz to zrobić, jeśli chcesz manipulować kodem (jako danymi). Przez większość czasu, jeśli nie widzisz potrzeby wyrażeń, prawdopodobnie nie musisz ich używać.
źródło
Głównym powodem jest to, że nie chcesz bezpośrednio uruchamiać kodu, ale chcesz go sprawdzić. Może to być z wielu powodów:
źródło
Expression
serializacja może być tak samo niemożliwa do serializacji jak delegata, ponieważ każde wyrażenie może zawierać wywołanie dowolnego odwołania do delegata / metody. „Łatwość” jest oczywiście względna.Nie widzę jeszcze odpowiedzi, które wspominałyby o wydajności. Przekazywanie
Func<>
s doWhere()
lubCount()
jest złe. Naprawdę źle. Jeśli użyjeszFunc<>
a, toIEnumerable
zamiast tego wywoła funkcję LINQIQueryable
, co oznacza, że całe tabele są pobierane, a następnie filtrowane.Expression<Func<>>
jest znacznie szybszy, szczególnie jeśli przeszukujesz bazę danych, która obsługuje inny serwer.źródło