Przejdź do treści
School IT / Przestrzeń do nauki
← C# od podstawLekcja 55 z 58

LINQ i lambda — wybieranie danych krótkim zapisem

Pytania o dane — kto zaliczył, kto na podium, ile średnio — da się zapisać jednym wyrażeniem zamiast czterema pętlami. Trzeba tylko wiedzieć, kiedy zapytanie faktycznie się wykonuje.

C# LINQ lambda Where Select GroupBy ToList 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Zamienisz pętlę filtrującą, sumującą i szukającą maksimum na jedno wyrażenie LINQ.
  • Odczytasz wyrażenie lambda w kontekście LINQ i wskażesz, jaki typ zwraca.
  • Użyjesz Where, Select, OrderBy, ThenBy, Take i Skip.
  • Policzysz Count, Sum, Average, Min i Max oraz sprawdzisz Any i All.
  • Dobierzesz First, FirstOrDefault albo Single do sytuacji.
  • Złożysz kilka metod w łańcuch i uzasadnisz kolejność jego ogniw.
  • Zbudujesz zestawienie metodą GroupBy z liczeniem wewnątrz grup.
  • Wyjaśnisz, dlaczego zapytanie wykonuje się dopiero przy odczycie wyników, i kiedy trzeba dopisać ToList().
  • Rozpoznasz składnię zapytania from … where … select jako równoważną składni metod.
TEORIA

Cztery pytania, cztery pętle

Mamy listę uczestników turnieju i cztery zwykłe pytania: kto zdobył co najmniej 40 punktów, kto jest na podium, ile punktów zdobyli razem i ile wynosi średnia. Napisane pętlami wygląda to tak:

cztery pytania, cztery pętle
// 1. kto ma co najmniej 40 punktow
List<Uczestnik> zaliczyli = new List<Uczestnik>();
foreach (Uczestnik u in turniej)
{
    if (u.Punkty >= 40)
    {
        zaliczyli.Add(u);
    }
}

// 2. suma punktow
int suma = 0;
foreach (Uczestnik u in turniej)
{
    suma += u.Punkty;
}

// 3. srednia
double srednia = (double)suma / turniej.Count;

// 4. najlepszy wynik
int najlepszy = turniej[0].Punkty;
foreach (Uczestnik u in turniej)
{
    if (u.Punkty > najlepszy)
    {
        najlepszy = u.Punkty;
    }
}

Dwadzieścia pięć wierszy, cztery pętle i trzy zmienne pomocnicze. Kod jest poprawny, ale trzeba go przeczytać w całości, żeby się dowiedzieć, co robi. Do tego każda z tych pętli to okazja do pomyłki: zła nierówność, zapomniana inicjalizacja, dzielenie liczb całkowitych, puste źródło.

To samo w LINQ:

to samo w LINQ
List<Uczestnik> zaliczyli = turniej.Where(x => x.Punkty >= 40).ToList();
int suma       = turniej.Sum(x => x.Punkty);
double srednia = turniej.Average(x => x.Punkty);
int najlepszy  = turniej.Max(x => x.Punkty);

Cztery wiersze, które czyta się jak zdania. LINQ (Language Integrated Query — zapytania wbudowane w język) to zestaw gotowych metod do filtrowania, sortowania, grupowania i liczenia na kolekcjach. Nie zastępuje pętli w każdym zastosowaniu, ale w tych czterech — zastępuje.

Co trzeba mieć

Metody LINQ leżą w przestrzeni nazw System.Linq. W projektach .NET 8 z włączonym ImplicitUsings — a takie tworzy dotnet new console — jest ona dodana automatycznie. Jeśli kompilator nie widzi metody Where, dopisz na górze pliku using System.Linq;.

LINQ działa na wszystkim, co da się przejść pętlą foreach: tablicach z lekcji 23, List<T> z lekcji 31, Dictionary z lekcji 32, kolejkach i stosach z lekcji 33. To właśnie umowa IEnumerable<T> z lekcji 48 — jedna umowa, a wszystkie te metody dostajesz za darmo.

TEORIA

Co dokładnie podajesz metodzie

Wyrażenia lambda poznałeś w lekcji 53. W LINQ pojawiają się w niemal każdym wywołaniu, więc przypomnijmy je w tym kontekście.

Lambda to metoda bez nazwy, zapisana w jednym wyrażeniu:

przypomnienie z lekcji 53
Func<Uczestnik, bool> warunek = x => x.Punkty >= 40;

//                                  ^   ^  ^
//                                  |   |  wynik (warunek do sprawdzenia)
//                                  |   strzalka - czytaj "daje"
//                                  parametr (jeden uczestnik)

x => x.Punkty >= 40 czyta się: „weź jednego uczestnika i powiedz, czy ma co najmniej 40 punktów”. Metoda Where wywoła tę lambdę raz dla każdego elementu i zatrzyma te, dla których wynikiem jest true.

