Dlaczego nie można przypisać metody anonimowej do zmiennej?

139

Mam następujący kod:

Func<string, bool> comparer = delegate(string value) {
    return value != "0";
};

Jednak następujące elementy nie są kompilowane:

var comparer = delegate(string value) {
    return value != "0";
};

Dlaczego kompilator nie może dowiedzieć się, że jest to plik Func<string, bool>? Pobiera jeden parametr łańcuchowy i zwraca wartość logiczną. Zamiast tego daje mi błąd:

Nie można przypisać metody anonimowej do zmiennej lokalnej o niejawnym typie.

Zgaduję, że jeśli skompilowano wersję var , brakowałoby spójności, gdybym miał:

var comparer = delegate(string arg1, string arg2, string arg3, string arg4, string arg5) {
    return false;
};

Powyższe nie miałoby sensu, ponieważ Func <> pozwala tylko na 4 argumenty (w .NET 3.5, którego używam). Być może ktoś mógłby wyjaśnić problem. Dzięki.

Marlon
źródło
3
Uwaga dotycząca 4 argumentów , w .NET 4, Func<>akceptuje do 16 argumentów.
Anthony Pegram
Dziękuję za wyjaśnienie. Używam .NET 3.5.
Marlon
9
Dlaczego kompilator miałby myśleć, że to Func<string, bool>? Converter<string, bool>Dla mnie wygląda na to!
Ben Voigt,
3
czasami tęsknię za VB ..Dim comparer = Function(value$) value <> "0"
Slai

Odpowiedzi:

155

Inni już zauważyli, że istnieje nieskończenie wiele możliwych typów delegatów, o których mogliście mieć na myśli; co jest takiego specjalnego Func, że zasługuje na domyślny zamiast Predicatelub Actionczy jakakolwiek inna możliwość? A w przypadku lambd, dlaczego jest oczywiste, że intencją jest wybór formy delegata, a nie formy drzewa wyrażeń?

Ale możemy powiedzieć, że Funcjest to coś specjalnego i że wywnioskowanym typem lambda lub metody anonimowej jest Func czegoś. Nadal mielibyśmy różnego rodzaju problemy. Jakie typy chciałbyś wywnioskować w następujących przypadkach?

var x1 = (ref int y)=>123;

Nie ma Func<T>typu, który cokolwiek przyjmowałby ref.

var x2 = y=>123;

Nie znamy typu parametru formalnego, chociaż znamy zwrot. (A może my? Czy zwrot jest int? Long? Short? Byte?)

var x3 = (int y)=>null;

Nie znamy typu zwrotu, ale nie może to być nieważne. Typ zwracany może być dowolnym typem referencyjnym lub dowolnym typem wartości dopuszczającej wartość null.

var x4 = (int y)=>{ throw new Exception(); }

Ponownie nie znamy typu zwracanego, a tym razem może być on nieważny.

var x5 = (int y)=> q += y;

Czy ma to być wyrażenie lambda zwracające void, czy coś, co zwraca wartość, która została przypisana do q? Obie są legalne; który powinniśmy wybrać?

Teraz możesz powiedzieć, cóż, po prostu nie obsługuj żadnej z tych funkcji. Po prostu obsługuj „normalne” przypadki, w których typy można opracować. To nie pomaga. Jak to ułatwia mi życie? Jeśli ta funkcja czasami działa, a czasami zawodzi, nadal muszę napisać kod, aby wykryć wszystkie te sytuacje awaryjne i podać znaczący komunikat o błędzie dla każdego. Nadal musimy określić całe to zachowanie, udokumentować je, napisać testy i tak dalej. Jest to bardzo kosztowna funkcja, która oszczędza użytkownikowi może pół tuzina naciśnięć klawiszy. Mamy lepsze sposoby na dodanie wartości do języka niż spędzanie dużo czasu na pisaniu przypadków testowych dla funkcji, która nie działa przez połowę czasu i nie zapewnia prawie żadnych korzyści w przypadkach, gdy działa.

Sytuacja, w której jest to rzeczywiście przydatne, to:

var xAnon = (int y)=>new { Y = y };

ponieważ nie ma „wypowiadalnego” typu dla tej rzeczy. Ale cały czas mamy ten problem i po prostu używamy wnioskowania o typie metody, aby wydedukować typ:

