Listen, Dictionaries und LINQ
Kurz gesagt: Eine List hält Elemente in einer Reihenfolge und wächst mit Add, ein Dictionary hält Paare aus Schlüssel und Wert, und LINQ fragt mit Where und Select die Daten mit Lambdas ab.
Problem
Der Warenkorb wächst und wächst
Eine Kundin des Onlineshops legt Kaffee, Tee und Kaffeesahne in den Warenkorb, dann noch Kaffeefilter. Danach nimmt sie die Sahne wieder heraus. Du kannst vorher nicht wissen, wie viele Produkte sie kauft. Ein Array will seine Größe aber schon bei new:
string[] cart = new string[3];cart[0] = "Kaffeebohnen";cart[1] = "Grüner Tee";cart[2] = "Kaffeesahne";
try{ cart[3] = "Kaffeefilter";}catch (IndexOutOfRangeException e){ Console.WriteLine("Passt nicht: " + e.Message);}Passt nicht: Index was outside the bounds of the array.Es gibt ein zweites Problem. Das Lager führt die Produkte nach Code, etwa C-101. Um die Menge für einen Code in einem Array zu finden, gehst du jedes Mal das ganze Array durch. Bei 10 000 Produkten und 100 Bestellungen pro Minute ist das viel unnötiges Durchlaufen.
Sieh es dir an
List<T>: ein Array, das wächst
Eine Liste (List) hält Elemente in einer Reihenfolge, wie ein Array, aber sie wächst mit Add und schrumpft mit Remove:
List<string> cart = new List<string>();cart.Add("Kaffeebohnen");cart.Add("Grüner Tee");cart.Add("Kaffeesahne");cart.Add("Kaffeefilter");Console.WriteLine("Im Warenkorb: " + cart.Count);Console.WriteLine("Erstes: " + cart[0]);
cart.Remove("Kaffeesahne");Console.WriteLine("Noch Sahne da? " + cart.Contains("Kaffeesahne"));foreach (string item in cart){ Console.WriteLine("- " + item);}Im Warenkorb: 4Erstes: KaffeebohnenNoch Sahne da? False- Kaffeebohnen- Grüner Tee- Kaffeefilter- In
List<string>sagen die spitzen Klammern, was in der Liste steht: Strings.List<int>ist eine Liste ganzer Zahlen undList<Product>eine Liste von Produkten. Der Typ in den Klammern heißt Typparameter. Deshalb schreiben wirList<T>, wobeiT„der Typ, den du wählst“ ist. Countist die Anzahl der Elemente (beim Array heißt sieLength). Der Index funktioniert wie beim Array:cart[0]ist das erste.Removenimmt das Element heraus, wenn es da ist, undContainssagt dir, ob es da ist.foreachgeht alle durch.
In diesem Modul haben die Programme keine Hülle aus class Program und Main: Die Zeilen stehen direkt in der Datei. Das sind die Anweisungen auf oberster Ebene aus der Lektion „Den Computer vorbereiten“: Der Compiler setzt Main für dich darum. Klassen, falls es welche gibt, stehen nach diesen Zeilen.
Genauer, für Neugierige
- Du kannst eine Liste gleich beim Erstellen füllen:
new List<int> { 2, 1, 3 }. Es gibt auch mehr Methoden:Insert(index, wert)setzt einen Wert an eine Position,RemoveAt(index)entfernt nach Position, undClear()leert die Liste.
List<int> quantities = new List<int> { 2, 1, 3 };quantities.Insert(0, 5);quantities.RemoveAt(2);Console.WriteLine(string.Join(", ", quantities));quantities.Clear();Console.WriteLine(quantities.Count);5, 2, 30string.Join(", ", liste)klebt die Elemente zu einem String zusammen, mit einem Trennzeichen dazwischen.- Innen ist eine Liste ein Array mit etwas Reserve. Wenn es voll ist, macht
Listein neues, doppelt so großes Array und kopiert die Elemente hinüber. Deshalb istAddmeist schnell, und du musst nicht an die Größe denken. - Wenn du einen Index außerhalb der Liste verwendest,
cart[10], ist die ExceptionArgumentOutOfRangeExceptionund nichtIndexOutOfRangeExceptionwie beim Array.
Probier es aus
Was in der Liste bleibt
Was gibt das Programm aus?
List<int> quantities = new List<int> { 2, 5, 2, 1 };quantities.Remove(2);quantities.Add(3);Console.WriteLine(quantities.Count + " " + quantities[0] + " " + quantities[3]);Remove(2) entfernt nur die erste 2. Übrig bleibt { 5, 2, 1 }, und Add(3) macht daraus { 5, 2, 1, 3 }. Vier Elemente: Das erste ist 5, das vierte 3.
Sieh es dir an
Ein Dictionary: Suche nach Code
Ein Dictionary (Dictionary) hält Paare aus Schlüssel → Wert. Hier ist der Schlüssel der Code des Produkts und der Wert die Menge auf Lager. Einen Schlüssel zu finden geht fast sofort, ohne Schleife, egal wie viele Produkte es gibt:
Dictionary<string, int> stock = new Dictionary<string, int>();stock["C-101"] = 12;stock["T-205"] = 30;stock["C-101"] = stock["C-101"] - 2;Console.WriteLine("Kaffee auf Lager: " + stock["C-101"]);Console.WriteLine("Gibt es X-999? " + stock.ContainsKey("X-999"));
if (stock.TryGetValue("T-205", out int tea)){ Console.WriteLine("Tee auf Lager: " + tea);}
try{ Console.WriteLine(stock["X-999"]);}catch (KeyNotFoundException){ Console.WriteLine("Kein Produkt mit dem Code X-999");}
foreach (KeyValuePair<string, int> pair in stock){ Console.WriteLine(pair.Key + ": " + pair.Value + " Stk.");}Kaffee auf Lager: 10Gibt es X-999? FalseTee auf Lager: 30Kein Produkt mit dem Code X-999C-101: 10 Stk.T-205: 30 Stk.Dictionary<string, int>: Die Schlüssel sind Strings, die Werte ganze Zahlen.stock["C-101"] = 12schreibt. Wenn der Schlüssel schon da ist, wird der Wert ersetzt.stock["C-101"]liest.- Einen Schlüssel zu lesen, den es nicht gibt, wirft eine
KeyNotFoundException. Du fängst sie mittry/catch, wie im Modul „Fehler und Exceptions“. Besser ist es, gar nicht so weit zu kommen:ContainsKeysagt dir, ob der Schlüssel da ist, undTryGetValueprüft und liest in einem Schritt. Der Wert kommt überoutzurück, wie beiint.TryParse. foreachgeht die Paare durch. Jedes ist einKeyValuePairmitKeyundValue.
Genauer, für Neugierige
Add(schlüssel, wert)schreibt auch, wirft aber eineArgumentException, wenn der Schlüssel schon da ist. So überschreibst du keinen Preis aus Versehen. Ein Dictionary kannst du beim Erstellen füllen, mit Paaren in geschweiften Klammern:
Dictionary<string, decimal> prices = new Dictionary<string, decimal>{ { "C-101", 18.90m }, { "T-205", 6.40m },};try{ prices.Add("C-101", 19.50m);}catch (ArgumentException){ Console.WriteLine("C-101 hat schon einen Preis: " + prices["C-101"] + " €");}prices.Remove("T-205");Console.WriteLine("Produkte mit Preis: " + prices.Count);C-101 hat schon einen Preis: 18.90 €Produkte mit Preis: 1- Schlüssel sind eindeutig, Werte nicht. Zwei Produkte können beide die Menge 30 haben, aber den Code
C-101gibt es nur einmal. - Die Reihenfolge, in der
foreachdir die Paare gibt, ist nicht versprochen. Heute ist es die Reihenfolge des Hinzufügens, wenn du nichts entfernt hast, aber verlass dich nicht darauf. Wenn du eine Reihenfolge brauchst, sortiere die Schlüssel. - Ein Dictionary findet einen Schlüssel schnell, weil es daraus eine Zahl berechnet (einen Hash) und damit weiß, wo es suchen muss. Deshalb darf sich ein Schlüssel nicht ändern, solange er im Dictionary steht.
Probier es aus
Ein Code, den es nicht gibt
Das Programm soll den Preis des Produkts mit dem Code C-102 zeigen. Stattdessen bricht es mit einer Exception ab. In welcher Zeile liegt das Problem?
Wähle die Zeile mit dem Fehler.
Der Schlüssel "C-102" steht nicht im Dictionary, also wirft prices[code] eine KeyNotFoundException. Wenn der Code von außen kommt (von der Kundin, aus einer Datei), prüf ihn: if (prices.TryGetValue(code, out decimal price)), und im else gib „Kein Preis für C-102“ aus.
Die Idee
Eine Regel als Wert: das Lambda
Die Preise ab 50 € zu finden ist eine Schleife mit einem if:
List<int> prices = new List<int> { 12, 75, 40, 130, 58 };List<int> expensive = new List<int>();foreach (int p in prices){ if (p >= 50) { expensive.Add(p); }}Console.WriteLine(string.Join(", ", expensive));75, 130, 58Morgen brauchst du die Produkte unter 10 €, dann nur die Tees. Die Schleife ist dieselbe, nur die Bedingung im if ändert sich. Praktisch wäre es, die Bedingung als Wert an eine Methode zu übergeben, die die Schleife für dich macht.
So eine Bedingung schreibt man als Lambda: p => p >= 50. Lies es als „gib für jedes p den Wert p >= 50“. Links von => steht der Parameter, rechts das Ergebnis. Ein Lambda ist eine kurze Methode ohne Namen, ohne return und ohne Typen: C# ermittelt sie selbst.
Genauer, für Neugierige
Ein Lambda hat einen Typ, wie jeder Wert. Func<int, bool> ist „eine Methode, die einen int nimmt und einen bool zurückgibt“. Du kannst sie in einer Variable speichern und wie eine Methode aufrufen. Ein Lambda kann auch einen Rumpf in geschweiften Klammern mit einem return haben, wenn ein Ausdruck nicht reicht:
Func<int, bool> expensive = p => p >= 50;Console.WriteLine(expensive(75) + " " + expensive(12));
Func<decimal, bool> overWithVat = p =>{ decimal withVat = p * 1.2m; return withVat > 50;};Console.WriteLine(overWithVat(45.00m));True FalseTrueSieh es dir an
Where und Select
List<int> prices = new List<int> { 12, 75, 40, 130, 58 };List<string> labels = prices .Where(p => p >= 50) .Select(p => p + " €") .ToList();Console.WriteLine(string.Join(", ", labels));Schritt für Schritt, in Worten
- Fünf Preise. Die Abfrage legt nur den Weg an: Noch ist nichts hindurchgegangen.
- 12 besteht p >= 50 nicht und fällt heraus. Es kommt nie bei Select an.
- 75 besteht Where, Select macht daraus den Text „75 €“, ToList() sammelt ihn.
- Auch 40 fällt heraus. Jedes Element geht den ganzen Weg, bevor das nächste startet.
- 130 und 58 kommen durch. Die Liste prices ändert sich nicht.
- Das Ergebnis ist eine neue Liste mit drei Strings.
Die Methoden Where und Select kommen aus LINQ (Language Integrated Query): Abfragen über Daten, in die Sprache eingebaut. Sie funktionieren mit jeder Liste und jedem Array:
Where(p => p >= 50)lässt nur die Elemente durch, für die das Lambdatrueergibt. Das ist die Schleife mit demifvon der vorigen Karte.Select(p => p + " €")macht aus jedem Element etwas anderes, hier einen String.ToList()sammelt das Ergebnis in einer neuenList. Die Listepricesändert sich nicht.
Die Elemente gehen einzeln hindurch: 12 fällt bei Where heraus und kommt nie bei Select an.
Sieh es dir an
Die Produkte des Shops
Ein echter Katalog ist eine Liste von Objekten. Die Klasse Product hat nur Eigenschaften, also werden die Objekte mit den Eigenschaften in geschweiften Klammern erzeugt: new Product { Id = 1, Name = "…" }. Das ist dasselbe wie new Product() und danach p.Id = 1; für jede Eigenschaft.
List<Product> catalog = new List<Product>{ new Product { Id = 1, Name = "Espressobohnen", Price = 24.90m, Category = "kaffee" }, new Product { Id = 2, Name = "Sencha-Grüntee", Price = 9.80m, Category = "tee" }, new Product { Id = 3, Name = "Kolumbienbohnen", Price = 31.50m, Category = "kaffee" }, new Product { Id = 4, Name = "Pfefferminze", Price = 4.20m, Category = "tee" }, new Product { Id = 5, Name = "Kaffeemühle", Price = 64.00m, Category = "zubehör" },};
public class Product{ public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string Category { get; set; }}Das Lambda bekommt ein ganzes Produkt, also liest die Bedingung seine Eigenschaften:
List<Product> coffee = catalog.Where(p => p.Category == "kaffee").ToList();Console.WriteLine("Kaffeesorten: " + coffee.Count);
List<string> byPrice = catalog .OrderByDescending(p => p.Price) .Select(p => p.Name + ": " + p.Price + " €") .ToList();foreach (string line in byPrice){ Console.WriteLine(line);}Kaffeesorten: 2Kaffeemühle: 64.00 €Kolumbienbohnen: 31.50 €Espressobohnen: 24.90 €Sencha-Grüntee: 9.80 €Pfefferminze: 4.20 €Where(p => p.Category == "kaffee")behält nur die Kaffeesorten.OrderByDescending(p => p.Price)sortiert nach Preis, vom höchsten an.OrderBysortiert vom niedrigsten an. Das Lambda sagt, wonach sortiert wird.- Die Methoden werden hintereinander verkettet: Jede bekommt das Ergebnis der vorigen.
Der Katalog ist derselbe. Was wird ausgegeben?
List<string> names = catalog .Where(p => p.Price < 30) .OrderBy(p => p.Price) .Select(p => p.Name) .ToList();Console.WriteLine(names.Count + ": " + names[0]);Drei Produkte kosten weniger als 30 €: der Espresso (24,90 €), der Sencha (9,80 €) und die Pfefferminze (4,20 €). OrderBy sortiert sie vom günstigsten an, also kommt die Pfefferminze zuerst.
Sieh es dir an
Eine Antwort aus der ganzen Liste
Manche LINQ-Methoden geben keine Liste zurück, sondern einen einzigen Wert: eine Summe, eine Anzahl, „gibt es eins“, ein Produkt:
Console.WriteLine("Summe: " + catalog.Sum(p => p.Price) + " €");Console.WriteLine("Durchschnitt: " + catalog.Average(p => p.Price) + " €");Console.WriteLine("Unter 5 €: " + catalog.Count(p => p.Price < 5));Console.WriteLine("Gibt es Zubehör? " + catalog.Any(p => p.Category == "zubehör"));
Product tea = catalog.First(p => p.Category == "tee");Console.WriteLine("Erster Tee: " + tea.Name);Product cocoa = catalog.FirstOrDefault(p => p.Category == "kakao");Console.WriteLine(cocoa == null ? "Kein Kakao" : cocoa.Name);Summe: 134.40 €Durchschnitt: 26.88 €Unter 5 €: 1Gibt es Zubehör? TrueErster Tee: Sencha-GrünteeKein KakaoSumundAverageaddieren und mitteln, was das Lambda liefert.Count(regel)zählt die passenden, undAny(regel)sagt dir, ob es mindestens eins gibt.First(regel)gibt das erste passende zurück und wirft eineInvalidOperationException, wenn es keins gibt.FirstOrDefaultgibt im selben Fallnullzurück. Nimm es, wenn es normal ist, dass es kein Ergebnis gibt, und prüf aufnull.
Welche Methode liefert den Gesamtwert des Katalogs?
decimal total = catalog.(p => p.Price);Sum(p => p.Price) addiert die Preise aller Produkte: 134,40 €.
Genauer, für Neugierige
GroupByteilt die Elemente nach einem Schlüssel in Gruppen. Jede Gruppe hat einenKeyund verhält sich wie eine Liste, also funktionierenCountundSumdarauf. Der Typ der Gruppe ist lang (IGrouping<string, Product>), deshalb istvarhier praktisch:
foreach (var group in catalog.GroupBy(p => p.Category)){ Console.WriteLine(group.Key + ": Anzahl " + group.Count() + ", gesamt " + group.Sum(p => p.Price) + " €");}kaffee: Anzahl 2, gesamt 56.40 €tee: Anzahl 2, gesamt 14.00 €zubehör: Anzahl 1, gesamt 64.00 €- Dieselbe Abfrage lässt sich auch in Abfragesyntax schreiben, die wie SQL aussieht. Der Compiler macht daraus
Where,OrderByundSelect:
List<string> teas = (from p in catalog where p.Category == "tee" orderby p.Price select p.Name).ToList();Console.WriteLine(string.Join(", ", teas));Pfefferminze, Sencha-Grüntee- Verzögerte Ausführung.
WhereundSelectgehen nichts durch, bis jemand das Ergebnis verlangt:ToList,foreach,Sum,string.Join. Bis dahin ist die Abfrage nur ein Rezept vom TypIEnumerable<int>(„etwas, das man durchgehen kann“). Deshalb landet hier 130 im Ergebnis, obwohl es erst nachWherehinzugefügt wurde:
List<int> prices = new List<int> { 12, 75 };IEnumerable<int> expensive = prices.Where(p => p >= 50);prices.Add(130);Console.WriteLine(string.Join(", ", expensive));75, 130- Wenn du das Ergebnis so brauchst, wie es in einem bestimmten Moment ist, ruf sofort
ToList()auf: Dann ist die Liste fertig, und spätere Änderungen betreffen sie nicht. - Die Preise hier sind
decimal: Bei Geld gibtdoubleRundungsfehler.Sumbehält die zwei Nachkommastellen: 134.40. Die Zahlen werden mit Dezimalpunkt ausgegeben, weil die Codeprüfung ohne Regionaleinstellungen läuft. Auf einem Computer mit deutschen Einstellungen würdest du 134,40 sehen.
Übung
Warenkorb, Lager und Bericht
Löse die Aufgaben in deinem Editor. Verwende die Klasse Product aus der Lektion.
1. Warenkorb. Leg Espressobohnen (24,90 €), Pfefferminze (4,20 €) und eine Kaffeemühle (64,00 €) in eine List<Product>. Gib die Anzahl und die Summe aus. Dann such die Mühle mit First nach Id, entferne sie und gib noch einmal aus:
Produkte: 3, gesamt 93.10 €Entfernt: KaffeemühleProdukte: 2, gesamt 29.10 €2. Lager. Ein Dictionary von Code → Menge: C-101 12 Stück, T-205 30 Stück, G-300 2 Stück. Schreib eine Methode Sell(stock, code, quantity), die die Menge verringert und eine Meldung über das Ergebnis zurückgibt. Wenn der Code fehlt oder nicht genug da ist, ändert sich das Dictionary nicht. Verkaufe 5 von C-101, 3 von G-300 und 1 von X-999, dann gib das Lager aus:
C-101: verkauft 5, 7 übrigG-300: nur noch 2 übrigKein Produkt X-999C-101: 7T-205: 30G-300: 23. Bericht. Gib mit dem Katalog aus der Lektion die drei teuersten Produkte und die Summe der Preise pro Kategorie aus. Take(3) nimmt die ersten drei Elemente. Für die Summen nimm ein Dictionary von Kategorie → Summe (oder GroupBy aus „Genauer, für Neugierige“):
Die drei teuersten: Kaffeemühle 64.00 € Kolumbienbohnen 31.50 € Espressobohnen 24.90 €Nach Kategorie: kaffee: 56.40 € tee: 14.00 € zubehör: 64.00 €Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Kapselung · static: was alle Objekte teilen · Exceptions · try, catch und finally
- Geht weiter in: async und await · Wie das Internet funktioniert · Ein Webserver mit Minimal API · MVC: Model, View, Controller
- Aufgaben zum Thema: Web und Desktop