Zapis lambdyZnaczenieTypowe użycie
x => x.Punkty >= 40z uczestnika daje boolWhere, Count, Any, All, First
x => x.Punktyz uczestnika daje intSum, Average, Max, OrderBy
x => x.Imiez uczestnika daje stringSelect, OrderBy, GroupBy
x => x.Punkty * 2z uczestnika daje nową liczbęSelect
(a, b) => a + bz dwóch liczb daje jednąAggregate
() => 5bez parametrówrzadko w LINQ

Nazwa parametru nie ma znaczenia

x => x.Punkty >= 40, u => u.Punkty >= 40 i uczestnik => uczestnik.Punkty >= 40 to dokładnie to samo. W krótkich wyrażeniach przyjęło się jedno litera, w dłuższych — pełna nazwa. Uważaj tylko, żeby nie użyć nazwy, która już istnieje w tym miejscu programu: nazwa parametru lambdy nie może powtarzać nazwy zmiennej z otaczającej pętli (błąd CS0136).

ELEMENTY WBUDOWANE

Zestaw na każde zadanie

Metod LINQ jest kilkadziesiąt. Tych dziesięć wystarcza do zdecydowanej większości zadań.

Wybieranie: Where

Program.cs
IEnumerable<Uczestnik> mocni = turniej.Where(x => x.Punkty >= 40);
IEnumerable<Uczestnik> z3TP  = turniej.Where(x => x.Klasa == "3TP");
IEnumerable<Uczestnik> oboje = turniej.Where(x => x.Punkty >= 40 && x.Klasa == "3TP");

foreach (Uczestnik u in mocni)
{
    Console.WriteLine(u.Imie);
}

Przekształcanie: Select

Select zamienia każdy element na coś innego. Wynik ma tyle samo elementów, ale innego typu:

Program.cs
IEnumerable<string> imiona = turniej.Select(x => x.Imie);
IEnumerable<int> punkty       = turniej.Select(x => x.Punkty);
IEnumerable<string> wiersze   = turniej.Select(x => $"{x.Imie} - {x.Punkty} pkt");

Console.WriteLine(string.Join(", ", imiona));
MetodaCo robiIle elementów w wyniku
Wherewybiera te, które spełniają warunektyle samo albo mniej
Selectzamienia każdy element na coś innegodokładnie tyle samo
OrderByukłada w kolejnoścityle samo

Porządkowanie: OrderBy, OrderByDescending, ThenBy

Program.cs
// rosnaco po punktach
var a = turniej.OrderBy(x => x.Punkty);

// malejaco po punktach
var b = turniej.OrderByDescending(x => x.Punkty);

// malejaco po punktach, a przy remisie - krotszy czas wyzej
var c = turniej.OrderByDescending(x => x.Punkty).ThenBy(x => x.Czas);

// po klasie, a w klasie alfabetycznie
var d = turniej.OrderBy(x => x.Klasa).ThenBy(x => x.Imie);

Sortowanie nie zmienia listy źródłowej

turniej.OrderBy(...) nie porządkuje listy turniej — zwraca nową, uporządkowaną sekwencję. To odwrotnie niż lista.Sort() z lekcji 51, które przestawia elementy w miejscu. Jeśli chcesz zachować kolejność, przypisz wynik: turniej = turniej.OrderBy(...).ToList();

Liczenie: Count, Sum, Average, Min, Max

Program.cs
int ilu        = turniej.Count();                       // wszyscy
int iluMocnych = turniej.Count(x => x.Punkty >= 40);    // z warunkiem
int suma       = turniej.Sum(x => x.Punkty);
double srednia = turniej.Average(x => x.Punkty);
int najwiecej  = turniej.Max(x => x.Punkty);
double najkrotszy = turniej.Min(x => x.Czas);

Console.WriteLine($"{ilu} osob, {iluMocnych} z wynikiem 40+, srednia {srednia:F2}");

Sprawdzanie: Any, All, Contains

Program.cs
bool ktokolwiek = turniej.Any();                          // czy cokolwiek jest
bool jestMistrz = turniej.Any(x => x.Punkty > 50);        // czy ktos przekroczyl 50
bool wszyscyZdali = turniej.All(x => x.Punkty >= 30);     // czy KAZDY ma 30+

Console.WriteLine($"{jestMistrz} / {wszyscyZdali}");

Any i All na pustej kolekcji

Any() na pustej kolekcji daje false — to zgodne z intuicją. All(warunek) na pustej kolekcji daje true, bo „każdy element spełnia warunek” jest prawdą, gdy nie ma żadnego elementu. To zaskakuje, ale jest logicznie poprawne i tak samo działa w matematyce.

Pobieranie jednego: First, FirstOrDefault, Single

Program.cs
Uczestnik pierwszy = turniej.First();                        // pierwszy z listy
Uczestnik pierwszyZ2TP = turniej.First(x => x.Klasa == "2TP");

Uczestnik? moze = turniej.FirstOrDefault(x => x.Punkty > 100);
if (moze == null)
{
    Console.WriteLine("nikt nie przekroczyl 100 punktow");
}