Func<A, R> WorkItOut<A, R>(Func<A, R> f) { return f; }
...
var xAnon = WorkItOut((int y)=>new { Y = y });

a teraz wnioskowanie o typie metody sprawdza, czym jest typ func.

Eric Lippert
źródło
43
Kiedy zamierzasz zebrać odpowiedzi SO w książce? Kupiłbym to :)
Matt Greer
13
Popieram propozycję książki Erica Lipperta zawierającej odpowiedzi SO. Sugerowany tytuł: „Refleksje ze stosu”
Adam Rackis,
24
@Eric: Dobra odpowiedź, ale zilustrowanie tego jako czegoś, co nie jest możliwe, jest nieco mylące, ponieważ w rzeczywistości działa to całkowicie dobrze w D.Tyle, że nie zdecydowaliście się nadać delegatom ich własnego typu, a zamiast tego sprawili, że są zależni w ich kontekście ... więc IMHO bardziej niż cokolwiek innego powinno brzmieć „ponieważ tak to zrobiliśmy”. :)
user541686
5
@abstractdissonance Zwracam również uwagę, że kompilator jest open source. Jeśli zależy Ci na tej funkcji, możesz poświęcić niezbędny czas i wysiłek, aby to się stało. Zachęcam do przesłania pull requesta.
Eric Lippert
7
@AbstractDissonance: Zmierzyliśmy koszt pod kątem ograniczonych zasobów: deweloperów i czasu. Ta odpowiedzialność nie została nałożona przez boga; narzucił go wiceprezes pionu deweloperskiego. Pomysł, że zespół C # może w jakiś sposób zignorować proces budżetowy, jest dziwny. Zapewniam, że kompromisy były i nadal są dokonywane dzięki starannej, przemyślanej analizie ekspertów, którzy wyrazili życzenia społeczności C #, strategicznej misji firmy Microsoft oraz własnym, doskonałym gustem projektowym.
Eric Lippert
29

Tylko Eric Lippert wie na pewno, ale myślę, że to dlatego, że podpis typu delegata nie określa jednoznacznie typu.

Rozważ swój przykład:

var comparer = delegate(string value) { return value != "0"; };

Oto dwa możliwe wnioski dotyczące tego, co varpowinno być:

Predicate<string> comparer  = delegate(string value) { return value != "0"; };  // okay
Func<string, bool> comparer = delegate(string value) { return value != "0"; };  // also okay

Który z nich powinien wywnioskować kompilator? Nie ma dobrego powodu, aby wybrać jedną lub drugą. I chociaż a Predicate<T>jest funkcjonalnym odpowiednikiem a Func<T, bool>, to wciąż są różnymi typami na poziomie systemu typów .NET. W związku z tym kompilator nie może jednoznacznie rozstrzygnąć typu delegata i musi zakończyć się niepowodzeniem wnioskowania o typie.

itowlson
źródło
1
Jestem pewien, że sporo osób w firmie Microsoft również wie na pewno. ;) Ale tak, nawiązujesz do głównego powodu, nie można określić typu czasu kompilacji, ponieważ nie ma go. Sekcja 8.5.1 specyfikacji języka szczególnie podkreśla powód, dla którego nie zezwala się na używanie funkcji anonimowych w deklaracjach zmiennych z niejawną typizacją.
Anthony Pegram
3
Tak. A co gorsza, w przypadku lambd nie wiemy nawet, czy będzie to typ delegata; może to być drzewo wyrażeń.
Eric Lippert
Dla wszystkich zainteresowanych, napisałem się nieco więcej na ten temat i jak C # i F # zbliża kontrast w mindscapehq.com/blog/index.php/2011/02/23/...
itowlson
dlaczego kompilator nie może po prostu stworzyć nowego unikalnego typu, takiego jak C ++ robi to dla swojej funkcji lambda
Weipeng L
Czym się różnią „na poziomie systemu typu .NET”?
arao6
6

Eric Lippert ma na ten temat stary post , w którym mówi

I faktycznie specyfikacja C # 2.0 to wywołuje. Wyrażenia grupy metod i anonimowe wyrażenia metod są wyrażeniami bez typu w języku C # 2,0, a wyrażenia lambda dołączają do nich w języku C # 3,0. Dlatego nielegalne jest ich pojawienie się „nagich” po prawej stronie ukrytej deklaracji.

