Beetje Coderen

Les 3

LINQ en collecties

Data filteren, sorteren en groeperen met LINQ.

⏱ 45–60 minuten leestijd + oefenen

🎯 Na deze les kun je

  • LINQ-methodes als Where, Select en OrderBy gebruiken
  • lambdas schrijven
  • uitgestelde uitvoering uitleggen

In Python schrijf je [r.naam for r in resultaten if r.cijfer < 5.5] of gebruik je sorted(..., key=...). C# heeft daarvoor LINQ (Language Integrated Query): een set methoden waarmee je verzamelingen filtert, omzet, sorteert, groepeert en samenvat. Je gaat LINQ de hele cursus gebruiken, ook voor databasequery's in Entity Framework (les 13). In deze les leer je de belangrijkste methoden, lambda's, en het begrip uitgestelde uitvoering.

Voorbereiding: een record

We werken met een lijst cijfers. Daarvoor maak je eerst een eigen type:

record Resultaat(string Naam, string Vak, double Cijfer);

Een record is een klein gegevenstype in één regel: het heeft de eigenschappen Naam, Vak en Cijfer, een constructor, en een nette ToString(). Vergelijkbaar met een @dataclass in Python. (In een los .cs-bestand zet je typen als dit onderaan.)

var cijfers = new List<Resultaat>
{
    new("Aylin", "Wiskunde", 8.2),
    new("Aylin", "Engels", 6.4),
    new("Bram", "Wiskunde", 5.1),
    new("Bram", "Engels", 7.8),
    new("Chris", "Wiskunde", 9.0),
    new("Chris", "Engels", 4.9),
};

new("Aylin", ...) zonder typenaam mag, omdat de compiler uit List<Resultaat> weet wat er gemaakt moet worden.

Lambda's

LINQ-methoden krijgen een kleine functie mee die zegt wát je wilt. Zo'n functie schrijf je als lambda:

r => r.Cijfer < 5.5

Lees: "gegeven r, geef r.Cijfer < 5.5". Dit is hetzelfde als Pythons lambda r: r.cijfer < 5.5. Het type van r leidt de compiler af uit de lijst. Je kent de vorm al uit JavaScript ((r) => ...).

Filteren en omzetten: Where en Select

var onvoldoendes = cijfers.Where(r => r.Cijfer < 5.5);
foreach (var r in onvoldoendes)
{
    Console.WriteLine($"{r.Naam}: {r.Vak} {r.Cijfer}");
}
Bram: Wiskunde 5.1
Chris: Engels 4.9
var namen = cijfers.Select(r => r.Naam).Distinct().ToList();
Console.WriteLine(string.Join(", ", namen));   // Aylin, Bram, Chris
LINQ Python Doet
Where(r => ...) [r for r in x if ...] / filter houdt elementen die voldoen
Select(r => ...) [f(r) for r in x] / map zet elk element om
Distinct() set(...) (maar met volgorde) haalt dubbelen weg
ToList() list(...) maakt er een echte lijst van

Je ketent methoden: elke methode geeft een nieuwe reeks terug waar je de volgende op aanroept. De oorspronkelijke lijst verandert nooit.

Samenvatten

Console.WriteLine(cijfers.Average(r => r.Cijfer).ToString("F2"));  // 6.90
Console.WriteLine(cijfers.Max(r => r.Cijfer));                     // 9
Console.WriteLine(cijfers.Count(r => r.Cijfer >= 5.5));            // 4
Console.WriteLine(cijfers.Any(r => r.Cijfer < 5));                 // True
Console.WriteLine(cijfers.All(r => r.Cijfer >= 1));                // True

Sum, Min, Max, Average en Count kunnen een lambda krijgen die zegt waarover. Any en All zijn Pythons any() en all().

Eén element: First, FirstOrDefault, Single

var beste = cijfers.OrderByDescending(r => r.Cijfer).First();
Console.WriteLine(beste);   // Resultaat { Naam = Chris, Vak = Wiskunde, Cijfer = 9 }

var daan = cijfers.FirstOrDefault(r => r.Naam == "Daan");
Console.WriteLine(daan is null);   // True
Methode Als er niets is Als er meer zijn
First() fout (InvalidOperationException) geeft de eerste
FirstOrDefault() null (of 0 bij getallen) geeft de eerste
Single() fout fout

Gebruik First als er zeker iets is, FirstOrDefault als het mag ontbreken (en controleer dan op null), en Single als er precies één hoort te zijn, zoals bij zoeken op een uniek id.

Sorteren

var gesorteerd = cijfers
    .OrderBy(r => r.Vak)
    .ThenByDescending(r => r.Cijfer);
Engels    Bram   7.8
Engels    Aylin  6.4
Engels    Chris  4.9
Wiskunde  Chris  9
Wiskunde  Aylin  8.2
Wiskunde  Bram   5.1

ThenBy/ThenByDescending sorteert binnen gelijke waarden verder. Twee keer OrderBy achter elkaar werkt níet zo: de tweede sortering gooit de eerste weg.

Groeperen

var perLeerling = cijfers
    .GroupBy(r => r.Naam)
    .Select(g => new { Naam = g.Key, Gemiddelde = g.Average(r => r.Cijfer) })
    .OrderByDescending(x => x.Gemiddelde);

foreach (var x in perLeerling)
{
    Console.WriteLine($"{x.Naam,-6} {x.Gemiddelde:F2}");
}
Aylin  7.30
Chris  6.95
Bram   6.45
  • GroupBy maakt groepen. Elke groep g heeft een Key (hier de naam) en is zelf een reeks resultaten, waarop je weer LINQ kunt gebruiken.
  • new { Naam = ..., Gemiddelde = ... } maakt een anoniem type: een object zonder eigen klassenaam, handig als tussenresultaat binnen één methode.

