Klassen und Objekte
Verfügbar — implementiert in Phase B.1
Eine Klasse ist eine Schablone für Objekte. Stell dir eine Klasse vor wie einen Bauplan für ein Haus — viele Häuser können nach diesem Bauplan gebaut werden, jedes Haus ist seine eigene Instanz.
13.1 Eine einfache Klasse definieren
klasse Person {
funktion neu(name, alter) {
dies.name = name
dies.alter = alter
}
funktion grüße() {
sag "Hallo, ich bin " + dies.name
}
}
Erklärung Stück für Stück:
klasse Person { ... }— definiert eine Klasse namensPersonfunktion neu(...)— der Konstruktor, läuft beim Erstellen einer Instanzdies— verweist auf das aktuelle Objekt (in anderen Sprachen:this/self)dies.name = name— speichert ein Feld (Eigenschaft) am Objektfunktion grüße()— eine Methode (Funktion, die zur Klasse gehört)
13.2 Instanzen erstellen mit neu
nimm sascha = neu Person("Sascha", 38)
nimm anna = neu Person("Anna", 30)
sascha.grüße() // Hallo, ich bin Sascha
anna.grüße() // Hallo, ich bin Anna
neu Klasse(...) erstellt eine neue Instanz und ruft den Konstruktor auf.
Wichtig: Jede Instanz hat ihre eigenen Felder. sascha.name und anna.name sind verschiedene Werte.
13.3 Felder lesen und schreiben
nimm p = neu Person("Sascha", 38)
sag p.name // Sascha — Feld lesen
sag p.alter // 38
p.alter = 39 // Feld direkt überschreiben
sag p.alter // 39
13.4 Methoden mit Rückgabewerten
klasse Rechner {
funktion neu(start) {
dies.wert = start
}
funktion plus(n) {
gib dies.wert + n
}
funktion mal(n) {
gib dies.wert * n
}
}
nimm r = neu Rechner(10)
sag r.plus(5) // 15
sag r.mal(3) // 30
13.5 Methoden, die andere Methoden aufrufen
dies.methode() ruft eine andere Methode des Objekts auf:
klasse Zähler {
funktion neu() {
dies.wert = 0
}
funktion erhöhe() {
dies.wert = dies.wert + 1
}
funktion erhöhe_um(n) {
für i = 1 bis n {
dies.erhöhe() // ← Methoden-Aufruf auf sich selbst
}
}
}
nimm z = neu Zähler()
z.erhöhe_um(5)
sag z.wert // 5
13.6 Vererbung mit erbt
Eine Klasse kann eine andere als Eltern-Klasse haben — sie erbt dann alle Methoden.
klasse Tier {
funktion neu(name) {
dies.name = name
}
funktion vorstellen() {
sag "Ich bin " + dies.name
}
}
klasse Hund erbt Tier {
funktion bellen() {
sag dies.name + " bellt: Wuff!"
}
}
nimm rex = neu Hund("Rex")
rex.vorstellen() // Ich bin Rex ← geerbt von Tier
rex.bellen() // Rex bellt: Wuff!
Was passiert:
Hunddefiniert nurbellen()selbstvorstellen()und der Konstruktorneu()kommen automatisch vonTier
13.7 Methoden überschreiben
Ein Kind kann Methoden des Eltern-Typs überschreiben — die eigene Version gewinnt:
klasse Tier {
funktion sage() {
sag "Tiergeräusch"
}
}
klasse Katze erbt Tier {
funktion sage() {
sag "Miau" // ← überschreibt die geerbte Version
}
}
nimm k = neu Katze()
k.sage() // Miau
13.8 Konstruktor in Kindklasse
Wenn die Kindklasse eigene Felder hat, definiert sie einen eigenen Konstruktor:
klasse Tier {
funktion neu(name) {
dies.name = name
}
}
klasse Hund erbt Tier {
funktion neu(name, rasse) {
dies.name = name
dies.rasse = rasse
}
}
nimm rex = neu Hund("Rex", "Schäferhund")
sag rex.name // Rex
sag rex.rasse // Schäferhund
13.9 Klassen in Interpolation
Felder lassen sich direkt in Interpolations-Strings nutzen:
klasse Person {
funktion neu(name, alter) {
dies.name = name
dies.alter = alter
}
funktion beschreibung() {
gib "{dies.name} ist {dies.alter}"
}
}
nimm p = neu Person("Anna", 25)
sag p.beschreibung() // Anna ist 25
13.10 Was passiert bei nicht existierenden Feldern?
klasse Leer {
funktion neu() { }
}
nimm x = neu Leer()
sag x.gibtsNicht // nichts
Zugriff auf ein nicht existierendes Feld liefert nichts — kein Fehler. Genau wie bei Karten.
13.11 Zwei Instanzen vergleichen
nimm a = neu Person("Sascha", 38)
nimm b = neu Person("Sascha", 38)
nimm c = a
sag a == b // falsch — verschiedene Objekte (Identitäts-Vergleich)
sag a == c // wahr — gleiches Objekt
Wichtig: Selbst wenn zwei Instanzen die gleichen Werte haben, sind sie standardmäßig verschiedene Objekte (Identitäts-Vergleich). Du kannst das aber ändern: mit der Magic-Method gleicht wird == zum Wert-Vergleich — siehe Kapitel 32.
Weiter geht's in Kapitel 32: Dort lernst du zwei mächtige Erweiterungen für Klassen:
super— im Konstruktor und in Methoden die Eltern-Klasse aufrufen (super.neu(...),super.methode()), statt geerbte Felder mühsam von Hand neu zu setzen wie in §13.8.- Magic-Methods —
als_text(eigene Anzeige beisag/Interpolation),gleicht(==),vergleicht_mit(</>),länge.
13.12 Tipps für die Praxis
Tu:
- Klassen-Namen mit GroßSchreibung beginnen (
Person,BankKonto) - Kurze, eindeutige Klassen-Namen
- Konstruktor (
neu) sollte alle Pflichtfelder setzen - Eine Klasse sollte eine Sache gut machen
Vermeide:
- Riesige Klassen mit 50 Feldern und 30 Methoden
- Tiefe Vererbungs-Hierarchien (mehr als 2-3 Ebenen)
- Felder ohne Zweck — wenn du's nicht nutzt, weg damit
13.13 Listen von Objekten
Bisher hielten unsere Listen (Kapitel 9) Zahlen oder Text. Eine Liste darf aber genauso Objekte — also Instanzen deiner Klassen — enthalten. So verwaltest du viele gleichartige Dinge auf einmal: alle Produkte, alle Spieler, alle Ebenen eines Bildes. Auf jedes Element greifst du mit dem gewohnten Punkt zu — Felder und Methoden:
klasse Produkt {
funktion neu(name, preis) {
dies.name = name
dies.preis = preis
}
funktion teuer() { gib dies.preis > 15 }
}
nimm produkte = [neu Produkt("Apfel", 10), neu Produkt("Birne", 20)]
sag produkte[0].name // Apfel — Feld des ersten Elements
sag produkte[1].preis // 20
sag produkte[1].teuer() // wahr — Methode des zweiten Elements
Mit wiederhole (Kapitel 7) gehst du bequem über alle Objekte:
wiederhole p in produkte {
sag "{p.name}: {p.preis}"
}
// Apfel: 10
// Birne: 20
Das funktioniert auch aus Funktionen heraus — du kannst eine Objekt-Liste übergeben und darin per Index auf Felder und Methoden zugreifen:
funktion gesamtpreis(waren) {
nimm summe = 0
für i = 0 bis länge(waren) - 1 {
summe = summe + waren[i].preis
}
gib summe
}
sag gesamtpreis(produkte) // 30
Und die Listen-Helfer aus Kapitel 17 (gefiltert, sortiere, abgebildet, gesammelt …) arbeiten genauso über Objekte — du filterst und sortierst nach Feldern:
nutze liste
nimm teure = liste.gefiltert(produkte, funktion(p) { gib p.preis > 15 })
sag länge(teure) // 1 — nur die Birne
nimm namen = liste.abgebildet(produkte, funktion(p) { gib p.name })
wiederhole n in namen { sag n }
// Apfel
// Birne
Merke: Eine Liste ist immer homogen — alle Elemente müssen dieselbe Klasse haben (nicht Produkt und Kunde gemischt). Und Listen bleiben flach: eine Liste von Objekten ja, aber keine Liste von Listen. Beides passt gut zum Klassen-Denken — brauchst du eine Struktur in der Struktur, gib dem Objekt einfach ein Feld dafür.
Objekte, die selbst Objekt-Listen halten
Genau das ist der übliche Weg zu verschachtelten Strukturen: Statt einer Liste von Listen gibt eine Klasse einer ihrer Felder eine Objekt-Liste. So hält ein Bild eine Liste von Ebene-Objekten, und du sprichst ein Element über zwei Stufen an — auch aus Funktionen heraus:
nutze liste
klasse Ebene {
funktion neu(größe) { dies.pixel = liste.gefüllt(größe, 0) }
funktion setze(i, wert) {
nimm p = dies.pixel
p[i] = wert
}
funktion hole(i) { gib dies.pixel[i] }
}
klasse Bild {
funktion neu(größe) {
dies.ebenen = [neu Ebene(größe), neu Ebene(größe)]
}
}
nimm bilder = [neu Bild(4)]
funktion male(bi, ei, i, wert) { bilder[bi].ebenen[ei].setze(i, wert) }
funktion lies(bi, ei, i) { gib bilder[bi].ebenen[ei].hole(i) }
male(0, 1, 2, 7)
sag lies(0, 1, 2) // 7
bilder[bi].ebenen[ei].setze(…) liest sich von links nach rechts: das Bild an Stelle bi, darin die Ebene ei, darauf die Methode. Feld lesen/schreiben (bilder[0].ebenen[0].pixel), das Element in einer eigenen Variablen festhalten (nimm eb = bilder[0].ebenen[1]) und die Reihenfolge tauschen gehen genauso.
Dynamisch wachsen darf so eine Objekt-Liste auch — mit liste.hänge_an/liste.entferne fügst du zur Laufzeit neue Objekte hinzu oder entfernst welche (die Liste bleibt homogen, also nur dieselbe Klasse):
funktion neues_bild(größe) {
liste.hänge_an(bilder, neu Bild(größe))
}
sag länge(bilder) // 1
neues_bild(4)
sag länge(bilder) // 2
Tipp für Konstruktoren, die andere Objekte bauen: die Größen-Parameter (
größe) bekommen ihren Typ am einfachsten aus der Verwendung — z. B.dies.pixel = liste.gefüllt(größe, 0)sagt Kiste, dassgrößeeine Ganzzahl ist. Text- oder Zustandsfelder, die nicht aus der Größe folgen, belegst du im Konstruktor mit einem festen Wert vor (dies.name = "") und setzt sie nach dem Erzeugen über das Feld (bild.ebenen[0].name = "Hintergrund").