// Uczestnik brak = turniej.First(x => x.Punkty > 100);   // WYJATEK!
MetodaGdy znajdzie jedenGdy nie znajdzie nicGdy znajdzie kilka
First(w)zwraca gowyjątek InvalidOperationExceptionzwraca pierwszy
FirstOrDefault(w)zwraca gozwraca null (lub 0 dla liczb)zwraca pierwszy
Single(w)zwraca gowyjątekwyjątek
SingleOrDefault(w)zwraca gozwraca nullwyjątek
Last(w)zwraca ostatniwyjątekzwraca ostatni

Single jest ostrzejsze i bardzo przydatne przy wyszukiwaniu po numerze: jeśli w bazie są dwa bilety o tym samym numerze, wolisz o tym wiedzieć od razu niż dostać cicho pierwszy z nich.

Ucinanie: Take, Skip

Program.cs
var podium = turniej.OrderByDescending(x => x.Punkty).Take(3);
var poza   = turniej.OrderByDescending(x => x.Punkty).Skip(3);

// strona 2 listy po 10 pozycji:
var strona2 = turniej.OrderBy(x => x.Imie).Skip(10).Take(10);
TEORIA

Składanie i kolejność ogniw

Każda z tych metod zwraca sekwencję, więc można je składać w łańcuch. Tak powstaje jedno wyrażenie odpowiadające na złożone pytanie.

„Trzy najlepsze osoby z klas trzecich, wypisane jako imiona”:

Program.cs
var wynik = turniej
    .Where(x => x.Klasa.StartsWith("3"))        // 1. wybierz klasy trzecie
    .OrderByDescending(x => x.Punkty)           // 2. uloz od najlepszego
    .Take(3)                                    // 3. wez trzy pierwsze
    .Select(x => x.Imie);                       // 4. zostaw same imiona

Console.WriteLine(string.Join(", ", wynik));

Zapis w osobnych wierszach nie jest wymogiem, ale przy trzech i więcej krokach czyta się znacznie lepiej — każdy wiersz to jedno zdanie o danych.

Kolejność ma znaczenie

Program.cs
// DOBRZE: najpierw odsiej, potem sortuj mniejszy zbior
turniej.Where(x => x.Punkty >= 40).OrderBy(x => x.Imie);

// DZIALA, ale sortuje wszystko po to, zeby wiekszosc wyrzucic
turniej.OrderBy(x => x.Imie).Where(x => x.Punkty >= 40);

// ZLE: Take przed OrderBy bierze trzy PRZYPADKOWE osoby i je sortuje
turniej.Take(3).OrderByDescending(x => x.Punkty);

// DOBRZE: najpierw uloz, potem wez trzy najlepsze
turniej.OrderByDescending(x => x.Punkty).Take(3);
Para metodZamiana kolejnościSkutek
Where i OrderBywynik ten samtylko wydajność — filtruj najpierw
Where i Selectwynik innypo Select nie ma już pól, po których filtrowałeś
OrderBy i Takewynik innyTake pierwsze bierze dowolne elementy
Skip i Takewynik innySkip(10).Take(10) to strona druga, odwrotnie — pustka lub co innego

Zakończenie łańcucha: ToList, ToArray, ToDictionary

Program.cs
List<Uczestnik> lista  = turniej.Where(x => x.Punkty >= 40).ToList();
Uczestnik[] tablica    = turniej.OrderBy(x => x.Imie).ToArray();

Dictionary<string, int> punktyPoImieniu =
    turniej.ToDictionary(x => x.Imie, x => x.Punkty);

Console.WriteLine(punktyPoImieniu["Kasia Nowak"]);

ToDictionary wymaga, żeby klucze były unikalne — dwa takie same imiona dają wyjątek ArgumentException. To ta sama zasada, którą poznałeś w lekcji 32.

TEORIA

Zestawienia po wspólnej wartości

GroupBy dzieli kolekcję na paczki po wspólnej wartości. To najczęstsze zadanie w każdym zestawieniu: „ile osób z każdej klasy”, „suma sprzedaży po miesiącach”, „średnia ocen po przedmiotach”.

Program.cs
foreach (IGrouping<string, Uczestnik> grupa in turniej.GroupBy(x => x.Klasa))
{
    Console.WriteLine($"{grupa.Key}: {grupa.Count()} osob");

    foreach (Uczestnik u in grupa)
    {
        Console.WriteLine($"    {u.Imie} - {u.Punkty} pkt");
    }
}
wynik w konsoli
3TP: 3 osob
    Kasia Nowak - 51 pkt
    Ala Wrona - 42 pkt
    Jan Klon - 45 pkt
2TP: 2 osob
    Marek Lis - 34 pkt
    Ewa Sosna - 28 pkt
1TP: 1 osob
    Piotr Dab - 51 pkt

Każda grupa to dwie rzeczy naraz: Key — wartość, po której grupowaliśmy — oraz zbiór elementów, który da się przejść pętlą i na którym działają wszystkie metody LINQ:

Program.cs
var zestawienie = turniej
    .GroupBy(x => x.Klasa)
    .OrderByDescending(g => g.Sum(y => y.Punkty));

