Dlaczego miałbyś używać wyrażenia <Func <T>> zamiast Func <T>?

948

Rozumiem lambda Funci Actiondelegatów. Ale wyrażenia mnie zaskakują.

W jakich okolicznościach użyłbyś Expression<Func<T>>raczej zwykłego niż starego Func<T>?

Richard Nagle
źródło
14
Func <> zostanie przekonwertowany na metodę na poziomie kompilatora c #, Wyrażenie <Func <>> zostanie wykonane na poziomie MSIL po bezpośredniej kompilacji kodu, dlatego jest szybszy
Waleed AK
1
oprócz odpowiedzi pomocna jest specyfikacja języka
csharp

Odpowiedzi:

1133

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, delegatektó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:

Expression vs. Func większy obraz

Podczas gdy oba wyglądają tak samo w czasie kompilacji, to co generuje kompilator jest zupełnie inne .

Mehrdad Afshari
źródło
95
Innymi słowy, an Expressionzawiera meta-informacje o pewnym delegacie.
bertl
39
@bertl Właściwie nie. Delegat w ogóle nie jest zaangażowany. Powodem jakiegokolwiek powiązania z delegatem jest to, że możesz skompilować wyrażenie do delegata - lub, mówiąc ściślej, skompilować je do metody i uzyskać delegata do tej metody jako wartość zwracaną. Ale samo drzewo wyrażeń to tylko dane. Delegat nie istnieje, gdy używasz Expression<Func<...>>zamiast po prostu Func<...>.
Luaan
5
@Kyle Delaney (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }takie wyrażenie jest ExpressionTree, gałęzie są tworzone dla instrukcji If.
Matteo Marciano - MSCP
3
@bertl Delegate to, co widzi CPU (kod wykonywalny jednej architektury), Expression to, co widzi kompilator (tylko inny format kodu źródłowego, ale nadal kod źródłowy).
codewarrior
5
@bertl: Można by to dokładniej podsumować, mówiąc, że wyrażenie jest func, czym jest budowniczy ciągów dla łańcucha. To nie jest ciąg / func, ale zawiera dane potrzebne do ich utworzenia, gdy zostanie o to poproszony.
Flater
336

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 Expressionzamiast tego potrzebuję Func, kończąc tutaj.

Wyrażenie po prostu zamienia delegata w dane o sobie. a => a + 1Staje 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 Expressioni Funcnie jest wystarczające. Funcnie 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. Funcnie 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 Expressiondo tego, zachowujesz IQueryable, w wyniku czego, przechodzisz z Funcpowrotem 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.

Chad Hedgcock
źródło
2
Czad; Wyjaśnij ten komentarz nieco bardziej: „Func nie działał, ponieważ mój DbContext był ślepy na to, co faktycznie było w wyrażeniu lambda, aby przekształcić go w SQL, więc zrobił następną najlepszą rzecz i powtórzył to warunkowe przez każdy wiersz w mojej tabeli . ”
John Peters
2
>> Func ... Wszystko, co możesz zrobić, to uruchomić. To nie do końca prawda, ale myślę, że należy to podkreślić. Funkcje / akcje mają być uruchamiane, wyrażenia należy analizować (przed uruchomieniem lub nawet zamiast uruchomienia).
Konstantin
@Chad Czy problem polegał na tym, że ?: db.Set <T> sprawdził całą tabelę bazy danych, a następnie, ponieważ .Where (conditionLambda) użył metody rozszerzenia Where (IEnumerable), która jest wyliczana na całej tabeli w pamięci . Myślę, że otrzymujesz OutOfMemoryException, ponieważ ten kod próbował załadować całą tabelę do pamięci (i oczywiście stworzył obiekty). Czy mam rację? Dzięki :)
Bence Végert
104

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)

LSpencer777
źródło
+ l dla wyjaśnienia. Jednak otrzymuję komunikat „Wywołanie typu węzła wyrażenia LINQ„ Invoke ”nie jest obsługiwane w LINQ to Entities.” i musiałem użyć ForEach po pobraniu wyników.
tymtam
77

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ń;
  • drzewo wyrażeń można konstruować poprzez składnię wyrażeń lambda lub składnię API;
  • drzewo wyrażeń można skompilować do delegata Func<T>;
  • odwrotna konwersja jest teoretycznie możliwa, ale jest to rodzaj dekompilacji, nie ma wbudowanej funkcjonalności, ponieważ nie jest to prosty proces;
  • drzewo wyrażeń można obserwować / tłumaczyć / modyfikować poprzez ExpressionVisitor;
  • metody rozszerzenia dla IEnumerable działają z Func<T>;
  • metody rozszerzenia dla IQueryable działają z 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.

