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>
?
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>
?
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 od Func<T>
. Func<T>
oznacza wskaźnik, delegate
który jest właściwie wskaźnikiem do metody i Expression<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.
Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }
skutecznie skompiluje się do metody IL, która nic nie otrzymuje i zwraca 10.
Expression<Func<int>> myExpression = () => 10;
zostanie przekonwertowany na strukturę danych opisującą wyrażenie, które nie otrzymuje parametrów i zwraca wartość 10:
Podczas gdy oba wyglądają tak samo w czasie kompilacji, to co generuje kompilator jest zupełnie inne .
Expression
zawiera meta-informacje o pewnym delegacie.
Expression<Func<...>>
zamiast po prostu Func<...>
.
(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:
public IEnumerable<T> Get(Func<T, bool> conditionLambda){
using(var db = new DbContext()){
return db.Set<T>.Where(conditionLambda);
}
}
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>
w Expression<Func<T, bool>>
, więc wyszukałem w Google, dlaczego Expression
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 jest int 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
i Func
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 przekazujesz Expression
do tego, zachowujesz IQueryable, w wyniku czego, przechodzisz z Func
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.
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)
Chciałbym dodać kilka uwag na temat różnic między Func<T>
i Expression<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.
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:
Najczęściej potrzebujesz Func lub Action, jeśli wszystko, co musi się wydarzyć, to uruchomić kod. Potrzebujesz wyrażenia, gdy kod musi zostać przeanalizowany, zserializowany lub zoptymalizowany przed jego uruchomieniem. Wyrażenie służy do myślenia o kodzie, Func / Action służy do jego uruchamiania.
database.data.Where(i => i.Id > 0)
zostać stracony jako SELECT FROM [data] WHERE [id] > 0
. Jeśli po prostu przejść w Func, musisz umieścić blinders na sterowniku i wszystko może zrobić to SELECT *
, a następnie po jej załadowaniu wszystkich tych danych do pamięci, iterację każdego i odfiltrować wszystko z id> 0. Owijanie swojej Func
w Expression
upoważnia sterownik do analizy Func
i przekształcenia go w zapytanie Sql / MongoDb / inne.
Expression
ale kiedy będę na wakacjach, będzie Func/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:
string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));
To dekonstruuje drzewo wyrażeń do rozwiązania SomeMethod
(i wartość każdego argumentu), wykonuje wywołanie RPC, aktualizuje dowolny ref
/ 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 .
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ć.
Głównym powodem jest to, że nie chcesz bezpośrednio uruchamiać kodu, ale chcesz go sprawdzić. Może to być z wielu powodów:
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 do Where()
lub Count()
jest złe. Naprawdę źle. Jeśli użyjesz Func<>
a, to IEnumerable
zamiast tego wywoła funkcję LINQ IQueryable
, 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.