#3 LambdaCzęść NR.3

Nie wszystkie funkcje są tak ważne, że muszą mieć nazwę. C# wspiera anonimowe funkcje od wersji 2.0. A od wersji 3.0 anonimowe funkcje pojawiły się w lepszej wersji. W wersji wyrażeń lambda.

Te funkcje nie żyją na poziomie klas i dlatego nie mają nazw. Referencje do takich funkcji zazwyczaj przetrzymuje się w postaci zmiennych typu danej delegaty.

Istnieją pewne ograniczenia dotyczące tego, gdzie anonimowe funkcje mogą się znajdować i jakie mają być.

Przykładowo nie mogą one być generyczne.

Nie mogą być użyte do implementacji iteratorów.

Poza tym anonimowe funkcję mogą robić to samo, co normalne metody.

Oto jak wygląda składnia anonimowej funkcji według standardów C# 2.0.

static void AnonymousMethodsExample()
{
    MatchMaker.IsMatch matchInt =
    delegate (object a, object b) {
        return ((int)a) > ((int)b);
    };
}

Jak widzisz słowo kluczowe delegate jest użyte zamiast nazwy metody. Lista parametrów i ciało wyglądają tak samo jak w metodzie.

Zmienna jest użyta do przetrzymania referencji do metody.

Ta sama anonimowa metoda może być napisana według standardów C# 3.0, czyli z wyrażeniem lambda.

MatchMaker.IsMatch matchInt2 =
(object a, object b) => { return ((int)a) == ((int)b); };

To wyrażenie jest krótsze ponieważ nie musimy pisać słowa kluczowego delegate i całe ciało zostało sformatowane do jednej linijki.

Wyrażenie lambda określa się operatorem =>. Po lewej mamy w nawisach parametry, po prawej ciało operacji.

Biorąc pod uwagę, że typ przesłanych parametrów nas nie interesuje, to wyrażenie możemy skrócić do takiej postaci.

MatchMaker.IsMatch compareInt2 =
(a, b) => { return ((int)a) == ((int)b); };

Wyrażenie te możemy jeszcze bardziej przekształcić.

Całe ciało funkcji możemy zmienić na wyrażenie “Expression Body”. Nie mam pojęcia jak to przetłumaczyć. Oto więc nasze ciało wyrażenia.

MatchMaker.IsMatch matchInt3 =
(a, b) => ((int)a) == ((int)b);

Wyrażenie to jest bardzo przydatne, gdyż koncentruje się na zachowaniu.

Expression body może zawierać tylko kod, który koncentruje się na wartości zwracanej.

image

Chociaż jest możliwe napisanie takiego wyrażenia, które nie będzie zwracało żadnej wartości.

Wyrażenia lambda dają spore możliwości, zwłaszcza dzięki nim powstała kolejna fajna funkcjonalność, którą określa się pod nazwą Expression Trees. Jak się domyślasz ma to duży związek właśnie z wyrażeniami Expression Body. Otóż całe wyrażenie może zostać przeanalizowane.

Mogę np. z takiego wyrażenia pobrać nazwy parametrów. Na razie jednak nie będę opowiadał o Expression Trees.

W poprzednim wpisie użyliśmy delegaty, która miała zadeklarowany typ. W .NET 3.5 pojawiło się dużo gotowych delegat. Co oznacza, że nie musimy tworzyć swoich własnych delegat w C#.

Te delegaty są generyczne. Jedynym powodem do tworzenie własnych delegat jest ich nazewnictwo. W praktyce dzisiaj już swoich delegat się nie tworzy.

Oto więc przykład gotowej delegaty Func.

Func<object, object, bool> compareInt4 =
(a, b) => ((int)a) == ((int)b);

Na początku w tym typie generycznym podaję typy parametrów, a na końcu typ zwracany.

Przewaga typów generycznych polega na łatwym zauważeniu, jakie typy występują w tej delegacie.

Nie możesz użyć typu VAR przy wyrażeniach lambda. Kompilator C# musi wiedzieć z jakimi typami będzie działać na poziomie deklaracji. Co jest niemożliwe z takim wyrażeniem.

image

Język funkcjonalny jak Haskell posiada konstrukcje, która nazywa się TypeClasses.

Pozwala ona zdefiniować coś takiego: “ten element może zostać pomnożony przez inny element”. W przykładzie powyżej Haskell umiałby powiedzieć, że typ x i y muszą wykonać operację mnożenia, ponieważ typ klasy na to pozwala. Przypomina to interfejsy, ale nie jest to ta sama rzecz.

Inne języki funkcjonalne wspierają taki scenariusz, w C# jest to błąd kompilacyjny.

Nawet gdy podam typ parametrów, to typ VAR nie da rady.

image

Dlaczego tak jest. Drużyna C# zdecydowała, że kompilator w takim scenariuszu nie ma możliwości określenia, czy typ zwracany powinien być typu Func<int,int,int>, czy typu Expression<Func<int,int,int>> (ten typ wspiera wyrażenia drzewiaste).

W C# istnieje też problem z delegatami, które pomimo podobnej sygnatury nie są kompatybilne ze sobą.

private static void DeclaringLambdas()
{
    Func<int, int, int> add = (a, b) => a + b;
    //Błąd
    TakeMethod((StringStringStringDelegate)add);
}
delegate string StringStringStringDelegate(string a, string b);
private static void TakeMethod(StringStringStringDelegate del)
{
}

W obiektowym świecie ma to sens, ale funkcjonalnym już nie.

image

Niektórzy programiści chcieliby aby C# zakładał, w takich wpadkach, jaki typ powinien być zwracany, albo powinien być jakiś mechanizm, w którym byśmy określali, jaki typ ma być zwracany.