foreach (IGrouping<string, Uczestnik> g in zestawienie)
{
    Console.WriteLine($"{g.Key,-5} osob {g.Count()}  suma {g.Sum(y => y.Punkty),3}  srednia {g.Average(y => y.Punkty):F1}  najlepszy {g.Max(y => y.Punkty)}");
}
wynik w konsoli
3TP   osob 3  suma 138  srednia 46,0  najlepszy 51
2TP   osob 2  suma  62  srednia 31,0  najlepszy 34
1TP   osob 1  suma  51  srednia 51,0  najlepszy 51

Kolejność grup

GroupBy ustawia grupy w takiej kolejności, w jakiej ich klucze pierwszy raz pojawiły się w danych. Jeśli chcesz innej kolejności — na przykład alfabetycznie po kluczu albo malejąco po sumie — dopisz OrderBy po GroupBy, tak jak powyżej.

TEORIA

Przepis kontra policzony wynik

Zostaje jedna właściwość LINQ, która zaskakuje każdego po pierwszym razie. Zapytanie nie wykonuje się w chwili napisania. Wykonuje się wtedy, gdy ktoś sięgnie po wyniki.

Program.cs
List<int> liczby = new List<int> { 1, 2, 3 };

var duze = liczby.Where(x => x > 1);        // tu jeszcze NIC sie nie policzylo

liczby.Add(10);                             // zmiana zrodla PO napisaniu zapytania

Console.WriteLine(string.Join(", ", duze)); // dopiero tu zapytanie sie wykonuje
wynik — z dziesiątką
2, 3, 10

Dziesiątka jest w wyniku, choć została dodana po napisaniu zapytania. Where nie zwróciło listy — zwróciło przepis na listę, a przepis wykonał się dopiero przy wypisywaniu, już na zmienionych danych.

Skutek uboczny bywa kosztowny: zapytanie przejrzane dwa razy wykonuje się dwa razy.

Program.cs
var mocni = turniej.Where(x => { Console.WriteLine("sprawdzam"); return x.Punkty >= 40; });

Console.WriteLine(mocni.Count());     // przeglada cala liste
Console.WriteLine(mocni.Count());     // przeglada JESZCZE RAZ

Lekarstwo jest jedno i bardzo proste: ToList() albo ToArray() wykonuje zapytanie natychmiast i zapamiętuje wynik.

Program.cs
List<Uczestnik> mocni = turniej.Where(x => x.Punkty >= 40).ToList();

Console.WriteLine(mocni.Count);       // gotowa lista, zero pracy
Console.WriteLine(mocni.Count);
MetodaKiedy się wykonujeCo zwraca
Where, Select, OrderBy, GroupBy, Take, Skipprzy sięgnięciu po wynikiIEnumerable<T> — przepis
ToList, ToArray, ToDictionarynatychmiastgotową kolekcję
Count, Sum, Average, Min, Maxnatychmiastjedną liczbę
Any, All, Containsnatychmiastbool
First, Single, Lastnatychmiastjeden element

Prosta zasada praktyczna

Jeśli wynik zapytania przechodzisz więcej niż raz albo przechowujesz go dłużej niż kilka wierszy — zakończ łańcuch ToList(). Kosztuje jedno wywołanie, a usuwa i podwójne liczenie, i niespodzianki z danymi, które zmieniły się w międzyczasie.

TEORIA

Drugi, równoważny zapis

To samo zapytanie można w C# zapisać drugą, bardziej „bazodanową” składnią. Obie są w pełni równoważne — kompilator zamienia tę drugą na pierwszą.

Program.cs
// skladnia metod (ta, ktora uzywamy w calej lekcji)
var a = turniej
    .Where(x => x.Punkty >= 40)
    .OrderByDescending(x => x.Punkty)
    .Select(x => x.Imie);

// skladnia zapytania - dokladnie to samo
var b = from x in turniej
        where x.Punkty >= 40
        orderby x.Punkty descending
        select x.Imie;
Składnia metodSkładnia zapytania
.Where(x => w)where w
.Select(x => e)select e
.OrderBy(x => k)orderby k
.OrderByDescending(x => k)orderby k descending
.ThenBy(x => k)orderby k1, k2
.GroupBy(x => k)group x by k
.Count(), .Sum(), .Take()brak odpowiednika — trzeba dopisać metodę

Którą wybrać

Składnia zapytania kończy się zawsze na select albo group i nie ma słów dla Count, Sum czy Take — trzeba wtedy objąć całość nawiasami i dopisać metodę. Dlatego w kodzie C# zdecydowanie częściej spotkasz składnię metod i tej się trzymamy. Składnię zapytania warto rozpoznawać, bo pojawia się w starszych materiałach i przypomina SQL, którego uczysz się na innych zajęciach.

PRZYKŁAD

Wyniki turnieju w dziewięciu pytaniach

Jeden zestaw danych i dziewięć pytań. Każde z nich to jedno wyrażenie — bez ani jednej pętli liczącej, bez zmiennych pomocniczych i bez ręcznego sortowania.