Oleksander Iwanicki
źródło
Ładna lista, jedna mała uwaga: wspominasz, że konwersja odwrotna jest możliwa, jednak dokładna odwrotność nie jest możliwa. Niektóre metadane zostały utracone podczas procesu konwersji. Można go jednak zdekompilować do drzewa wyrażeń, które daje ten sam wynik po ponownej kompilacji.
Aidiakapi
76

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 );

Rico Mariani

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.

Oğuzhan Soykan
źródło
10
Dobrze wyłożone. to znaczy. Potrzebujesz wyrażenia, gdy spodziewasz się przekształcenia Func w jakieś zapytanie. To znaczy. musisz 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 Funcw Expressionupoważnia sterownik do analizy Funci przekształcenia go w zapytanie Sql / MongoDb / inne.
Chad Hedgcock
Więc kiedy planuję wakacje, skorzystam, Expressionale kiedy będę na wakacjach, będzie Func/Action;)
GoldBishop
1
@ChadHedgcock To był ostatni kawałek, którego potrzebowałem. Dzięki. Patrzę na to od dłuższego czasu, a Twój komentarz tutaj sprawił, że całe badanie kliknęło.
Johnny
37

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/ outargs 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 .

Marc Gravell
źródło
20

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ć.

Andrew Hare
źródło
19

Głównym powodem jest to, że nie chcesz bezpośrednio uruchamiać kodu, ale chcesz go sprawdzić. Może to być z wielu powodów:

  • Mapowanie kodu do innego środowiska (np. Kod C # na SQL w Entity Framework)
  • Zamiana części kodu w środowisku wykonawczym (programowanie dynamiczne lub nawet zwykłe techniki DRY)
  • Sprawdzanie poprawności kodu (bardzo przydatne podczas emulacji skryptów lub analizy)
  • Serializacja - wyrażenia można serializować raczej łatwo i bezpiecznie, delegaci nie
  • Silnie wpisane bezpieczeństwo rzeczy, które nie są silnie wpisane i wykorzystujące sprawdzanie kompilatora, nawet jeśli wykonujesz dynamiczne wywołania w czasie wykonywania (ASP.NET MVC 5 z Razor jest dobrym przykładem)
Luaan
źródło
czy możesz rozwinąć nieco więcej na temat nr 5
uowzd01,
@ uowzd01 Wystarczy spojrzeć na Razor - używa tego podejścia w szerokim zakresie.
Luaan,
@Luaan Szukam serializacji wyrażeń, ale nie mogę nic znaleźć bez ograniczonego użycia przez osoby trzecie. Czy .Net 4.5 obsługuje serializację drzewa wyrażeń?
vabii,
@vabii Nie wiem o tym - i tak naprawdę nie byłby to dobry pomysł w ogólnym przypadku. Chodzi mi bardziej o to, że jesteś w stanie napisać dość prostą serializację dla konkretnych przypadków, które chcesz wspierać, w porównaniu z interfejsami zaprojektowanymi wcześniej - zrobiłem to już kilka razy. W ogólnym przypadku Expressionserializacja 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.
Luaan,
15

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 IEnumerablezamiast 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.

mhenry1384
źródło
Czy dotyczy to również zapytania w pamięci?
stt106
@ stt106 Prawdopodobnie nie.
mhenry1384
Dzieje się tak tylko wtedy, gdy wyliczasz listę. Jeśli użyjesz GetEnumerator lub foreach, nie załadujesz w pełni licznika do pamięci.
nelsontruran
1
@ stt106 Po przekazaniu do klauzuli .Where () List <>, wyrażenie <Func <>> pobiera wywołanie .Compile (), więc Func <> jest prawie na pewno szybszy. Patrz referenceource.microsoft.com/#System.Core/System/Linq/…
NStuke