Uitgestelde uitvoering

Dit is het belangrijkste idee van de les. Voorspel de uitvoer:

var lijst = new List<int> { 1, 2, 3 };
var groot = lijst.Where(x =>
{
    Console.WriteLine($"test {x}");
    return x > 1;
});
lijst.Add(4);
Console.WriteLine("voor de lus");
foreach (var g in groot)
{
    Console.WriteLine(g);
}
voor de lus
test 1
test 2
2
test 3
3
test 4
4
  • Bij de regel met Where gebeurt nog niets. groot is geen lijst, maar een recept: "filter lijst op x > 1".
  • Het recept wordt pas uitgevoerd als je de elementen echt opvraagt, in de foreach. Daarom staat "voor de lus" bovenaan, en doet de later toegevoegde 4 gewoon mee.
  • Elke keer dat je groot opnieuw doorloopt (bijvoorbeeld met groot.Count()), wordt het recept opnieuw uitgevoerd.

Methoden als Where, Select, OrderBy en GroupBy zijn uitgesteld. Methoden die een enkele waarde of een concrete verzameling teruggeven, voeren meteen uit: ToList(), ToArray(), Count(), Sum(), First(), Any().

Moet je een resultaat meerdere keren gebruiken, of mag het niet meer veranderen als de bron verandert? Zet er .ToList() achter. Bij EF Core (les 13) is dit extra belangrijk: daar betekent opnieuw uitvoeren een nieuwe databasequery.

Het type van zo'n recept is IEnumerable<T>: "iets waar je doorheen kunt lopen". Een List<T> is ook een IEnumerable<T>, dus LINQ werkt op lijsten, arrays, dictionaries en meer.

Typische fouten

Symptoom Oorzaak
Sequence contains no elements First(), Single() of Max() op een lege reeks
Sequence contains more than one element Single() terwijl er meerdere zijn
Resultaat verandert onverwacht Uitgestelde uitvoering; zet er .ToList() achter
Tweede sortering doet niets OrderBy().OrderBy() in plaats van OrderBy().ThenBy()
'List<Resultaat>' does not contain a definition for 'Where' using System.Linq; ontbreekt (staat in nieuwe projecten automatisch aan)

Oefeningen

Oefening 1. Geef de namen (zonder dubbelen) van leerlingen met minstens één cijfer onder de 5,5, alfabetisch.

Modelantwoord oefening 1
var namen = cijfers
    .Where(r => r.Cijfer < 5.5)
    .Select(r => r.Naam)
    .Distinct()
    .OrderBy(n => n)
    .ToList();
// Bram, Chris

Oefening 2. Bereken per vak het hoogste cijfer en wie het haalde.

Modelantwoord oefening 2
var besten = cijfers
    .GroupBy(r => r.Vak)
    .Select(g => g.OrderByDescending(r => r.Cijfer).First());
foreach (var r in besten)
{
    Console.WriteLine($"{r.Vak}: {r.Naam} ({r.Cijfer})");
}
// Wiskunde: Chris (9), Engels: Bram (7.8)

In .NET 6 en nieuwer kan ook g.MaxBy(r => r.Cijfer).

Oefening 3. Wat drukt dit af, en waarom?

var getallen = new List<int> { 5, 10 };
var dubbel = getallen.Select(x => x * 2);
getallen.Clear();
Console.WriteLine(dubbel.Count());
Modelantwoord oefening 3

0. Select is uitgesteld; pas bij Count() wordt het recept uitgevoerd, en dan is de lijst al leeg.

Lab: cijferlijst-analyse met LINQ

  1. Lees in een console-app een CSV-bestand met naam, vak en cijfer in met File.ReadAllLines en zet elke regel om naar een Resultaat.
  2. Toon per leerling het gemiddelde, gesorteerd van hoog naar laag.
  3. Toon per vak het aantal onvoldoendes en het beste resultaat.
  4. Toon welke leerlingen voor alle vakken een voldoende hebben.
  5. beetje check aspnet-les3

Samenvatting

  • LINQ-methoden werken op elke IEnumerable<T>: Where, Select, OrderBy/ThenBy, GroupBy, Distinct.
  • Samenvatten met Count, Sum, Average, Min, Max, Any, All; één element met First, FirstOrDefault, Single.
  • Een lambda x => ... zegt wat er per element moet gebeuren.
  • Reeks-methoden zijn uitgesteld; ToList() en samenvattende methoden voeren direct uit.

Volgende les: async en await, voor werk dat moet wachten op netwerk of schijf.

Lab

Cijferlijst-analyse met LINQ

💻 Dit lab doe je op je eigen computer met Docker. Start de lab-container met (geeft de opdracht een eigen startcommando, gebruik dan dat):

$ docker run -it --rm registry.wizg.it/webhosting/beetje-labs:aspnet-les3

Volg daarna de stappen van de opdracht hierboven. Nog geen Docker? Zie les 14 van Een beetje Linux & Docker. Voor dit lab is er geen live lab in de browser.

Klaar? Controleer je werk met:

$ beetje check aspnet-les3

📝 Quiz: LINQ en collecties

5 vragen · 70% nodig om te slagen

1. Welke methode houdt alleen de elementen die aan een voorwaarde voldoen?
2. Wanneer wordt een Where-query uitgevoerd?
3. Wat geeft FirstOrDefault als er niets voldoet (bij een record)?
4. Hoe sorteer je op vak en daarbinnen op cijfer?
5. Welke van deze voert direct uit?