Program.cs — pełny plik
// Program.cs - pelny plik
List<Uczestnik> turniej = new List<Uczestnik>
{
    new Uczestnik("Kasia Nowak", "3TP", 51, 62.5),
    new Uczestnik("Marek Lis",   "2TP", 34, 71.0),
    new Uczestnik("Ala Wrona",   "3TP", 42, 58.2),
    new Uczestnik("Piotr Dab",   "1TP", 51, 55.9),
    new Uczestnik("Ewa Sosna",   "2TP", 28, 80.4),
    new Uczestnik("Jan Klon",    "3TP", 45, 66.1)
};

Console.WriteLine("=== 1. kto zaliczyl (40 pkt i wiecej) ===");
foreach (Uczestnik u in turniej.Where(z => z.Punkty >= 40))
{
    Console.WriteLine($"  {u}");
}

Console.WriteLine();
Console.WriteLine("=== 2. klasyfikacja koncowa ===");
int miejsce = 1;
foreach (Uczestnik u in turniej.OrderByDescending(z => z.Punkty).ThenBy(z => z.Czas))
{
    Console.WriteLine($"  {miejsce}. {u}");
    miejsce++;
}

Console.WriteLine();
Console.WriteLine("=== 3. podium (same imiona) ===");
var podium = turniej
    .OrderByDescending(z => z.Punkty)
    .ThenBy(z => z.Czas)
    .Take(3)
    .Select(z => z.Imie);

Console.WriteLine("  " + string.Join(" | ", podium));

Console.WriteLine();
Console.WriteLine("=== 4. liczby ===");
Console.WriteLine($"  uczestnikow:      {turniej.Count()}");
Console.WriteLine($"  z wynikiem 40+:   {turniej.Count(z => z.Punkty >= 40)}");
Console.WriteLine($"  suma punktow:     {turniej.Sum(z => z.Punkty)}");
Console.WriteLine($"  srednia punktow:  {turniej.Average(z => z.Punkty):F2}");
Console.WriteLine($"  najlepszy wynik:  {turniej.Max(z => z.Punkty)}");
Console.WriteLine($"  najkrotszy czas:  {turniej.Min(z => z.Czas):F1} s");
Console.WriteLine($"  sredni czas:      {turniej.Average(z => z.Czas):F2} s");

Console.WriteLine();
Console.WriteLine("=== 5. sprawdzenia ===");
Console.WriteLine($"  ktos przekroczyl 50 pkt: {turniej.Any(z => z.Punkty > 50)}");
Console.WriteLine($"  kazdy ma co najmniej 30: {turniej.All(z => z.Punkty >= 30)}");
Console.WriteLine($"  ktos z klasy 4TP:        {turniej.Any(z => z.Klasa == "4TP")}");

Console.WriteLine();
Console.WriteLine("=== 6. szukanie jednego ===");
Uczestnik pierwszyZ2TP = turniej.First(z => z.Klasa == "2TP");
Console.WriteLine($"  pierwszy z 2TP: {pierwszyZ2TP.Imie}");

Uczestnik? rekordzista = turniej.FirstOrDefault(z => z.Punkty > 100);
if (rekordzista == null)
{
    Console.WriteLine("  nikt nie przekroczyl 100 punktow");
}

Console.WriteLine();
Console.WriteLine("=== 7. zestawienie po klasach ===");
var poKlasach = turniej
    .GroupBy(z => z.Klasa)
    .OrderByDescending(g => g.Sum(y => y.Punkty));

foreach (IGrouping<string, Uczestnik> g in poKlasach)
{
    Console.WriteLine($"  {g.Key,-5} osob {g.Count()}  suma {g.Sum(y => y.Punkty),3}  srednia {g.Average(y => y.Punkty),5:F1}");

    foreach (Uczestnik u in g.OrderByDescending(y => y.Punkty))
    {
        Console.WriteLine($"          {u.Imie,-14} {u.Punkty,3} pkt");
    }
}

Console.WriteLine();
Console.WriteLine("=== 8. leniwe kontra policzone ===");
List<Uczestnik> kopia = new List<Uczestnik>(turniej);

var leniwe = kopia.Where(z => z.Punkty >= 40);
List<Uczestnik> policzone = kopia.Where(z => z.Punkty >= 40).ToList();

kopia.Add(new Uczestnik("Zosia Buk", "1TP", 99, 50.0));

Console.WriteLine($"  zapytanie leniwe:   {leniwe.Count()} osob");
Console.WriteLine($"  lista policzona:    {policzone.Count} osob");

Console.WriteLine();
Console.WriteLine("=== 9. slownik imie -> punkty ===");
Dictionary<string, int> slownik = turniej.ToDictionary(z => z.Imie, z => z.Punkty);
Console.WriteLine($"  Ala Wrona ma {slownik["Ala Wrona"]} pkt");


class Uczestnik
{
    public string Imie { get; }
    public string Klasa { get; }
    public int Punkty { get; }
    public double Czas { get; }

    public Uczestnik(string imie, string klasa, int punkty, double czas)
    {
        Imie = imie;
        Klasa = klasa;
        Punkty = punkty;
        Czas = czas;
    }

