Zum Inhalt
wannacode

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} €");
Ausgabe
Gesamt: 37.00 €
  • Von außen heißt sie Order.Line. Innerhalb von Order heißt sie nur Line.
  • Die Zeile ist an keine bestimmte Bestellung gebunden. new Order.Line(...) entsteht ohne ein Order-Objekt, wie bei einer normalen Klasse.
  • Eine verschachtelte Klasse, vor deren class nichts steht, ist private: Niemand außerhalb von Order kann sie benutzen. Darum steht hier public class Line. Ohne public hält der Compiler bei new Order.Line(...) an:
class Order
{
class Line
{
}
}
Kompilierfehler
error CS0122: 'Order.Line' is inaccessible due to its protection level

So 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);
Ausgabe
KeyCollection
Dictionary`2

Dictionary`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# bedeutet static vor einer Klasse etwas anderes: Eine statische Klasse ist eine Klasse ohne Objekte, nur mit static-Membern, wie Math. Mit new geht bei ihr nichts:
class Order
{
public static class Line
{
}
}
Kompilierfehler
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();
Ausgabe
Kopfhörer: 72.00 €
Hülle: 18.00 €
Java hat eine innere Klasse (inner class): Jedes Objekt davon hält eine versteckte Referenz auf das äußere Objekt. C# hat so etwas nicht, darum ist die Referenz sichtbar: 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
  1. Der Warenkorb ist ein Cart-Objekt mit 10% Rabatt.
  2. 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.
  3. Das zweite Item zeigt auf denselben Warenkorb.
  4. FinalPrice von „Kopfhörer“ folgt dem Pfeil cart und liest discountPercent: 80 * (100 - 10) / 100 = 72.
  5. 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, wie add hier:
var cart = new Cart(10);
var cable = new Cart.Item(cart, "Kabel", 10);
Console.WriteLine(cable.FinalPrice());
Ausgabe
9
  • Das Feld cart ist genau das, was in Java das versteckte Cart.this macht. 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();
Antwort

Probier es aus

Finde den Fehler

Die Zeile soll die Währung der Bestellung zeigen. Das Programm kompiliert nicht:
Kompilierfehler
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.

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);
Ausgabe
Bestseller: Wasser (300 Stk.)
  • (string Name, int Count) ist der Typ: zwei Werte, jeder mit seinem eigenen Namen. best.Name und best.Count liest man wie Properties.
  • (names[i], sold[i]) macht ein neues Tupel, und es wird auf einmal zugewiesen.
  • Außerhalb von PrintBestSeller benutzt es niemand. Braucht man dieselben Daten auch woanders oder haben sie Verhalten, sind sie eine Klasse, wie Order.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));
Ausgabe
70
100
  • BlackFriday und NoDiscount sind außerhalb von Checkout nicht sichtbar. Von außen weiß man nur, dass For ein IDiscount zurückgibt.
  • 100 * 70 / 100 mit double ist 70. C# gibt einen double ohne .0 aus, 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);
Ausgabe
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();
Fehler
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));
Ausgabe
Maria 6, Jonas 5.5, Anna 5.5, Peter 4.75
  • b.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.Sort verspricht das nicht: Bei mehr Schülern können gleiche ihre Plätze tauschen. Ist die Reihenfolge bei gleicher Note wichtig, sag es in Compare, wie in der Übung.
  • string.Join ruft ToString für jeden Schüler auf.
  • Für Text und Zahlen in umgekehrter Reihenfolge: Sort() und danach Reverse().
var cities = new List<string> { "Hamburg", "Berlin", "München" };
cities.Sort();
cities.Reverse();
Console.WriteLine(string.Join(", ", cities));
Ausgabe
München, Hamburg, Berlin
Genauer, für Neugierige
  • IComparer<T> liegt in System.Collections.Generic, mit einem Typparameter: IComparer<Student> vergleicht Schüler.
  • Text und Zahlen werden ohne IComparer sortiert, weil string und int das Interface IComparable<T> mit der Methode CompareTo implementieren. Auch deine Klasse kann es implementieren, wenn sie eine natürliche Reihenfolge hat. Dann funktioniert Sort() 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));
Ausgabe
Kiel, Hamburg, Berlin

IComparer ist für die Fälle, in denen es mehrere Reihenfolgen gibt.

  • CompareTo schützt dich vor Fehlern. a.Grade - b.Grade kompiliert nicht einmal, weil es ein double ist und Compare einen int zurückgibt:
class ByGrade : IComparer<Student>
{
public int Compare(Student a, Student b)
{
return a.Grade - b.Grade;
}
}
Kompilierfehler
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 Sort direkt ü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);
}
Antwort

Ü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.

Ausgabe
Artikel: 3
Gesamt: 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.

Ausgabe
Anfragen: 5
200: 3
401: 1
500: 1
Erfolgreich: 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.

Ausgabe
Wohnung 101: 3 Zimmer, Eigentümer Schmidt
Wohnung 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.

Ausgabe
Maria 6
Anna 5.5
Jonas 5.5
Peter 4.75

Weitere Aufgaben

  • Computer-Konfigurator. Eine Klasse Computer mit den verschachtelten Klassen Cpu (Marke, Takt in GHz), Ram (GB) und Storage (GB) und den Methoden SetCpu, SetRam, SetStorage. ToString beschreibt die Konfiguration: Intel 3.5 GHz, 16 GB RAM, 512 GB.
  • Reiseplaner, noch einmal. Löse die Aufgabe aus der Lektion über Dictionary mit einer verschachtelten Klasse TripPlanner.Destination, die den Namen und die Liste der Aktivitäten hält.
  • Kontaktnetz, noch einmal. Verschiebe Contact aus der Aufgabe in der Lektion über Dictionary in ContactNetwork. Sollte Contact sein Netz kennen?
  • Kopfrechnen, noch einmal. Verschiebe Question aus der Aufgabe in der Lektion über Listen in MathGame.
  • 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 Checkout verschachtelte Klassen, die IDiscount implementieren: 10 % ab 100 €, feste 5 € Rabatt und „zweites Produkt zum halben Preis“. Wende sie auf ein und denselben Warenkorb an.

Test

Teste dich

1. class Item ist in class Cart verschachtelt. Sieht Item die Felder eines bestimmten Warenkorbs?
2. Warum kompiliert Show() von Order.Line nicht, wenn es die Währung der Bestellung liest?
3. Was schreibst du in C# statt einer anonymen Klasse new Discount() { … } aus Java?
4. Compare(a, b) gibt eine negative Zahl zurück. Was bedeutet das für List.Sort?
5. Was in Compare sortiert die Schüler nach Note, von der höchsten abwärts?

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