Generische Programmierung
Generische Programmierung (im Gegensatz zu generative Programmierung) ist ein Programmierparadigma, bei der einzelne Funktionen und Klassen immer möglichst allgemein geschrieben werden, so dass sie für unterschiedliche Datentypen verwendet werden können. Das wird von einigen Programmiersprachen durch das Konzept von generischen Typen bzw. Templates unterstützt - so sind dynamische Programmiersprachen, bei denen der Typ nicht an die Variable gebunden ist, sind durch ihre verallgemeinerte Polymorphie immer generisch. Von Sprachen, die solche Mechanismen bieten, sagt man auch, dass sie Generik erlauben.
Wesentlich bei der generischen Programmierung ist, dass die Algorithmen nicht für einen bestimmten Datentyp geschrieben werden, sondern nur bestimmte Anforderungen an die Typen stellen. Das Prinzip wird auch parametrische Polymorphie genannt.
Paradebeispiel ist die C++-Standardbibliothek der Programmiersprache C++, bei der die Algorithmen so weit wie möglich von den Datenstrukturen, mit denen sie arbeiten, getrennt werden.
Vergleich verschiedener Techniken
Ein gutes Beispiel liefert die Maximumsfunktion
- ,
die für zwei gegebene Werte , desselben Typs den größeren Wert zurückgibt.
Voraussetzung ist, dass und miteinander vergleichbar sind, der Ausdruck also definiert ist und eine Ordnung beschreibt.
Neuschreiben der Algorithmen für jeden Datentyp
In klassischen Programmiersprachen wie C (Programmiersprache würde man hier für jeden (nativen) Datentyp eine Funktion programmieren, die jeweils das gleiche Ergebnis im entsprechenden Typ erzeugt.
Die folgenden Funktionen finden von den beiden Zahlen a und b jeweils die Größere raus, allerdings einmal als Integer und einmal als Float:
int maxint(int a, int b)
{
if (a < b)
return b;
else
return a;
}
float maxfloat(float a, float b)
{
if (a < b)
return b;
else
return a;
}
Hierbei fällt auf, dass der Code an sich immer derselbe ist, nur die Typen unterscheiden sich.
Wiederholungen vermeiden mit Hilfe von Makros
Die Wiederholung lässt sich zwar mit Makros umgehen – so kann man in C die selbe Maximumsfunktion über ein Makro definieren:
#define MAX(a, b) ((a)<(b) ? (b) : (a))
Anstatt jedoch Funktionen durch Makros zu ersetzen, kann man auch mit Makros Funktionen zur Compilerzeit generieren:
#define MAXIMUM(type) \ type max ## type(type a, type b) { \ if (a < b) \ return b; \ else \ return a; \ } MAXIMUM(float) /* Definiert die Funktion maxfloat() */ MAXIMUM(int) /* Definiert die Funktion maxint() */
Diese Variante ist weitestgehend typensicher und die Ausdrücke werden auch nicht mehrmals ausgewertet, so dass man sich Tipparbeit spart, was bei längeren Routinen durchaus sinnvoll sein kann und vor allem die Wartbarkeit des Codes erhöht. Allerdings muss man manuell die entsprechende Variante auswählen (z. B. maxint um Konstanten oder Variablen des Typs int zu vergleichen).
Verwendung objektorientierter Mittel
Auch der objektorientierte Ansatz funktioniert im Kontext statischer Typisierung nicht zufriedenstellend. Zwar lässt sich hier mit Interfaces oder Mehrfachableitung Klassen um "andere" Datentypen erweitern, sodass sich mittels Polymorphie das Verhalten der generischen Programmierung zum Teil nachbauen lässt, allerdings wird die vollständige Unabhängigkeit von unterschiedlichen Typen (bzw. Klassen) damit nicht umgesetzt.
Definiert man (beispielsweise in C++) eine Klasse Vergleichbares
und leitet davon zum Beispiel für eine physikalische Simulation die Klassen Laenge
und Masse
ab (sagt also, dass sowohl Längen als auch Massen etwas vergleichbares sind), so kann man zwar schreiben:
Vergleichbares& max(Vergleichbares& a, Vergleichbares& b)
{
if (a < b)
return b;
else
return a;
}
aber es gibt immer noch zwei Probleme:
- Zunächst, dass der Ergebnistyp nur „Vergleichbares“ ist; man muss dem Compiler also beim Aufruf explizit sagen, dass das Maximum zweier Längen wieder eine Länge ist (sofern man dessen Längeneigenschaften benutzen will, was wahrscheinlich ist), vor allem aber,
- dass diese Funktion es erlaubt, das Maximum einer Länge und einer Masse zu „bestimmen“, obwohl diese Operation keinen physikalischen Sinn hat.
Templates
Generische Programmierung, beispielsweise in C++ über Templates realisiert, verbindet nun die Flexibilität des Makros mit der Typsicherheit und den anderen Eigenschaften der Funktion. Die generische Implementierung von max in C++ ist
template<typename T>
T max(T a, T b)
{
if (a < b)
return b;
else
return a;
}
Die so definierte Funktion max() kann nun für alle Typen mit Vergleichsoperator verwendet werden.
Ein weiterer Vorteil ist, dass man nicht explizit bei der Definition sagen muss, dass ein Typ vergleichbar ist (zum Beispiel durch Ableiten von einer entsprechenden Klasse), sondern man muss nur sicherstellen, dass er die nötigen Eigenschaften hat (in diesem Fall einen operator< mit korrekter Semantik). Auf diese Weise können auch Typen mit der Funktion verwendet werden, die in Unkenntnis der Funktion entworfen wurden (beispielsweise die eingebauten Typen der Programmiersprache).
Ein Algorithmus sollte dabei stets möglichst wenig vom Typ fordern. So arbeiten die Algorithmen der STL nicht direkt auf den Containern, sondern mit Iteratoren. Auf diese Weise werden sie weitgehend unabhängig von den genauen Eigenschaften des speziellen Containers und können teilweise sogar direkt auf Ein- und Ausgabeströmen arbeiten.
Generische Programmierung in verschiedenen Programmiersprachen
- Wie bereits erwähnt, wird die generische Programmierung in C++ durch Templates unterstützt.
- In Ada gibt es generische Typen schon sehr lange. Es gab sie schon lange vor der Einführung von Templates in C++.
- In Java kommen generische Typen seit der Version 1.5 vor.
- In den .NET-Sprachen C# und VB .NET gibt (ab .NET 2.0) es die generischen Typen (generics).
- In der Programmiersprache Eiffel existieren generische Klassen.
- Die funktionale Programmiersprache ML (und Abkömmlinge wie OCaml) erlaubt generische Programmierung. Typ-Polymorphie ist dort konzeptuelle Grundlage. Es lassen sich sogar Module generieren. Generische Module („parametric modules“) werden hier als Functor bezeichnet. Man kann Funktoren Module als Parameter übergeben und erhält ein neues Modul als Ergebnis.
- Schließlich kann man auch die Makros in C zur generischen Programmierung benutzen, wenngleich sie ursprünglich nicht zu diesem Zweck eingeführt wurden.
Ein generisches Modell ist an spezifische Gegebenheiten einer konkreten Situation anpassbar; z. B. ein generisches Vorgehensmodell wie Wasserfall- oder Spiralmodell an ein konkretes Projekt. Dieser Vorgang wird im Bereich des Software-Engineering auch Tailoring genannt.
Ändert sich das generische Modell, kann man dessen Ausprägungen leicht anpassen, wenn der Weg vom Modell zur Ausprägung detailliert beschrieben ist und man nur die geänderten Elemente verfolgen muss. Bei nicht-generischen Modellen spricht man in diesem Zusammenhang auch von Overfitting.