    public override string ToString()
    {
        return $"{Imie,-14} {Klasa,-5} {Punkty,3} pkt   {Czas,5:F1} s";
    }
}
wynik w konsoli
=== 1. kto zaliczyl (40 pkt i wiecej) ===
  Kasia Nowak    3TP    51 pkt    62,5 s
  Ala Wrona      3TP    42 pkt    58,2 s
  Piotr Dab      1TP    51 pkt    55,9 s
  Jan Klon       3TP    45 pkt    66,1 s

=== 2. klasyfikacja koncowa ===
  1. Piotr Dab      1TP    51 pkt    55,9 s
  2. Kasia Nowak    3TP    51 pkt    62,5 s
  3. Jan Klon       3TP    45 pkt    66,1 s
  4. Ala Wrona      3TP    42 pkt    58,2 s
  5. Marek Lis      2TP    34 pkt    71,0 s
  6. Ewa Sosna      2TP    28 pkt    80,4 s

=== 3. podium (same imiona) ===
  Piotr Dab | Kasia Nowak | Jan Klon

=== 4. liczby ===
  uczestnikow:      6
  z wynikiem 40+:   4
  suma punktow:     251
  srednia punktow:  41,83
  najlepszy wynik:  51
  najkrotszy czas:  55,9 s
  sredni czas:      65,68 s

=== 5. sprawdzenia ===
  ktos przekroczyl 50 pkt: True
  kazdy ma co najmniej 30: False
  ktos z klasy 4TP:        False

=== 6. szukanie jednego ===
  pierwszy z 2TP: Marek Lis
  nikt nie przekroczyl 100 punktow

=== 7. zestawienie po klasach ===
  3TP   osob 3  suma 138  srednia  46,0
          Kasia Nowak     51 pkt
          Jan Klon        45 pkt
          Ala Wrona       42 pkt
  2TP   osob 2  suma  62  srednia  31,0
          Marek Lis       34 pkt
          Ewa Sosna       28 pkt
  1TP   osob 1  suma  51  srednia  51,0
          Piotr Dab       51 pkt

=== 8. leniwe kontra policzone ===
  zapytanie leniwe:   5 osob
  lista policzona:    4 osob

=== 9. slownik imie -> punkty ===
  Ala Wrona ma 42 pkt

Co dzieje się po kolei

  • Blok 1. Where zachowuje kolejność źródła — dlatego Piotr jest trzeci, a nie pierwszy, choć ma najwięcej punktów.
  • Blok 2. Piotr i Kasia mają po 51 punktów. Rozstrzyga ThenBy(Czas): krótszy czas wyżej, więc Piotr wyprzedza Kasię. Bez ThenBy kolejność przy remisie byłaby taka jak w źródle.
  • Blok 3. Cztery kroki w jednym wyrażeniu. Select jest ostatni, bo po zamianie na imiona nie dałoby się już sortować po punktach.
  • Blok 4. Średnia punktów to 251 / 6. Zauważ, że Average zwraca double — nie ma tu pułapki dzielenia liczb całkowitych, którą trzeba pamiętać przy ręcznej pętli.
  • Blok 5. All(z => z.Punkty >= 30) jest False z powodu jednej osoby — Ewa ma 28. Any dla klasy 4TP zwraca False, bo takiej klasy nie ma; to bezpieczniejsze niż First, które rzuciłoby wyjątek.
  • Blok 6. First z warunkiem działa, bo klasa 2TP istnieje. FirstOrDefault dla wyniku powyżej 100 zwraca null — i właśnie dlatego zmienna ma typ Uczestnik?.
  • Blok 7. Grupy są ułożone malejąco po sumie punktów klasy, a w każdej grupie osoby malejąco po punktach. To dwa niezależne sortowania w jednym zapytaniu.
  • Blok 8. Najważniejszy blok tej lekcji. Oba zapytania były identyczne, ale leniwe wykonało się po dodaniu Zosi i zobaczyło ją, a policzone zapamiętało stan z chwili wywołania ToList(). Stąd 5 kontra 4.
  • Blok 9. ToDictionary zamienia listę na słownik z lekcji 32. Gdyby dwie osoby miały to samo imię, dostałbyś wyjątek — to celowe.

Sprawdź cztery rzeczy

  1. W bloku 2 usuń .ThenBy(z => z.Czas). Piotr i Kasia zamienią się miejscami, bo przy remisie zostanie kolejność ze źródła.
  2. W bloku 3 przenieś .Select(z => z.Imie) na początek łańcucha. Program się nie skompiluje — po Select elementy są napisami i nie mają właściwości Punkty.
  3. W bloku 6 zamień FirstOrDefault na First. Program przerwie się wyjątkiem InvalidOperationException z komunikatem „Sequence contains no matching element”.
  4. W bloku 8 dopisz .ToList() do zapytania leniwe. Oba liczniki pokażą 4 — to cała różnica między przepisem a policzonym wynikiem.
ELEMENTY WBUDOWANE

Zestawienie elementów

