sobota, 28 sierpnia 2010

Wyrażenia lambda

Wyrażenia lambda pozwalają na uproszczenie zapisu do wymaganego minimum. Zobaczymy na przykładzie jak od zwykłej metody przejść do wyrażenia lambda.

Mamy następujący problem: chcemy w naszej liście znaleźć tylko parzyste liczby i zwrócić je jako nowa lista. Aby łatwo wykonać to zadanie możemy skorzystać z metody FindAll które przyjmuje jako argument typ Predicate(bool (T)target). Predicate jest tutaj delegatem przyjmującą jeden parametr i zwracającą bool. Tak więc zaczynajmy od najprostszej wersji:

    class Program
    {
        static void Main(string[] args)
        {
            List<int> intList = new List<int>
            {
                1,2,3,4,5,6,7,8,9,10
            };

            List<int> matchedInts = intList.FindAll(new Predicate<int>(IsEven));
            foreach (var item in matchedInts)
            {
                Console.WriteLine(item);
            }
        }

        private static bool IsEven(int i)
        {
            return i % 2 == 0;
        }
    }

Pytanie tylko czy takie rozwiązanie jest nam na rękę? Metodę IsEven wykorzystamy tylko raz w naszym programie więc po co ją zapisywać globalnie? C# umożliwia skorzystanie z metod anonimowych tak więc powyższą funkcję możemy zapisać jako:

    class Program
    {
        static void Main(string[] args)
        {
            List<int> intList = new List<int>
            {
                1,2,3,4,5,6,7,8,9,10
            };

            List<int> matchedInts = intList.FindAll(delegate(int i)
            {
                return i % 2 == 0;
            });

            foreach (var item in matchedInts)
            {
                Console.WriteLine(item);
            }
        }
    }

Rozwiązanie całkiem niezłe, jednak można jeszcze lepiej za pomocą wyrażeń lambda:

    class Program
    {
        static void Main(string[] args)
        {
            List<int> intList = new List<int>
            {
                1,2,3,4,5,6,7,8,9,10
            };

            List<int> matchedInts = intList.FindAll(i => { return i % 2 == 0; });

            foreach (var item in matchedInts)
            {
                Console.WriteLine(item);
            }
        }
    }

Całkiem nieźle. Dużo mniej kodu a taki sam efekt. Czas na składnię:
lista parametrów => instrukcje
i => {return i % 2 == 0;}

Teraz kilka faktów:
Jeżeli delegat nie przyjmuje parametrów nasze wyrażenie zapisujemy jako:
() => instrukcje
Jeżeli delegat przyjmuje więcej niż 1 parametr, delegatę zapisujemy jako:
(i, j, ...) => instrukcje
Jeżeli parametry nie można zidentyfikować po typie, możemy je przekazać jawnie:
(int i, string j) => instrukcje


C# dostarcza nam predefiniowane delegaty:
Func - jest to generyczny delegat, przyjmujący do 9 parametrów i zwracający TRes. Przykład:

            Func<int, int, int> AddTwoIntegers = (a, b) => { return a + b; };
            Console.WriteLine(AddTwoIntegers(5, 5));
Action - generyczna akcja przyjmująca do 16 parametrów i zwracajaca void:

            Action<int> action = x => { x = x * 5; Console.WriteLine(x); };
            action(10);
Predicate - jest to generyczny delegat przyjmujący jeden parametr i zwracający bool:

            Predicate<int> predicate = x => { return x % 2 == 0; };
            Console.WriteLine(predicate(21));

Zobaczmy jeszcze na jeden aspekt:
Podczas debugowania naszych metod możemy uświadczyć następującego widoku:

Metoda jest przekształcana do strumienia, można w tym przypadku wykorzystać klasę Expression i otrzymać bardziej czytelniejszy kod:


Jednym aspektem, który z pewnością nas zmartwi jest to, iż nie każdy Func może zostać przekształcony do drzewa decyzyjnego. Niektóre przykłady musimy sami przekształcać.

2 komentarze: