Programmierkurs C-Sharp: Polymorphie
Aus Wikibooks
Polymorphie, zu gut deutsch Vielgestaltigkeit, ist ein in der objektorientierten Programmierung häufig auftretender Begriff. Aber wie wir in den letzten Kapiteln immer wieder lesen konnten, ist der Name meistens viel komplizierter als die Sache, die dahinter steckt. Hier ist das nichts anderes. Eher ist wohl das Gegenteil der Fall: Ein besonders schwieriger und nach komplizierter Arbeit klingender Name verbirgt eine ganz einfache Sache.
Inhaltsverzeichnis |
[Bearbeiten] Voraussetzungen
Damit wir wirklich nachvollziehen können, was Polymorphie ist, brauchen wir allerdings ein paar Grundkenntnisse der objektorientierten Programmierung. Wir sollten daher ohne zu zögern beschreiben können,
- was Klassen sind,
- wie Methoden aufgebaut sind,
- was die Zugriffsmodifizierer zu bedeuten haben und
- wie Vererbung funktioniert.
[Bearbeiten] Problem
Schauen wir uns ein einfaches, aber alltägliches, Problem an:
Teilproblem a: Wir möchten eine Klasse haben, die ein paar Berechnungen durchführt. Dabei soll sie eine beliebig große Menge an Zahlen übernehmen und diese - der Einfachheit halber - addieren. Allerdings können diese Zahlen sowohl Ganzzahlen, als auch Fließkommazahlen sein.
Teilproblem b: Außerdem soll diese Klasse von einer anderen mathematischen Klasse erben, die schon früher geschrieben wurde, und die noch mit dem alten Mehrwertsteuersatz (16%) statt dem aktuellen Satz von 19% rechnet. Diese Korrektur können wir nicht in die alte Klasse schreiben, weil sonst die Berechnungen bis zum 31.12.2006 fehlerhaft wären. Also muss unsere neue mathematische Klasse diese Korrektur durchführen.
Ohne Polymorphie könnten wir uns an dieser Stelle schon warm anziehen, denn wir hätten jede Menge Arbeit vor uns. Stattdessen gehen wir diese Aufgabe ganz entspannt an:
[Bearbeiten] Überladen
Schauen wir uns zunächst das Problem mit der Addition der Zahlen an. Die Anforderung sagt, dass wir unterschiedlich viele Parameter erhalten werden und dass unterschiedliche Datentypen übergeben werden sollen. Natürlich könnten wir jetzt spontan anfangen und Methoden schreiben, die etwa so aussehen:
public int AddiereGanzzahl(int[] i)
{
int ergebnis = 0;
foreach(int zahl in i)
{
ergebnis += zahl;
}
return ergebnis;
}
public float AddiereFliesskommazahl(float[] f)
{
float ergebnis = 0f;
foreach(float zahl in f)
{
ergebnis += zahl;
}
return ergebnis;
}
Besonders sinnvoll sieht das aber nicht aus, oder? Von der Übersichtlichkeit ganz zu schweigen. Aber auch dafür gibt es eine saubere Lösung: das Überladen. Überladen nennt man den Vorgang (oder Zustand), wenn mehrere Methoden mit dem gleichen Namen erstellt werden (oder vorhanden sind). Auf unser Beispiel umgelegt, bedeutet das folgendes:
public int Addiere(int[] i)
{
int ergebnis = 0;
foreach(int zahl in i)
{
ergebnis += zahl;
}
return ergebnis;
}
public float Addiere(float[] f)
{
float ergebnis = 0f;
foreach(float zahl in f)
{
ergebnis += zahl;
}
return ergebnis;
}
Jetzt haben wir zwei verschiedene Methoden, die völlig gleich heißen. Und, wichtiger noch, wir müssen uns nicht mehr um irgendwelche Details kümmern, wenn wir die Methoden später aufrufen:
int[] paramInteger = new int[] { 1, 2, 3, 4, 5, 6 };
float[] paramFloat = new float[] { 1.2f, 3.4f, 5.6f };
// Wir rufen "Addiere" auf
int ergebnisInteger = Addiere ( paramInteger );
// Wir rufen noch einmal "Addiere" auf
float ergebnisFloat = Addiere ( paramFloat );
// Wir geben die Ergebnisse aus:
Console.WriteLine("Das Ergebnis der Ganzzahl-Berechnung lautet: {0}", ergebnisInteger);
Console.WriteLine("Das Ergebnis der Fließkomma-Berechnung lautet: {0}", ergebnisFloat);
... das war schon fast das ganze Geheimnis des Überladens.
[Bearbeiten] Überschreiben
Das Überschreiben (engl. override; wörtlich außer Kraft setzen) ist uns im Zusammenhang mit abstrakten Methoden schon einmal begegnet. Damals haben wir gelernt, dass wir mit dem Schlüsselwort override abstrakte Methoden "außer Kraft setzen" und so unsere eigene Vorstellung von den Aktionen in den Methoden einbauen konnten. Ohne es zu merken, haben wir also schon kräftig an der Polymorphie herumgeschraubt. Das einzig wirklich Neue an dieser Stelle ist deshalb, dass man es jetzt auch physisch merkt.
Gehen wir noch einmal zu unserem Teilproblem (b) zurück: Wir haben eine Klasse, nennen wir sie MwStRechner1, die bereits eine Methode BerechneBrutto eingebaut hat. Diese Klasse können wir nicht mehr ändern. Trotzdem besitzt sie zahlreiche andere Methoden, die wir immer noch benutzen wollen. Ein Verzicht auf diese Klasse, und damit der komplette Neubau, würde viel Zeit kosten. Wie schaffen wir es also nun, das Problem zu lösen? Schauen wir uns zunächst die alte Klasse an:
class MwStRechner1
{
virtual public decimal BerechneBrutto(decimal netto)
{
// Hier finden zahlreiche komplizierte Berechnungen statt...
// Zum Schluss berechnen wir das Brutto mit 16%
return netto * 1.16m;
}
public void AndereNuetzlicheMethode() { }
public void NochEineSinnvolleMethode() { }
}
Weise und vorausschauend, wie die Programmierer waren, haben sie die wichtige Methode BerechneBrutto damals als virtuelle Methode mit virtual deklariert. Aus dem Kapitel Zugriffsmodifizierer wissen wir ja noch, dass dieses virtual dafür steht, dass man die Methode in abgeleiteten Subklassen überschreiben darf. Diesen Umstand machen wir uns jetzt zunutze und entwickeln unsere Subklasse:
class MwStRechner2 : MwStRechner1
{
override public decimal BerechneBrutto(decimal netto)
{
// Wir berechnen das Brutto.
//
// Weil aber die alte Methode noch andere Berechnungen durchführt
// auf die wir nicht verzichten können,
// lassen wir uns zunächst das Brutto-Ergebnis von ihr geben.
decimal zwischenErgebnis = base.BerechneBrutto(netto);
// Dieses Mal müssen wir 19% kalkulieren.
// Dazu ziehen wir zuerst die alten 16% ab,
// und schlagen auf das - dann wieder Netto -
// die neue Steuer auf:
return ( zwischenErgebnis / 1.16m ) * 1.19m;
}
}
Das war der ganze Job. Unspektakulär, nicht wahr? Bleibt die Frage, ob es auch wirklich funktioniert:
// Wir holen uns ein Objekt der neuen MwSt-Klasse zum Testen: MwStRechner2 rechner19 = new MwStRechner2(); // Test: decimal brutto19 = rechner19.BerechneBrutto( 1000 ); Console.WriteLine ( brutto19 );
Okay, das hat schon mal geklappt. Aber was ist aus der alten Klasse geworden? Gemäß der Anforderungen durfte sie nicht geändert werden. Eigentlich wissen wir ja auch, dass sie nicht geändert wurde, aber es heißt schließlich nicht umsonst Probieren geht über studieren!:
// Wir holen uns je ein Objekt // + der alten MwSt-Klasse und // + der neuen MwSt-Klasse zum Testen: MwStRechner1 rechner16 = new MwStRechner1(); MwStRechner2 rechner19 = new MwStRechner2(); // Test: decimal brutto16 = rechner16.BerechneBrutto( 1000 ); decimal brutto19 = rechner19.BerechneBrutto( 1000 ); Console.WriteLine ( brutto16 ); Console.WriteLine ( brutto19 );
Funktioniert offensichtlich tadellos. Damit haben wir alle Anforderungen der Problemstellung erfüllt.
[Bearbeiten] Ein paar Regeln
In der Tat ist das Überschreiben nicht wirklich kompliziert. Und damit es so einfach werden konnte, müssen wir ein paar Regeln in Kauf nehmen, die uns darin einschränken, nun loszuziehen und alles zu überschreiben, was uns über den Weg läuft:
- Methoden, die überschrieben werden sollen, müssen den Modifizierer abstract, virtual oder override (was im Grunde nur bedeutet, dass in irgendeiner Basisklasse schon mal abstract oder virtual auftauchte) besitzen. Alles andere lässt sich nicht überschreiben.
- Aus der ersten Regel folgt logischerweise, dass wir keine statischen Methoden (Klassenmethoden) überschreiben können.
- Die neue Methode muss exakt die gleiche Signatur wie die alte haben.
Weder der Rückgabe-Datentyp, noch die Parameter - und schon gar nicht der Name der Methode - dürfen geändert werden. Allerdings wäre das auch Unfug: Wie kann man eine Methode "überschreiben", wenn man doch eigentlich eine ganz andere Methode schreibt?!
- Als private deklarierte Methoden lassen sich nicht überschreiben.
Wozu auch? Private Methoden gehören ausschließlich der Klasse selbst. Niemand, nicht einmal die Kinder, hat darauf Zugriff. Wie könnte man also etwas überschreiben, auf das man gar nicht zugreifen darf?!
Soweit zu den Regeln. Eigentlich alle logisch nachvollziehbar, nicht wahr?
[Bearbeiten] Ausblenden
Nehmen wir einmal an, die Programmierer wären damals nicht so vorausschauend gewesen, die Methode BerechneBrutto mit dem Modifizierer virtual zur Anpassung an den neuen Steuersatz zu markieren. Müssten wir dann unsere ganze Arbeit wegwerfen? ... Nein. Müssten wir nicht. Für diesen Fall gibt es den Modifizierer new, mit dem wir nahezu jede Methode "überschreiben" können. Allerdings überschreiben wir sie nicht wirklich. Vielmehr setzen wir einen "neuen Vererbungspunkt" in der Hierarchie der weiteren Erbschaftslinie der Subklassen. Wir blenden sie aus. Aber der Reihe nach...
Zunächst einmal schauen wir uns an, wie es funktioniert. Dazu nehmen wir uns wieder unsere alte Klasse her:
class MwStRechner1
{
public decimal BerechneBrutto(decimal netto)
{
// Hier finden zahlreiche komplizierte Berechnungen statt...
// Zum Schluss berechnen wir das Brutto mit 16%
return netto * 1.16m;
}
public void AndereNuetzlicheMethode() { }
public void NochEineSinnvolleMethode() { }
}
Listigerweise steht hier kein virtual. Andere würden jetzt wohl aufgeben. Aber wir nicht. Stattdessen passen wir uns an und schreiben unsere neue Klasse eben so:
class MwStRechner2 : MwStRechner1
{
new public decimal BerechneBrutto(decimal netto)
{
// Wir berechnen das Brutto.
//
// Weil aber die alte Methode noch andere Berechnungen durchführt
// auf die wir nicht verzichten können,
// lassen wir uns zunächst das Brutto-Ergebnis von ihr geben.
decimal zwischenErgebnis = base.BerechneBrutto(netto);
// Dieses Mal müssen wir 19% kalkulieren.
// Dazu ziehen wir zuerst die alten 16% ab,
// und schlagen auf das - dann wieder Netto -
// die neue Steuer auf:
return ( zwischenErgebnis / 1.16m ) * 1.19m;
}
}
Wenn wir das Resultat unserer Bemühungen nun testen, müsste eigentlich alles funktionieren:
// Wir holen uns je ein Objekt // + der alten MwSt-Klasse und // + der neuen MwSt-Klasse zum Testen: MwStRechner1 rechner16 = new MwStRechner1(); MwStRechner2 rechner19 = new MwStRechner2(); // Test: decimal brutto16 = rechner16.BerecheBrutto( 1000 ); decimal brutto19 = rechner19.BerecheBrutto( 1000 ); Console.WriteLine ( brutto16 ); Console.WriteLine ( brutto19 );

