Zum Inhalt
wannacode

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);
}
Ausgabe
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);
}
Ausgabe
Im Warenkorb: 4
Erstes: Kaffeebohnen
Noch 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 und List<Product> eine Liste von Produkten. Der Typ in den Klammern heißt Typparameter. Deshalb schreiben wir List<T>, wobei T „der Typ, den du wählst“ ist.
  • Count ist die Anzahl der Elemente (beim Array heißt sie Length). Der Index funktioniert wie beim Array: cart[0] ist das erste.
  • Remove nimmt das Element heraus, wenn es da ist, und Contains sagt dir, ob es da ist. foreach geht 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, und Clear() 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);
Ausgabe
5, 2, 3
0
  • string.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 List ein neues, doppelt so großes Array und kopiert die Elemente hinüber. Deshalb ist Add meist schnell, und du musst nicht an die Größe denken.
  • Wenn du einen Index außerhalb der Liste verwendest, cart[10], ist die Exception ArgumentOutOfRangeException und nicht IndexOutOfRangeException wie 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]);
Antwort

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.");
}
Ausgabe
Kaffee auf Lager: 10
Gibt es X-999? False
Tee auf Lager: 30
Kein Produkt mit dem Code X-999
C-101: 10 Stk.
T-205: 30 Stk.
  • Dictionary<string, int>: Die Schlüssel sind Strings, die Werte ganze Zahlen.
  • stock["C-101"] = 12 schreibt. 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 mit try/catch, wie im Modul „Fehler und Exceptions“. Besser ist es, gar nicht so weit zu kommen: ContainsKey sagt dir, ob der Schlüssel da ist, und TryGetValue prüft und liest in einem Schritt. Der Wert kommt über out zurück, wie bei int.TryParse.
  • foreach geht die Paare durch. Jedes ist ein KeyValuePair mit Key und Value.
Genauer, für Neugierige
  • Add(schlüssel, wert) schreibt auch, wirft aber eine ArgumentException, 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);
Ausgabe
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-101 gibt es nur einmal.
  • Die Reihenfolge, in der foreach dir 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.

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));
Ausgabe
75, 130, 58

Morgen 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));
Ausgabe
True False
True

Sieh 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
  1. Fünf Preise. Die Abfrage legt nur den Weg an: Noch ist nichts hindurchgegangen.
  2. 12 besteht p >= 50 nicht und fällt heraus. Es kommt nie bei Select an.
  3. 75 besteht Where, Select macht daraus den Text „75 €“, ToList() sammelt ihn.
  4. Auch 40 fällt heraus. Jedes Element geht den ganzen Weg, bevor das nächste startet.
  5. 130 und 58 kommen durch. Die Liste prices ändert sich nicht.
  6. 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 Lambda true ergibt. Das ist die Schleife mit dem if von der vorigen Karte.
  • Select(p => p + " €") macht aus jedem Element etwas anderes, hier einen String.
  • ToList() sammelt das Ergebnis in einer neuen List. Die Liste prices ä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);
}
Ausgabe
Kaffeesorten: 2
Kaffeemü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. OrderBy sortiert 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]);
Antwort

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);
Ausgabe
Summe: 134.40 €
Durchschnitt: 26.88 €
Unter 5 €: 1
Gibt es Zubehör? True
Erster Tee: Sencha-Grüntee
Kein Kakao
  • Sum und Average addieren und mitteln, was das Lambda liefert.
  • Count(regel) zählt die passenden, und Any(regel) sagt dir, ob es mindestens eins gibt.
  • First(regel) gibt das erste passende zurück und wirft eine InvalidOperationException, wenn es keins gibt. FirstOrDefault gibt im selben Fall null zurück. Nimm es, wenn es normal ist, dass es kein Ergebnis gibt, und prüf auf null.

Welche Methode liefert den Gesamtwert des Katalogs?

decimal total = catalog.(p => p.Price);
Genauer, für Neugierige
  • GroupBy teilt die Elemente nach einem Schlüssel in Gruppen. Jede Gruppe hat einen Key und verhält sich wie eine Liste, also funktionieren Count und Sum darauf. Der Typ der Gruppe ist lang (IGrouping<string, Product>), deshalb ist var hier praktisch:
foreach (var group in catalog.GroupBy(p => p.Category))
{
Console.WriteLine(group.Key + ": Anzahl " + group.Count() + ", gesamt " + group.Sum(p => p.Price) + " €");
}
Ausgabe
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, OrderBy und Select:
List<string> teas = (from p in catalog
where p.Category == "tee"
orderby p.Price
select p.Name).ToList();
Console.WriteLine(string.Join(", ", teas));
Ausgabe
Pfefferminze, Sencha-Grüntee
  • Verzögerte Ausführung. Where und Select gehen nichts durch, bis jemand das Ergebnis verlangt: ToList, foreach, Sum, string.Join. Bis dahin ist die Abfrage nur ein Rezept vom Typ IEnumerable<int> („etwas, das man durchgehen kann“). Deshalb landet hier 130 im Ergebnis, obwohl es erst nach Where hinzugefü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));
Ausgabe
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 gibt double Rundungsfehler. Sum behä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:

Ausgabe
Produkte: 3, gesamt 93.10 €
Entfernt: Kaffeemühle
Produkte: 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:

Ausgabe
C-101: verkauft 5, 7 übrig
G-300: nur noch 2 übrig
Kein Produkt X-999
C-101: 7
T-205: 30
G-300: 2

3. 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“):

Ausgabe
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

1. Warum ist List<Product> für einen Warenkorb praktischer als ein Array Product[]?
2. stock ist ein Dictionary<string, int> ohne den Schlüssel "X-999". Was macht stock["X-999"]?
3. Was bedeutet p => p.Price > 50?
4. Der Katalog hat kein Produkt in der Kategorie "kakao". Was gibt catalog.FirstOrDefault(p => p.Category == "kakao") zurück?
5. Was ist der Unterschied zwischen Count und Count(p => p.Price < 5) bei einer Liste?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·