Les 3
LINQ en collecties
Data filteren, sorteren en groeperen met LINQ.
🎯 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
GroupBymaakt groepen. Elke groepgheeft eenKey(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
Wheregebeurt nog niets.grootis geen lijst, maar een recept: "filterlijstopx > 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
grootopnieuw doorloopt (bijvoorbeeld metgroot.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
- Lees in een console-app een CSV-bestand met naam, vak en cijfer in met
File.ReadAllLinesen zet elke regel om naar eenResultaat. - Toon per leerling het gemiddelde, gesorteerd van hoog naar laag.
- Toon per vak het aantal onvoldoendes en het beste resultaat.
- Toon welke leerlingen voor alle vakken een voldoende hebben.
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 metFirst,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
Klaar? Controleer je werk met:
$ beetje check aspnet-les3