Klassen in Klassen
Kurz gesagt: Eine verschachtelte Klasse ist eine Klasse, die innerhalb einer anderen Klasse deklariert ist, weil sie nur dort Sinn ergibt; in C# sieht sie die private-Member der äußeren Klasse, ist aber an kein äußeres Objekt gebunden.
Problem
Eine Klasse, die nur an einer Stelle Sinn ergibt
In der Lektion über die verkettete Liste war Node eine eigene Klasse. Jeder im Programm kann Knoten anlegen, aber sie ergeben nur in der Liste Sinn. Genauso ist es mit einer Zeile der Bestellung:
class OrderLine{ public string Product { get; set; } public int Quantity { get; set; } public double Price { get; set; }}
class Order{ public List<OrderLine> Lines { get; } = new List<OrderLine>();}OrderLine ohne Order ergibt keinen Sinn. Der Name ist lang, damit er nicht mit der Zeile einer Rechnung, InvoiceLine, oder mit der Zeile im Lager zusammenstößt. Und nichts sagt, dass die beiden Klassen zusammengehören. Kann die Zeile innerhalb der Bestellung leben?
Die Idee
Verschachtelte Klasse
Ja. Eine Klasse, die innerhalb einer anderen Klasse deklariert ist, heißt verschachtelte Klasse (nested class):
class Order{ public class Line { public string Product { get; } public int Quantity { get; } public double Price { get; }
public Line(string product, int quantity, double price) { Product = product; Quantity = quantity; Price = price; }
public double Total() { return Quantity * Price; } }
private List<Line> lines = new List<Line>();
public void Add(Line line) { lines.Add(line); }
public double Total() { double sum = 0; foreach (Line line in lines) { sum += line.Total(); } return sum; }}var order = new Order();order.Add(new Order.Line("Kaffeebohnen", 2, 12.5));order.Add(new Order.Line("Tasse", 4, 3.0));Console.WriteLine($"Gesamt: {order.Total():F2} €");Gesamt: 37.00 €- Von außen heißt sie
Order.Line. Innerhalb vonOrderheißt sie nurLine. - Die Zeile ist an keine bestimmte Bestellung gebunden.
new Order.Line(...)entsteht ohne einOrder-Objekt, wie bei einer normalen Klasse. - Eine verschachtelte Klasse, vor deren
classnichts steht, istprivate: Niemand außerhalb vonOrderkann sie benutzen. Darum steht hierpublic class Line. Ohnepublichält der Compiler beinew Order.Line(...)an:
class Order{ class Line { }}error CS0122: 'Order.Line' is inaccessible due to its protection levelSo eine Klasse hast du schon benutzt, ohne sie zu sehen: Keys eines Dictionary ist ein Objekt von KeyCollection, einer Klasse, die in Dictionary verschachtelt ist:
var stock = new Dictionary<string, int>();Console.WriteLine(stock.Keys.GetType().Name);Console.WriteLine(stock.Keys.GetType().DeclaringType.Name);KeyCollectionDictionary`2Dictionary`2 ist der Name, unter dem die Laufzeitumgebung Dictionary<TKey, TValue> führt: 2 ist die Anzahl der Typen in den < >.
Genauer, für Neugierige
- In Java schreibt man so eine Klasse
static class Line, und sie heißt statische verschachtelte Klasse. In C# bedeutetstaticvor einer Klasse etwas anderes: Eine statische Klasse ist eine Klasse ohne Objekte, nur mitstatic-Membern, wieMath. Mitnewgeht bei ihr nichts:
class Order{ public static class Line { }}error CS0712: Cannot create an instance of the static class 'Order.Line'- Jede verschachtelte Klasse in C# ist wie die statische in Java. Eine innere Klasse, die an ein äußeres Objekt gebunden ist, hat C# nicht. Was man stattdessen macht, steht in der nächsten Karte.
Die Idee
Das verschachtelte Objekt hält das äußere
Der Warenkorb hat einen Rabatt für alle Produkte darin. Jedes Produkt muss den Rabatt seines eigenen Warenkorbs kennen. In C# sieht eine verschachtelte Klasse das äußere Objekt nicht, darum wird der Warenkorb ausdrücklich übergeben:
class Cart{ private int discountPercent; private List<Item> items = new List<Item>();
public Cart(int discountPercent) { this.discountPercent = discountPercent; }
public class Item { private Cart cart; public string Name { get; } public double Price { get; }
public Item(Cart cart, string name, double price) { this.cart = cart; Name = name; Price = price; }
public double FinalPrice() { return Price * (100 - cart.discountPercent) / 100; } }
public void Add(string name, double price) { items.Add(new Item(this, name, price)); }
public void Print() { foreach (Item item in items) { Console.WriteLine($"{item.Name}: {item.FinalPrice():F2} €"); } }}var cart = new Cart(10);cart.Add("Kopfhörer", 80);cart.Add("Hülle", 20);cart.Print();Kopfhörer: 72.00 €Hülle: 18.00 €Add übergibt this, und Item hält sie im Feld cart. FinalPrice liest den discountPercent des Warenkorbs, in dem es liegt. Das Feld ist private, aber Item ist innerhalb von Cart geschrieben, und eine verschachtelte Klasse sieht die private-Member der äußeren.var cart = new Cart(10);cart.Add("Kopfhörer", 80);cart.Add("Hülle", 20);cart.Print();Schritt für Schritt, in Worten
- Der Warenkorb ist ein Cart-Objekt mit 10% Rabatt.
- Add erzeugt ein Item und übergibt ihm den Warenkorb, this. Neben Name und Price hält jedes Item in cart einen Pfeil zu seinem Warenkorb.
- Das zweite Item zeigt auf denselben Warenkorb.
- FinalPrice von „Kopfhörer“ folgt dem Pfeil cart und liest discountPercent: 80 * (100 - 10) / 100 = 72.
- FinalPrice von „Hülle“ folgt dem Pfeil cart und liest discountPercent: 20 * (100 - 10) / 100 = 18.
Genauer, für Neugierige
- Von außen wird ein
Item-Objekt mit dem Warenkorb als erstem Argument erzeugt. Meist erzeugt die äußere Klasse die inneren selbst, wieaddhier:
var cart = new Cart(10);var cable = new Cart.Item(cart, "Kabel", 10);Console.WriteLine(cable.FinalPrice());9- Das Feld
cartist genau das, was in Java das versteckteCart.thismacht. Hier siehst du es einfach. - Benutzt die verschachtelte Klasse die Felder der äußeren nicht, gib ihr das äußere Objekt nicht. So hält sie keine Referenz, die sie nicht braucht.
Probier es aus
Zwei Warenkörbe
Die Klasse Cart ist dieselbe. Ein Warenkorb hat keinen Rabatt, der andere 20 % für Clubmitglieder.
Was gibt das Programm aus?
var regular = new Cart(0);var member = new Cart(20);regular.Add("Buch", 15);member.Add("Buch", 15);regular.Print();member.Print();Jedes Item sieht den Rabatt des Warenkorbs, der es erzeugt hat. Das Buch in regular hat 0 %, das in member 20 %.
Probier es aus
Finde den Fehler
error CS0120: An object reference is required for the non-static field, method, or property 'Order.currency'In welcher Zeile liegt das Problem?
Wähle die Zeile mit dem Fehler.
currency ist ein Feld eines Order-Objekts, und Line hat keine Bestellung. Eine verschachtelte Klasse in C# ist an kein äußeres Objekt gebunden, darum gibt es keine Bestellung, deren Währung sie lesen könnte. Die Lösung: Line bekommt die Bestellung, new Order.Line(order), hält sie in einem Feld und liest order.currency. Dass currency private ist, stört nicht: Line liegt innerhalb von Order. Ein anderer Weg ist, currency static zu machen, wenn die Währung für alle Bestellungen dieselbe ist.Die Idee
Statt einer Klasse in einer Methode: ein Tupel
In Java kann man eine Klasse auch in einer Methode deklarieren. Dann ist sie nur dort sichtbar und heißt lokale Klasse. C# hat so etwas nicht: Eine Klasse in einer Methode kompiliert nicht. Braucht eine Methode ein paar Werte, die nur in ihr zusammengehören, hat C# ein Tupel (tuple):
static void PrintBestSeller(string[] names, int[] sold){ (string Name, int Count) best = (names[0], sold[0]); for (int i = 1; i < names.Length; i++) { if (sold[i] > best.Count) { best = (names[i], sold[i]); } } Console.WriteLine($"Bestseller: {best.Name} ({best.Count} Stk.)");}string[] names = { "Kaffee", "Tee", "Wasser" };int[] sold = { 120, 45, 300 };PrintBestSeller(names, sold);Bestseller: Wasser (300 Stk.)(string Name, int Count)ist der Typ: zwei Werte, jeder mit seinem eigenen Namen.best.Nameundbest.Countliest man wie Properties.(names[i], sold[i])macht ein neues Tupel, und es wird auf einmal zugewiesen.- Außerhalb von
PrintBestSellerbenutzt es niemand. Braucht man dieselben Daten auch woanders oder haben sie Verhalten, sind sie eine Klasse, wieOrder.Line.
Die Idee
Eine anonyme Klasse gibt es nicht
Die Rabatte im Shop sind ein Interface:
interface IDiscount{ double Apply(double price);}Bisher hast du für jeden Rabatt eine Klasse gemacht: class BlackFriday : IDiscount { ... }. Für einen Rabatt, der nur an einer Stelle benutzt wird, hat Java eine anonyme Klasse: eine Klasse ohne Namen, genau dort geschrieben, new Discount() { ... }. C# hat so etwas nicht: new IDiscount() { ... } kompiliert nicht. Eine Klasse hat immer einen Namen. Wird sie nur in einer Klasse gebraucht, ist sie verschachtelt und private, wie in dieser Lektion:
class Checkout{ private class BlackFriday : IDiscount { public double Apply(double price) { return price * 70 / 100; } }
private class NoDiscount : IDiscount { public double Apply(double price) { return price; } }
public static IDiscount For(string promo) { if (promo == "black friday") { return new BlackFriday(); } return new NoDiscount(); }}
Console.WriteLine(Checkout.For("black friday").Apply(100));Console.WriteLine(Checkout.For("ohne").Apply(100));70100BlackFridayundNoDiscountsind außerhalb vonCheckoutnicht sichtbar. Von außen weiß man nur, dassForeinIDiscountzurückgibt.100 * 70 / 100mitdoubleist70. C# gibt einendoubleohne.0aus, wenn die Zahl ganz ist.
Genauer, für Neugierige
- Für eine einzige Aktion, wie den Rabatt, hat C# eine kürzere Schreibweise: ein Delegate und einen Lambda-Ausdruck. Dann brauchst du keine Klasse. Du siehst sie im Modul „Modernes C#“.
- Etwas mit „anonym“ im Namen hat C# doch: einen anonymen Typ.
new { Name = "Wasser", Count = 300 }macht ein Objekt einer Klasse ohne Namen, nur mit Properties zum Lesen. Methoden hat es keine, und ein Interface kann es nicht implementieren, darum ersetzt es die anonyme Klasse nicht:
var sale = new { Name = "Wasser", Count = 300 };Console.WriteLine(sale.Name);Console.WriteLine(sale);Wasser{ Name = Wasser, Count = 300 }Die Idee
IComparer: Sortieren nach eigener Regel
Sort hat Text und Zahlen sortiert. Schüler kann es aber nicht sortieren:
class Student{ public string Name { get; } public double Grade { get; }
public Student(string name, double grade) { Name = name; Grade = grade; }
public override string ToString() { return $"{Name} {Grade}"; }}var students = new List<Student>{ new Student("Jonas", 5.50), new Student("Maria", 6.00), new Student("Peter", 4.75), new Student("Anna", 5.50)};students.Sort();InvalidOperationException: Failed to compare two elements in the array.Der Compiler hat nichts dagegen, aber beim Ausführen bricht das Programm ab. C# weiß nicht, wonach es Schüler sortieren soll: nach Name, nach Note, nach Alter?
Die Regel gibst du getrennt an, als IComparer. Das ist ein Interface mit einer Methode, Compare(a, b). Sie gibt eine negative Zahl zurück, wenn a vor b kommen soll, null, wenn beide gleich sind, und eine positive Zahl, wenn a nach b kommt. Du schreibst eine Klasse, die es implementiert, und übergibst Sort ein Objekt davon:
class ByGradeDescending : IComparer<Student>{ public int Compare(Student a, Student b) { return b.Grade.CompareTo(a.Grade); }}
students.Sort(new ByGradeDescending());Console.WriteLine(string.Join(", ", students));Maria 6, Jonas 5.5, Anna 5.5, Peter 4.75b.Grade.CompareTo(a.Grade)vergleicht andersherum, darum steht die höchste Note vorn.- Jonas und Anna haben dieselbe Note. Hier sind sie in der Reihenfolge geblieben, in der sie waren, aber
List.Sortverspricht das nicht: Bei mehr Schülern können gleiche ihre Plätze tauschen. Ist die Reihenfolge bei gleicher Note wichtig, sag es inCompare, wie in der Übung. string.JoinruftToStringfür jeden Schüler auf.- Für Text und Zahlen in umgekehrter Reihenfolge:
Sort()und danachReverse().
var cities = new List<string> { "Hamburg", "Berlin", "München" };cities.Sort();cities.Reverse();Console.WriteLine(string.Join(", ", cities));München, Hamburg, BerlinGenauer, für Neugierige
IComparer<T>liegt inSystem.Collections.Generic, mit einem Typparameter:IComparer<Student>vergleicht Schüler.- Text und Zahlen werden ohne
IComparersortiert, weilstringundintdas InterfaceIComparable<T>mit der MethodeCompareToimplementieren. Auch deine Klasse kann es implementieren, wenn sie eine natürliche Reihenfolge hat. Dann funktioniertSort()ohne alles andere:
class City : IComparable<City>{ public string Name { get; } public int Population { get; }
public City(string name, int population) { Name = name; Population = population; }
public int CompareTo(City other) { return Population.CompareTo(other.Population); }
public override string ToString() { return Name; }}
var cities = new List<City> { new City("Berlin", 3800000), new City("Kiel", 250000), new City("Hamburg", 1900000) };cities.Sort();Console.WriteLine(string.Join(", ", cities));Kiel, Hamburg, BerlinIComparer ist für die Fälle, in denen es mehrere Reihenfolgen gibt.
CompareToschützt dich vor Fehlern.a.Grade - b.Gradekompiliert nicht einmal, weil es eindoubleist undCompareeinenintzurückgibt:
class ByGrade : IComparer<Student>{ public int Compare(Student a, Student b) { return a.Grade - b.Grade; }}error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?)Mit int kompiliert die Subtraktion, aber bei sehr großen Zahlen läuft sie über, und das Vorzeichen kippt.
- Eine ganze Klasse für einen Vergleich ist viel Code. Im Modul „Modernes C#“ wird die Regel
Sortdirekt übergeben, in einer Zeile.
Probier es aus
Was steht vorn?
Was gibt das Programm aus?
class ByPrice : IComparer<Product>{ public int Compare(Product a, Product b) { return a.Price.CompareTo(b.Price); }}
var products = new List<Product>{ new Product("Monitor", 220), new Product("Maus", 25), new Product("Tastatur", 60)};
products.Sort(new ByPrice());foreach (Product p in products){ Console.WriteLine(p.Name);}a.Price.CompareTo(b.Price) ist negativ, wenn a billiger ist. Darum kommt das Billigere zuerst: 25, 60, 220. Damit das Teuerste vorn steht, vertausche a und b.Übung
Klassen, die zusammengehören
Löse die Aufgaben in deinem Editor.
1. Warenkorb. Eine Klasse ShoppingCart mit einer verschachtelten Klasse Item (Name und Preis) und den Methoden AddItem(Item item), GetTotal() und Count(). Füge in Main den Artikel new ShoppingCart.Item("Milch", 2.5) hinzu, dazu Brot für 1,20 € und Kaffee für 7,50 €. Gib die Summe mit :F2 aus.
Artikel: 3Gesamt: 11.20 €2. Monitor für einen Webserver. Eine Klasse WebServer mit einer verschachtelten Klasse Request (Adresse und Statuscode) und den Methoden AddRequest(Request r) und PrintStats(). Die Statistik sagt, wie viele Anfragen es gibt, wie viele es von jedem Statuscode gibt (sortiert) und wie viel Prozent erfolgreich sind, also mit einem Code von 200 bis 299. Halte die Zähler in einem SortedDictionary<int, int>: Es hält die Schlüssel sortiert. Die Anfragen sind /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Anfragen: 5200: 3401: 1500: 1Erfolgreich: 60%3. Hausverwaltung. Eine Klasse Building mit einer verschachtelten Klasse Apartment (Nummer, Anzahl der Zimmer, Eigentümer) und den Methoden AddApartment(Apartment a) und GetApartmentInfo(int number). Halte die Wohnungen in einem Dictionary<int, Apartment>. Füge 101 mit 3 Zimmern von Schmidt und 102 mit 2 Zimmern von Müller hinzu. Suche 101 und 999.
Wohnung 101: 3 Zimmer, Eigentümer SchmidtWohnung 999 gibt es nicht.4. Rangliste. Sortiere die Schüler aus der Lektion nach Note, von der höchsten abwärts. Bei gleicher Note sortiere sie nach Namen, alphabetisch. Alles steckt in einem IComparer<Student>: Ist der Vergleich nach Note nicht 0, gib ihn zurück, sonst vergleiche die Namen mit string.Compare.
Maria 6Anna 5.5Jonas 5.5Peter 4.75Weitere Aufgaben
- Computer-Konfigurator. Eine Klasse
Computermit den verschachtelten KlassenCpu(Marke, Takt in GHz),Ram(GB) undStorage(GB) und den MethodenSetCpu,SetRam,SetStorage.ToStringbeschreibt die Konfiguration:Intel 3.5 GHz, 16 GB RAM, 512 GB. - Reiseplaner, noch einmal. Löse die Aufgabe aus der Lektion über
Dictionarymit einer verschachtelten KlasseTripPlanner.Destination, die den Namen und die Liste der Aktivitäten hält. - Kontaktnetz, noch einmal. Verschiebe
Contactaus der Aufgabe in der Lektion überDictionaryinContactNetwork. SollteContactsein Netz kennen? - Kopfrechnen, noch einmal. Verschiebe
Questionaus der Aufgabe in der Lektion über Listen inMathGame. - Produkte auf zwei Arten. Sortiere eine Liste von Produkten nach Preis, vom teuersten an, und bei gleichem Preis nach Namen. Sortiere dann dieselbe Liste nur nach Namen, in umgekehrter Reihenfolge.
- Dein eigener Rabatt. Schreib drei in
Checkoutverschachtelte Klassen, dieIDiscountimplementieren: 10 % ab 100 €, feste 5 € Rabatt und „zweites Produkt zum halben Preis“. Wende sie auf ein und denselben Warenkorb an.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Interfaces · Exceptions · Listen · Dictionary und HashSet
- Geht weiter in: Lambdas und Delegates · LINQ · Entwurfsmuster · Tests mit xUnit
- Aufgaben zum Thema: Collections