MetodaCo robiUwagi
Where(w)wybiera elementy spełniające warunekZachowuje kolejność źródła.
Select(f)zamienia każdy element na coś innegoWynik ma tyle samo elementów, ale inny typ.
OrderBy(k) / OrderByDescending(k)porządkujeNie zmienia kolekcji źródłowej.
ThenBy(k) / ThenByDescending(k)drugi klucz przy remisieTylko po OrderBy.
Count() / Count(w)liczy elementyZ warunkiem zastępuje pętlę z licznikiem.
Sum(k) / Average(k)suma i średniaAverage na pustej kolekcji rzuca wyjątek.
Min(k) / Max(k)najmniejsza i największa wartośćNa pustej kolekcji liczb rzuca wyjątek.
Any() / Any(w)czy jest choć jedenNa pustej kolekcji false.
All(w)czy każdy spełnia warunekNa pustej kolekcji true.
Contains(x)czy zawiera daną wartośćDla obiektów porównuje tak, jak Equals.
First(w) / FirstOrDefault(w)pierwszy dopasowanyBez dopasowania: wyjątek / null.
Single(w) / SingleOrDefault(w)dokładnie jeden dopasowanyWięcej niż jeden — wyjątek. Do szukania po numerze.
Take(n) / Skip(n)pierwsze n / pomiń nRazem dają stronicowanie.
Distinct()usuwa powtórzeniaPorównuje jak Equals.
Reverse()odwraca kolejnośćNie zmienia źródła.
GroupBy(k)dzieli na grupy po wspólnej wartościGrupa ma Key i elementy.
ToList() / ToArray()wykonuje zapytanie i zapamiętuje wynikKończy łańcuch; usuwa niespodzianki z leniwością.
ToDictionary(k, w)zamienia na słownikKlucze muszą być unikalne.
from … where … selectskładnia zapytaniaRównoważna składni metod; brak słów dla Count i Take.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
turniej.OrderBy(...) bez przypisania wynikuLista źródłowa się nie zmienia. LINQ zawsze zwraca nową sekwencję — przypisz ją albo od razu przejdź pętlą.
First(w) tam, gdzie może nie być dopasowaniaInvalidOperationException. Użyj FirstOrDefault i sprawdź null.
Average albo Max na pustej kolekcjiInvalidOperationException. Najpierw if (kolekcja.Any()).
Select przed WherePo Select elementy są innego typu i nie mają już pól, po których chciałeś filtrować (CS1061).
Take przed OrderByBierze dowolne n elementów i dopiero je sortuje. Zawsze najpierw OrderBy.
Przejście tego samego zapytania dwa razyWykonuje się dwa razy — przy dużych danych bardzo kosztowne. Zakończ ToList().
Zmiana kolekcji między napisaniem a wykonaniem zapytaniaWynik zawiera zmiany, których nie oczekujesz. To samo lekarstwo: ToList().
Zmiana kolekcji w trakcie foreach po zapytaniuInvalidOperationException — „kolekcja została zmieniona”. Zapisz wynik do listy przed zmianami.
ToDictionary z powtarzającym się kluczemArgumentException. Sprawdź unikalność albo użyj GroupBy.
Nazwa parametru lambdy jak zmiennej pętliCS0136. Nadaj lambdzie inną nazwę parametru.
LINQ do wszystkiegoJedno wyrażenie z sześcioma metodami i dwoma warunkami jest mniej czytelne niż pętla. Gdy zapytanie przestaje się czytać jak zdanie, wróć do pętli.
Brak using System.Linq;CS1061 — „nie zawiera definicji dla Where”. W projekcie z ImplicitUsings nie wystąpi.
ZADANIA

Zadania

ZAD 1Pierwsze cztery zapytania★☆☆

Utwórz tablicę int[] oceny = { 3, 5, 2, 4, 5, 1, 4, 6 };. Za pomocą LINQ wypisz: wszystkie oceny pozytywne (czyli różne od 1), liczbę ocen pozytywnych, sumę wszystkich ocen, średnią z dokładnością do dwóch miejsc, ocenę najwyższą i najniższą oraz oceny ułożone malejąco. Każde z siedmiu pytań rozwiąż jednym wyrażeniem, a wyniki wypisz z opisem. Do wypisania kolekcji użyj string.Join.

ZAD 2Where, Select i OrderBy na obiektach★☆☆

Napisz klasę Ksiazka z właściwościami Tytul, Autor, Rok i Strony, ustawianymi w konstruktorze, oraz przesłoniętym ToString(). Utwórz List<Ksiazka> z co najmniej sześcioma książkami różnych autorów i lat. Za pomocą LINQ wypisz: książki wydane po roku 2000 uporządkowane po tytule, same tytuły wszystkich książek oddzielone przecinkami, książki dłuższe niż 300 stron uporządkowane malejąco po liczbie stron oraz liczbę książek każdego z dwóch wybranych autorów.

ZAD 3Łańcuch metod i podium★★☆

Do zadania 2 dopisz cztery zapytania w postaci łańcucha: trzy najdłuższe książki (same tytuły, jedno wyrażenie z OrderByDescending, Take i Select); książki z lat 1990–2010 uporządkowane po autorze, a przy tym samym autorze po roku; tytuły książek wydanych po 2000 roku zapisane wielkimi literami; druga strona listy po trzy pozycje, uporządkowanej alfabetycznie po tytule. Następnie celowo zamień w pierwszym zapytaniu kolejność Take(3) i OrderByDescending, uruchom i zapisz w komentarzu, dlaczego wynik jest inny.

