Tutorial LINQ w C# i .NET 4.5 na przykładzie operatora Min

LINQ to rozszerzenie języka C# umożliwiające zadawanie zapytań do tablic i innych rodzajów kolekcji w sposób znany z języka SQL. Przyśpiesza to znacząco pisanie kodu oraz skraca konieczny kod do wykonania danego zadania.

LINQ oferuje wiele, wiele operatorów, którymi możemy budować nasze zapytania. Ja w poniższym tekście tylko pokażę możliwości tego rozszerzenia, na przykładzie operatora Min, którym możemy odnajdywać wartości minimalne w kolekcji – w dowolnym znaczeniu słowa ‘minimalne’.

Pierwszy przykład jest bardzo prosty. Po nim nawet nie widać, że zastosowano tu składnię LINQ. Kod odnajduje po prostu minimalną wartość w tablicy liczb. W tym przypadku kod wypisze na konsolę liczbę 3.

public void linq1() {
    int[] numbers = { 3, 5, 6, 7, 3, 9 };
    int minNum = numbers.Min();
    Console.WriteLine("Najmniejszą liczbą jest {0}.", minNum);
}

To było bardzo proste. Teraz bardziej ciekawy przykład. Ciekawszy, po pierwsze dlatego, że będziemy wywoływać operator Min na stringach, gdzie będziemy definiować, jak rozumiemy to, że jeden string jest mniejszy od drugiego. Po drugie, ciekawszy dlatego, że będziemy stosować do tego notację lambda.

Nie będę się tu teraz rozpisywał o notacji lambda, ale powiem tylko, że jest to sposób na bardzo skrótowy zapis funkcji. Dla przykładu zapis:

w => w.Length

oznacza skrócony zapis funkcji, która na wejściu bierze jakiś obiekt o nazwie w, a na wyjściu daje w.Length, czyli zwraca wartość Length tego obiektu. Można by to zapisać tak:

int fun(Object w) {
    return w.length;
}

Dobrze. Teraz wróćmy do naszego operatora. Rozpatrzmy poniższy kod.

public void linq2()
{
    string[] words = { "owoc", "warzywo", "sok" };
    int shortestWord = words.Min(w => w.Length);
    Console.WriteLine("Najkrótsze słowo ma {0} liter.", shortestWord);
}

Sytuacja jest podobna, jak w pierwszym przykładzie. Różnica polega na zastosowaniu operatora Min. Dostaje on w argumencie notację lambda, czyli innymi słowy, przekazujemy w argumencie jakąś funkcję – funkcję do porównywania, czy coś jest większe, czy mniejsze od drugiego. Na tej podstawie określany jest minimalny składnik. Jako, że przekazane wyrażenie lambda, dla każdego elementu zwraca jego długość, więc w rezultacie najmniejszy element to taki, który ma najmniejszą długość.

Teraz już będzie trudniejszy przykład. Zastosujemy tu kolejną notację LINQ. Rozważmy przykład, a zaraz go szczegółowo omówimy.

public void linq3(){
    List<Product> products = GetProductList();
 
    var categories =
        from p in products
        group p by p.Category into g
        select new { Category = g.Key, CheapestPrice = g.Min(p => p.UnitPrice) };
 
    ObjectDumper.Write(categories);
}

Najpierw mamy:

List<Product> products = GetProductList();

Jest to jakaś wyimaginowana metoda, która ma zwrócić nam jakąś listę zawierającą obiekty reprezentujące produkty, które to mają różne atrybuty, np. cenę, kategorię itd. Dalej mamy:

var categories =

czyli deklarujemy sobie jaką zmienną (bez określania jej konkretnego typu – jak np. w PHP). Dalej jest:

from p in products

czyli zapis podobny, jak ten znany z pętli foreach, gdzie mówimy, że iterujemy po kolekcji (tablicy) products, a każdy kolejny element tej tablicy jest dostępny za pomocą zmiennej p. Dalej mamy:

group p by p.Category into g

czyli zapis podobny do tych stosowanych w języku SQL. Grupujemy tu wszystkie elementy p (czyli wszystkie elementy z tablicy products) ze względu na atrybut Category. Tak pogrupowane elementy umieszczamy w zmiennej g. Teraz każdy kolejny element kolekcji g oznacza kolejną kategorię i zawiera wszystkie elementy, które w danej kategorii występują. Dalej mamy:

select new { Category = g.Key, CheapestPrice = g.Min(p => p.UnitPrice) };

Operatorem select wsadzamy coś do naszego wyniku, czyli zmiennej categories. Czyli mamy już pogrupowane nasze produkty p pochodzące z products i umieszczone w g. Więc teraz iterujemy po wszystkich elementach zawartych w g. I dla takiego każdego elementu w g (czyli de facto dla każdej kategorii – bo tak grupowaliśmy) zwracamy parametr Key, czyli nazwę pola po którym grupowaliśmy – tutaj nazwę kategorii. Zwracamy jeszcze jedno. Zwracamy jeszcze wynik operatora Min wywołanego dla każdego elementu należącego do g. Z tym, że operator Min dostaje w argumencie wyrażenie lambda, które mówi jak porównywać. Można tu wpaść w złe wrażenie, że te zmienne p w wyrażeniu lambda mają coś wspólnego z tymi p, co były wyżej – ale to nie prawda. Równie dobrze w wyrażeniu lambda mogłaby być inna litera. Reasumując, oprócz nazwy kategorii zwracana jest jeszcze wartość minimalna dla każdej kategorii, liczona tak, że wszystkie elementy składowe każdego g (czyli każdej kategorii) są ze sobą porównywane na podstawie atrybutu UnitPrice i jest zwracana najmniejsza wartość dla każdej kategorii. Na końcu jest jeszcze:

ObjectDumper.Write(categories);

który tylko wypisuje zawartość zmiennej categories. W efekcie moglibyśmy dostać coś takiego:

Category=Beverages CheapestPrice=4.5000
Category=Condiments CheapestPrice=10.0000
Category=Produce CheapestPrice=7.4500

Teraz podobny przykład. Zamiast ceny najtańszego produktu, będziemy zwracać nazwę tego najtańszego produktu. Rozważmy przykład:

public void Linq4(){
    List<Product> products = GetProductList();
 
    var categories =
        from p in products
        group p by p.Category into g
        let minPrice = g.Min(p => p.UnitPrice)
        select new { Category = g.Key, CheapestProducts = g.Where(p => p.UnitPrice == minPrice) };
 
    ObjectDumper.Write(categories, 1);
}

Początek jest podobny. Mamy jakąś wyimaginowaną funkcję, która zwraca nam jakąś listę produktów. Dalej mamy zapytanie LINQ. Iterujemy po kolekcji products oraz grupujemy elementy składowe p na podstawie atrybutu Category i w ten sposób tworzymy nową kolekcję g. Dalej już mamy coś innego.

let minPrice = g.Min(p => p.UnitPrice)

Tą linijką przypisywaliśmy (nie zwracaliśmy) do ‘zmiennej’ minPrice wartość minimalną każdego elementu g, czyli wartość minimalną dla każdej kategorii. Dalej jest:

select new { Category = g.Key, CheapestProducts = g.Where(p => p.UnitPrice == minPrice) };

czyli do wyniku zwracamy atrybut Key dla każdego g, czyli zwracamy de facto nazwę kategorii. Do tego zwracamy jeszcze wynik operatora Where dla każdego elementu w g. Operator ten zadaje pytanie do każdego g, czyli do każdej kategorii. W odpowiedzi dostajemy dla każdego g (każdej kategorii) te elementy składowe należące do g, gdzie cena tego elementu jest równa minPrice, czyli najmniejszej cenie. Reasumując, oprócz nazwy kategorii zwracamy jeszcze ten element składowy g, którego cena równa się cenie najmniejsze – czyli najtańszy element. W wyniku dostajemy:

Category=Beverages CheapestProducts= CheapestProducts: ProductName=Guarana Fantastica Category=Beverages UnitPrice=4.5000
Category=Condiments CheapestProducts= CheapestProducts: ProductName=Aniseed Syrup Category=Condiments UnitPrice=10.0000
Category=Produce CheapestProducts= ProductName=Longlife Tofu Category=Produce UnitPrice=10.0000