niedziela, 13 października 2019

LINQ Aggregate

Wielu z Was zapewne nie spotyka często użycia funkcji Aggregate w kodzie. Nie ma w tym nic dziwnego, przeważnie używamy funkcji Select, Where, Sum, Average, Any, All, Count, First, Single itd. Aggregate jest gdzieś na samym końcu a nie rzadko w ogóle nie występuje w słowniku funkcji LINQ.

Warto poświęcić kilka minut i poznać jej możliwości. Definicja funkcji:


public static TResult Aggregate<TSource,TAccumulate,TResult> (this System.Collections.Generic.IEnumerable<TSource> source, 
TAccumulate seed, 
Func<TAccumulate,TSource,TAccumulate> func, 
Func<TAccumulate,TResult> resultSelector);



Wygląda skomplikowanie ale tak na prawdę nie ma tutaj nic nadzwyczajnego. Poszczególne parametry:
source - kolekcja wejściowa na której wykonujemy operacje
seed - wartość początkowa, można to porównać do zmiennej "sum", która za zadanie ma poinformować nas o sumie elementów np. tablicy. Przed sumowaniem ustawiamy wartość takiej zmiennej na 0. Tutaj mamy do czynienia z tym samym mechanizmem.
func - funkcja wywoływana dla każdego elementu kolekcji wejściowej. Posiada dwa argumenty wejściowe - zmienną akumulacyjną (czyli przykładowo zmienna sum) oraz element z kolekcji wejściowej.
resultSelector - pozwala dodatkowo zmodyfikować/przetransformować ostateczny rezultat

Zobaczmy na prosty przykład. Chcemy policzyć sumę liczb parzystych które otrzymujemy jako dane wejściowe w postaci tablicy. Dodatkowo każdą sumowaną liczbę podwoimy (wyobraźmy sobie, że w rzeczywistości może to być znacznie bardziej skomplikowany warunek. Przykładowa implementacja:


        private static long SumDoubleEvenNumbers(IEnumerable<int> numbers)
        {
            return numbers
                .Where(n => n % 2 == 0)
                .Select(n => n + n)
                .Sum(x => (long)x);
        }


Nic nadzwyczajnego. Zobaczmy jakby wyglądał kod z zastosowaną funkcją Aggregate:


        private static long SumDoubleEvenNumbers_Aggregate(IEnumerable<int> numbers)
        {
            return numbers
                .Aggregate(0L, (coutner, number) => coutner += number % 2 == 0 ? (number + number) : 0);
        }



Kod może wydawać się początkowo bardziej skomplikowany, ale de fakto nie ma tutaj nic trudnego. Na początku ustawiamy początkową wartość naszego akumulatora (zmiennej przechowującej wynik) na 0L co powoduje zastosowanie typu long. Następna część funkcji podstawia pod zmienną sumy (counter) kolejne liczby parzyste.

Zastanówmy się czy powyższy kod jest tylko zmianą kosmetyczną. Aby to sprawdzić dokonajmy pomiaru ile trwa wykonanie jednej i drugiej wersji programu:


        static void Main(string[] args)
        {
            var numbers = Enumerable.Range(1, 1000000);
            long sum;
            
            var stopwatch = Stopwatch.StartNew();
            sum = SumDoubleEvenNumbers(numbers);
            stopwatch.Stop();
            Console.WriteLine($"Sum: {sum}, ElapsedMiliseconds: {stopwatch.ElapsedMilliseconds}");
            Console.WriteLine();

            stopwatch = Stopwatch.StartNew();
            sum = SumDoubleEvenNumbers_Aggregate(numbers);
            stopwatch.Stop();
            Console.WriteLine($"Sum: {sum}, ElapsedMiliseconds: {stopwatch.ElapsedMilliseconds}");
            Console.WriteLine();
        }

        private static long SumDoubleEvenNumbers(IEnumerable<int> numbers)
        {
            return numbers
                .Where(n => n % 2 == 0)
                .Select(n => n + n)
                .Sum(x => (long)x);
        }

        private static long SumDoubleEvenNumbers_Aggregate(IEnumerable<int> numbers)
        {
            return numbers
                .Aggregate(0L, (coutner, number) => coutner += number % 2 == 0 ? (number + number) : 0);
        }



Wyniki:



W przypadku Aggregate mamy ponad dwukrotny wzrost wydajności. Dzieje się tak dlatego, że w przypadku poprzedniego kodu musiały zostać utworzone dodatkowe obiekty dla funkcji Where, Select. W przypadku Aggregate wszystko zostało scalone do jednej funkcji.

Brak komentarzy:

Prześlij komentarz