ZAD 4Liczenie, sprawdzanie i szukanie jednego★★☆

Do listy z zadania 2 dopisz zapytania odpowiadające na pytania: ile jest łącznie stron we wszystkich książkach; jaka jest średnia liczba stron; czy jest choć jedna książka z lat przed 1980; czy wszystkie książki mają więcej niż 100 stron; jaka jest pierwsza książka danego autora (użyj First); czy istnieje książka o dokładnie zadanym tytule (użyj SingleOrDefault i obsłuż przypadek null). Następnie utwórz pustą listę książek i spróbuj na niej wywołać Average(x => x.Strony) oraz Any(). Zapisz w komentarzu, co się stało w każdym z dwóch przypadków, i popraw kod tak, żeby program nie przerywał się wyjątkiem.

ZAD 5Grupowanie★★☆

Do listy z zadania 2 dopisz zestawienie po autorach: dla każdego autora wypisz jego nazwisko, liczbę książek, łączną liczbę stron, średnią liczbę stron i rok najnowszej książki. Grupy uporządkuj malejąco po liczbie książek, a przy równej liczbie alfabetycznie po autorze. Wewnątrz każdej grupy wypisz tytuły uporządkowane po roku wydania. Następnie zrób drugie zestawienie — po dekadach wydania (wskazówka: kluczem grupowania niech będzie x.Rok / 10 * 10) — i wypisz dla każdej dekady liczbę książek.

ZAD 6Raport z pliku ocen — pełny program★★★

Napisz program, który wczytuje oceny z pliku tekstowego i buduje raport wyłącznie zapytaniami LINQ. Plik oceny.txt ma wiersze w formacie nazwisko;klasa;przedmiot;ocena — przygotuj go samodzielnie z co najmniej dwudziestoma wierszami, obejmującymi trzy klasy, cztery przedmioty i kilku uczniów w każdej klasie. Napisz klasę Wpis z odpowiednimi właściwościami oraz metodę static Wpis? Zparsuj(string wiersz), która dzieli wiersz metodą Split, sprawdza liczbę pól i poprawność oceny, a przy błędzie zwraca null. Wczytaj plik metodą z lekcji 36, zamień wiersze na obiekty, odrzuć te niepoprawne i wypisz na końcu programu, ile wierszy pominięto. Potem zbuduj raport: lista wszystkich uczniów ze średnią, uporządkowana malejąco po średniej; średnia z każdego przedmiotu; średnia w każdej klasie; przedmiot o najniższej średniej; liczba ocen niedostatecznych w każdej klasie; uczniowie, którzy nie mają ani jednej oceny niedostatecznej (użyj All w połączeniu z GroupBy); uczniowie z co najmniej jedną oceną celującą (użyj Any); trzech uczniów z najwyższą średnią. Cały raport zapisz do pliku raport.txt. Na końcu pliku źródłowego, w komentarzu, odpowiedz na dwa pytania: w których miejscach programu użyłeś ToList() i dlaczego, oraz które zapytanie napisałeś ostatecznie zwykłą pętlą, bo w LINQ było mniej czytelne.

PODSUMOWANIE

Co trzeba zapamiętać

  • LINQ to gotowe metody do filtrowania, sortowania, grupowania i liczenia na wszystkim, co da się przejść pętlą foreach.
  • Lambda x => wyrażenie mówi, co zrobić z jednym elementem; metoda LINQ wywołuje ją dla każdego.
  • Where wybiera, Select przekształca, OrderBy porządkuje — i żadna z nich nie zmienia kolekcji źródłowej.
  • ThenBy rozstrzyga remisy; bez niego przy równych kluczach zostaje kolejność ze źródła.
  • Count, Sum, Average, Min, Max, Any i All zwracają od razu jedną wartość.
  • All na pustej kolekcji daje true, a Average i Max rzucają wyjątek — sprawdź Any() najpierw.
  • First rzuca wyjątek, gdy nic nie znajdzie; FirstOrDefault zwraca null; Single pilnuje, żeby dopasowanie było dokładnie jedno.
  • W łańcuchu najpierw filtruj, potem sortuj, potem ucinaj, a Select zostaw na koniec.
  • GroupBy daje grupy z kluczem Key; wewnątrz grupy działają wszystkie metody LINQ.
  • Zapytanie to przepis, nie wynik — wykonuje się przy odczycie, więc zmiana danych w międzyczasie jest widoczna.
  • ToList() wykonuje zapytanie natychmiast i zapamiętuje wynik. Używaj go, gdy przechodzisz wynik więcej niż raz.
  • Gdy wyrażenie LINQ przestaje się czytać jak zdanie, zwykła pętla jest lepszym wyborem.

Dokumentacja: Microsoft Learn — temat tej lekcji.

Postęp zapisuje się w tej przeglądarce.