LambdaCzęść NR.19Przykłady z poprzedniego wpisu dodają do delegaty konkretne metody. Dla przypomnienia prosty przykład użycia delegaty.

Jeśli Google odesłało cię tutaj po informacje o tym, jak korzystać z delegat, to polecam poprzedni wpis kursu.

Klasa MaszynaPracująca ma do dyspozycji pewien zestaw metod.

 class MaszynaPracujaca 
{
    public void UruchomMaszyne() 
    {
        Console.WriteLine("Maszyna Uruchomiona");
    }

    public void ObliczWzorZimnejFuzji() 
    {
        Console.WriteLine
        ("Obliczyłem wzór na zimną fuzje: X^6 + Lin(3) * G");
    }

    public void OdnajdzSensZycia()
    {
        Console.WriteLine("42");
    }

    public void WylaczMaszyne(int czasZamkniecta)
    {
        Console.WriteLine("Zamkniecie za {0}", czasZamkniecta);
    }
}

Metody te nie wykonują niczego praktycznego, ale to tylko przykład. A teraz mamy klasę “MaszyneZadan” i celem tej klasy jest uruchomienie zestawu metod z klasy “MaszynaPracująca”

class MaszynaZadan 
{
    public delegate void listZadanDelegata();
    private listZadanDelegata listaZadan;

    public MaszynaZadan(MaszynaPracujaca maszyna)
    {
        listaZadan += maszyna.UruchomMaszyne;
        listaZadan += maszyna.ObliczWzorZimnejFuzji;
        listaZadan += maszyna.OdnajdzSensZycia;
    }
    public void RozpocznijZadania()
    { 
        listaZadan(); 
    }
}

Jak widać nie ma tutaj żadnego problemu. Klasa działa jak należy.

MaszynaZadan mz = new MaszynaZadan(new MaszynaPracujaca());
mz.RozpocznijZadania();

Wynik z konsoli
Jak dobrze pamiętasz mówiłem też coś o zasadzie hermetyzacji czyli klasa MaszynaZadan jest chwilowo źle napisana ponieważ jej działanie jest zależne od klasy MaszynaPracująca. Problem ten można rozwiązać w następujący sposób. Sprawiając , że klasa MaszynaZadan może być elastycznie dostosowywana.

class MaszynaZadan 
{
    public delegate void listZadanDelegata();
    private listZadanDelegata listaZadan;

    public void Dodaj(listZadanDelegata MetodaX)
    {
        listaZadan += MetodaX;
    }
    public void Usun(listZadanDelegata MetodX)
    {
        listaZadan -= MetodX;
    }  

    public void RozpocznijZadania()
    {
        if (listaZadan != null)
            listaZadan();
    }
}

Umieściłem też mały warunek if przy wywołaniu instancji delegaty, ponieważ jeśli jest ona pusta, czyli nie ma przypisanych metod, to zwróci wyjątek NullReferenceException.

Kod w Program.cs w metodzie Main.

MaszynaPracujaca mp = new MaszynaPracujaca();
MaszynaZadan mz = new MaszynaZadan();

mz.Dodaj(mp.UruchomMaszyne);
mz.Dodaj(mp.OdnajdzSensZycia);
mz.Dodaj(mp.ObliczWzorZimnejFuzji);

mz.RozpocznijZadania();//wywołanie wszystkich metod 

Teraz kolejność wykonywania poleceń, jak i jakie polecenia będą wykonywane są zależne tylko od mnie. Jak widzisz kolejność wykonanych metod jest inna.

Rezultat
Hermetyzacja to jednak piękna zasada obiektowości.

Ale, okej to wszystko już było przejdźmy do nowego bajeru.

Może nie zauważałeś ,ale w klasie MaszynaPracująca jest metoda “WylaczMaszyne”. Abyś nie patrzył na kod tej klasy na samej górze tego wpisu. Oto jak ta metoda wygląda.

public void WylaczMaszyne(int czasZamkniecta)
{
    Console.WriteLine("Zamkniecie za {0}", czasZamkniecta);
}

Metoda ta przyjmuje parametr int,i jej kształt bądź jak wolisz sygnatura jest inna niż innych metod tej klasy. Ten kształt jest niezgodny z sygnaturą delegaty, która przyjmuje tylko metody bezparametrowe.

Błąd delegaty

Co w takim wypadku trzeba zrobić.
Zakładając oczywiście, że nie możemy tknąć tej metody w tej klasie z jakiegoś powodu.

Tworzenie Metody Adaptera

Problem ten można rozwiązać stosując adapter czyli kolejną metodę, która wywołuje tą nieszczęsną metodę, ale ze stałym parametrem 0.

public void NatchmiastoweWylaczenie()
{
    this.WylaczMaszyne(0);
}

Teraz taka metoda może być dodana do delegaty z bezparametrowymi metodami.

mz.Dodaj(mp.NatchmiastoweWylaczenie);
listaZadan += maszyna.NatchmiastoweWylaczenie;
//dla klasy która jest w wersji konstruktora

Oczywiście pojawiło się pytanie gdzie ta metoda adaptująca powinna być. Oczywiście jest ona w klasie “MaszynaPracująca” stąd to słowo kluczowe “this” jako wskazówka. Jednak co jeśli nie możemy zmienić zawartości tej klasy.

Nawet jeśli możemy zmienić zawartość tej klasy, to co jeśli ta klasa jest gigantyczna.

Tak czy siak, ta metoda istnieje tylko z powodu jednej delegaty i raczej na pewno nie będzie używana nigdzie indziej.

W C# do takich przypadków lepiej zastosować wyrażenie lambda, czyli naszą gwiazdę tego wpisu.

Wyrażenie Lambda

Wyrażenie lambda zwraca metodę.

Brzmi to fantastycznie, chociaż na pewno możesz się zastanawiać jako to w ogóle działa. Zwykle wyrażenia w C# zwracają jakąś wartość “X”.

W językach programowania funkcjonalnego jest to dość często spotykane zachowanie. Wraz z C# 3.0 pojawiły się wyrażenia lambda (jak i zapytania LINQ) i od tamtej pory twój kod miejscami może przypominać język funkcjonalny.

Oto mała informacja z Wikipedii z nagłówka “Functional programming in non-functional languages” we wpisie o językach programowania funkcjonalnego. Jak widzisz pewne trendy przechodzą nawet na języki programowania, które nie są funkcjonale i nie mówi się tu tylko o C#. Doszło to nawet do języka PHP, he…no tego bym się nie spodziewał. Kiedyś nie był to obiektowy język.

Z tego, co kiedyś czytałem w kolejnej wersji języka Java też mają pojawić się te wyrażenia.

Wyrażenia lambda są nawet obecne we wzorcach projektowych.

It is possible to use a functional style of programming in languages that are not traditionally considered functional languages.[36] Among imperative programming languages, the D programming language's solid support for functional programming stands out. For example, D has a pure modifier to enforce functional purity. Only Fortran 95 has something similar.[37]

First class functions have slowly been added to mainstream languages. For example, in early 1994, support for lambda, filter, map, and reduce was added to Python. Then during the development of Python 3000, Guido van Rossum called for the removal of these features. So far, only the function has been removed.[38] First class functions were also introduced in PHP 5.3, Visual Basic 9, C# 3.0, and C++0x.

The Language Integrated Query (LINQ) feature, with its many incarnations, is an obvious and powerful use of functional programming in .NET.

In Java, anonymous classes can sometimes be used to simulate closures;[citation needed] however, anonymous classes are not always proper replacements to closures because they have more limited capabilities.

Many object-oriented design patterns are expressible in functional programming terms: for example, the strategy pattern simply dictates use of a higher-order function, and the visitor pattern roughly corresponds to a catamorphism, or fold.
The benefits of immutable data can be seen even in imperative programs, so programmers often strive to make some data immutable even in imperative programs

Koniec jednak tej lektury. Wyrażenie lambda nie jest takie skomplikowane. To tylko kolejny zestaw znaków, które trzeba zapamiętać i tyle. Zresztą jest ono bardzo użyteczne , a więc okazja użycia pojawi się jeszcze nie raz.

Typowa metoda musi składać się z 4 elementów:

  • Nazwy
  • Listy parametrów
  • Ciała metody
  • Zwracanej wartości

Wyrażenie lambda zawiera tylko dwa elementy: listę parametrów oraz ciało metody. Wyrażenia Lambda nie określają swojej nazwy.

Wyrażenie lambda nie definiuje też zwrotnego parametru. Jednak jakiś parametr może być zwracany, jeśli wynika to z zawartości jej ciała.

Wracając do problemu z metodą “WyłaczMaszyne(int czasZamkniecia)” .

Musimy stworzyć adapter metody WylaczMaszyne w taki sposób ,aby nie miała ona żadnych parametrów i mogłaby być dodana do instancji delegaty “ListaZadan”.

Wyrażenia lambda do tego zadania wygląda tak:

mz.Dodaj( (() => { mp.WylaczMaszyne(0);}) );

listaZadan += (() => { maszyna.WylaczMaszyne(0); });
//dla klasy która jest w wersji konstruktora 

Cały tekst w nawiasie w metodzie "dodaj" jak i po znaku “+=” to właśnie wyrażenie lambda. Możesz tutaj zauważyć:

() => { maszyna.WylaczMaszyne(0); }

Listę parametrów w nawiasach. Jak w każdej metodzie, nawet jeśli ich nie ma musisz wciąż to zdefiniować.

() => { maszyna.WylaczMaszyne(0); }

Operator => informuje kompilator , że ma do czynienia z wyrażeniem lambda.

() => { maszyna.WylaczMaszyne(0); }

Ciało metody, które znajduje się wewnątrz nawiasów klamrowych. Ponieważ przykład zawiera tylko jedno wyrażenie nie zostało one podzielone na kilka linijek. Możesz jednak formatować ciało metody jak ci się podoba ważne, aby kod był czytelny ,a zarazem krótki. Trzeba pamiętać o średnikach w końcu tutaj wykonują się polecenia z C# ,a one zawsze kończą średnikami.

Teraz gdy uruchomisz delegate "listeZadan" to uruchomi się adapter, który napisaliśmy w wyrażeniu lambda.

Wyrażenie lambda chce więcej…Więcej Form

Wyrażenie lambda może mieć wiele form. W końcu reprezentują one częściowo matematyczną notacje, którą w programowaniu funkcjonalnym nazywa się “Rachunkiem Lambda” . Dostarcza ona nam notacje do opisywania funkcji. Funkcje w pewnym sensie to metody np. f(x) = x^2.

Wyrażenie lambda w C# to jednak coś więcej niż “rachunek lambda” jednak jej zasady są przestrzegane.

Oto różne wyrażenia lambda dostępne w C# wraz z ich sensownym użyciem. Aplikacja konsolowa.

using System;

namespace ConsoleApplication1
{
    public class Program
    {
        private delegate int operacjeMatematyczneDelegata(int x);

        private delegate int operacjeMatDwuArgDelegata(int x, int y);

        private delegate int operacjeMatTrojArgDelegata(ref int x, out int y, int z);

        private static void Main(string[] args)
        {
            operacjeMatematyczneDelegata operacjeMatematyczne = null;

            operacjeMatematyczne += (x => x*x);
            //Proste wyrażenie które zwraca kwadrat swojego parametru
            //Typ parametru x jest wnioskowany z kontekstu 
            //x można traktować jak zmienną pomocniczą
            //nie może ona istnieć w innym miejscu w kodzie 
            operacjeMatematyczne += (x => { return x*x; });
            //Wyrażenie które używa bloków z C#. 
            //Jest to już ciało niż proste wyrażenie 
            operacjeMatematyczne += ((int x) => x*x);

            //proste wyrażenie które zwraca potęgę liczby x 
            //parametr x jest określony jawnie 
            MaszynaPracujaca mp = new MaszynaPracujaca();
            MaszynaZadan mz2 = new MaszynaZadan();
            mz2.Dodaj((() => mp.WylaczMaszyne(0)));

            //Wywołanie metody 
            //Wyrażenie nie pobiera żadnych parametrów
            //Wyrażenie w zależności od metody może zwracać
            //bądź nie zwracać wartości. 
            operacjeMatDwuArgDelegata operacjeMatDwuArg = null;
            operacjeMatDwuArg += ((x, y) =>
                                      {
                                          x++;
                                          y--;
                                          return x/y;
                                      });

            //Wiele parametrów 
            //Wnioskuje typ parametrów z delegaty 
            //Parametry x i y są zmienione lokalnie 
            //Czyli operacja ta nie zmienia oryginalnych zmiennych 
            //przesłanych do delegaty
            operacjeMatTrojArgDelegata operacjeMatTrojArg = null;
            operacjeMatTrojArg += ((ref int x, out int y, int z) =>
                                       {
                                           x++;
                                           y = 10;
                                           return x/y + z;
                                       });

            //Wiele parametrów 
            //Zastosowanie słów kluczowy ref i out 
            //Parametr x jest przesłany jako referencja więc jego zmiana 
            //będzie permanentna 
            //Parametr y musi otrzymać jakąś wartość w trakcie działania 
            // metody i tą wartość zachowa 
            Console.WriteLine(operacjeMatematyczne(10));

            //wyświetli działanie metod które wykonują return  
            //lepiej było dodać tą linijkę kodu do każdego wyrażenia lambda 
            mz2.RozpocznijZadania();
            Console.WriteLine(operacjeMatDwuArg(9, 21));
            int x1 = 9;
            int y1;
            Console.WriteLine(operacjeMatTrojArg(ref x1, out y1, 1));
        }
    }
}


public class MaszynaPracujaca
{
    public void UruchomMaszyne()
    {
        Console.WriteLine("Maszyna Uruchomiona");
    }

    public void ObliczWzorZimnejFuzji()
    {
        Console.WriteLine("Obliczyłem wzór na zimną fuzje: X^6 + Lin(3) * G");
    }

    public void OdnajdzSensZycia()
    {
        Console.WriteLine("42");
    }

    public void WylaczMaszyne(int czasZamkniecta)
    {
        Console.WriteLine("Zamkniecie za {0}", czasZamkniecta);
    }
}

class MaszynaZadan
{
    public delegate void listZadanDelegata();
    private listZadanDelegata listaZadan;

    public void Dodaj(listZadanDelegata MetodaX)
    {
        listaZadan += MetodaX;
    }
    public void Usun(listZadanDelegata MetodX)
    {
        listaZadan -= MetodX;
    }

    public void RozpocznijZadania()
    {
        if (listaZadan != null)
            listaZadan();
    }
}

Czas na podsumowanie tego kodu i to nie za pomocą zielonego komentarza w kodzie.

  • Wyrażenia lambda mogą zwracać wartości jednak muszą one pasować do typu danej delegaty, do której są dodawane.
  • Ciało wyrażenia lambda może być prostym wyrażeniem albo blokiem kodu C# z wieloma stwierdzeniami, wezwaniami innych metod, definicją zmiennych i innych rzeczy.
  • Zmienne zdefiniowane wewnątrz wyrażenia lambda istnieją tylko w tym bloku kodu i znikają, gdy metoda się skończy,
  • Wyrażenie lambda ma dostęp do wszystkich zmiennych i metod znajdujących się poza tym wyrażeniem. W czasie wykonywania wyrażenia zmienne ulegają zmianie tymczasowo. Warto o tym pamiętać.
  • Jeśli wyrażenie lambda pobiera jakieś parametry możesz nie podawać ich typów ponieważ kompilator skojarzy je z kontekstu danego wyrażenia. Jednak przy słowach kluczowych jak ref i out musisz też podać ich typ.
  • Wyrażenie lambda może zmienić wartości na zawsze, jeśli są one przesłane do metody za pomocą słów kluczowych ref i out.

Metody anonimowe

W C# 2.0, czyli przed wyrażeniami lambda były metody anonimowe, które spełniały podobny cel do wyrażeń lambda, ale nie były tak elastyczne.

Anonimowe metody zostały dodane po to aby dać programistom możliwość definiowania delegat bez określenia ich nazw. Metody te zawierały w sobie tylko swoją definicję.

listaZadan += delegate { maszyna.WylaczMaszyne(0); };
mz.Dodaj(delegate { mp.WylaczMaszyne(0); });

Aby stworzyć metodę anonimową musisz użyć słowa kluczowego delegate.

Parametry też muszą być określone w nawiasach przy słowie kluczowym delegate.

operacjeMatDwuArg += delegate(int x, int y) { return x - y; };

Zostały jednak one osunięte w cień. Złożone zadania wyglądają o wiele lepiej w wyrażeniu lambda dlatego też już prawie nikt nie pamięta o tamtej formie zapisu. Chyba że ktoś z jakiegoś powodu musi programować w C# 2.0.

To taka ciekawostka, ponieważ wyrażenia lambda są lepsze. Może jednak kiedyś spotkasz się z takimi zapisami.

Co dalej:

Zobaczmy spis treści. Ponieważ sam nie wiem.