Brian Rasmussen
źródło
Jest to podkreślone w sekcji 8.5.1 specyfikacji języka. „Wyrażenie inicjalizatora musi mieć typ czasu kompilacji”, aby mogło być użyte dla niejawnie wpisanej zmiennej lokalnej.
Anthony Pegram
5

Różni delegaci są uważani za różne typy. np. Actionjest inny niż MethodInvoker, a wystąpienia Actionnie można przypisać do zmiennej typu MethodInvoker.

Tak więc, biorąc pod uwagę anonimowego delegata (lub lambda) () => {}, czy jest to an Actionczy a MethodInvoker? Kompilator nie może powiedzieć.

Podobnie, jeśli zadeklaruję typ delegata pobierającego stringargument i zwracającego a bool, skąd kompilator będzie wiedział, że naprawdę chcesz mieć Func<string, bool>typ delegata zamiast mojego typu delegata? Nie może wywnioskować typu delegata.

Stephen Cleary
źródło
2

Poniższe punkty pochodzą z MSDN w odniesieniu do zmiennych lokalnych wpisanych niejawnie:

  1. var może być używane tylko wtedy, gdy zmienna lokalna jest zadeklarowana i zainicjowana w tej samej instrukcji; zmienna nie może zostać zainicjowana na wartość null ani na grupę metod lub funkcję anonimową.
  2. Słowo kluczowe var instruuje kompilator, aby wywnioskował typ zmiennej na podstawie wyrażenia po prawej stronie instrukcji inicjalizacji.
  3. Ważne jest, aby zrozumieć, że słowo kluczowe var nie oznacza „wariant” i nie oznacza, że ​​zmienna jest wpisana luźno lub z opóźnieniem. Oznacza to po prostu, że kompilator określa i przypisuje najbardziej odpowiedni typ.

Dokumentacja MSDN: zmienne lokalne wpisane niejawnie

Biorąc pod uwagę następujące kwestie dotyczące metod anonimowych:

  1. Metody anonimowe umożliwiają pominięcie listy parametrów.

Dokumentacja MSDN: metody anonimowe

Podejrzewałbym, że skoro metoda anonimowa może w rzeczywistości mieć różne sygnatury metody, kompilator nie jest w stanie poprawnie wywnioskować, jaki byłby najbardziej odpowiedni typ do przypisania.

nybbler
źródło
1

Mój post nie odpowiada na rzeczywiste pytanie, ale odpowiada na podstawowe pytanie:

„Jak uniknąć konieczności wpisywania jakiegoś niezręcznego typu Func<string, string, int, CustomInputType, bool, ReturnType>?” [1]

Będąc leniwym / hackim programistą, eksperymentowałem z użyciem Func<dynamic, object>- który przyjmuje pojedynczy parametr wejściowy i zwraca obiekt.

W przypadku wielu argumentów możesz użyć tego w następujący sposób:

dynamic myParams = new ExpandoObject();
myParams.arg0 = "whatever";
myParams.arg1 = 3;
Func<dynamic, object> y = (dynObj) =>
{
    return dynObj.arg0.ToUpper() + (dynObj.arg1 * 45); //screw type casting, amirite?
};
Console.WriteLine(y(myParams));

Wskazówka: możesz użyć, Action<dynamic>jeśli nie musisz zwracać obiektu.

Tak, wiem, że prawdopodobnie jest to sprzeczne z zasadami programowania, ale ma to sens dla mnie i prawdopodobnie dla niektórych programistów Pythona.

Jestem całkiem nowicjuszem w delegatach ... po prostu chciałem podzielić się tym, czego się nauczyłem.


[1] Zakłada się, że nie wywołujesz metody, która wymaga predefiniowanego Funcparametru, w takim przypadku będziesz musiał wpisać ten fugly string: /

Ambrose Leung
źródło
0

A co z tym?

var item = new
    {
        toolisn = 100,
        LangId = "ENG",
        toolPath = (Func<int, string, string>) delegate(int toolisn, string LangId)
        {
              var path = "/Content/Tool_" + toolisn + "_" + LangId + "/story.html";
              return File.Exists(Server.MapPath(path)) ? "<a style=\"vertical-align:super\" href=\"" + path + "\" target=\"_blank\">execute example</a> " : "";
        }
};

string result = item.toolPath(item.toolisn, item.LangId);
mmm
źródło