Wikipedia · einfach zusammengefasst · Stand
Generische Programmierung
Generische Programmierung ist ein Verfahren zur Entwicklung wiederverwendbarer Software-Bibliotheken. Dabei werden Funktionen möglichst allgemein entworfen, …
Inhalt6 Abschnitte
Grundidee und Bedeutung
Generische Programmierung ist ein Verfahren zur Entwicklung wiederverwendbarer Software-Bibliotheken. Funktionen und Algorithmen werden möglichst allgemein formuliert, damit sie mit unterschiedlichen Datentypen und Datenstrukturen arbeiten können. Eine Programmiersprache, die entsprechende Mechanismen bereitstellt, erlaubt Generik.
Entscheidend ist, dass ein Algorithmus nicht für einen bestimmten Datentyp geschrieben wird. Stattdessen legt er nur fest, welche Eigenschaften ein eingesetzter Typ besitzen muss. Dieses Prinzip heißt parametrische Polymorphie. Je nach Programmiersprache wird es durch generische Typen, Templates oder die verallgemeinerte Polymorphie dynamischer Sprachen umgesetzt. Bei dynamischen Sprachen kann sich der Typ einer Variablen zur Laufzeit ändern.
Ein wichtiges Beispiel ist die C++-Standardbibliothek: Ihre Algorithmen sind so weit wie möglich von den Datenstrukturen getrennt, auf denen sie arbeiten.
Beispiel einer generischen Maximum-Funktion
Eine typische generische Funktion ist
max: T × T → T.
Sie erhält zwei Werte a und b desselben Typs T und gibt den größeren Wert max{a,b} zurück. Der konkrete Typ ist nicht vorgegeben. Er muss jedoch die benötigte Anforderung erfüllen: a und b müssen miteinander vergleichbar sein. Insbesondere muss der Ausdruck a ≤ b definiert sein und eine Ordnung beschreiben.
Das Beispiel zeigt das Grundprinzip: Der Algorithmus benötigt nicht alle Eigenschaften eines Typs, sondern nur die für seine Aufgabe erforderliche Vergleichsoperation.
Umsetzung in C++
Ein Versuch, Generik allein mit objektorientierten Mitteln und statischer Typisierung nachzubilden, ist unzureichend. Zwar können Interfaces oder Mehrfachableitung gemeinsame Eigenschaften verschiedener Klassen ausdrücken, doch die vollständige Unabhängigkeit von den konkreten Typen wird damit nicht erreicht.
Werden etwa die Klassen Laenge und Masse von einer gemeinsamen Klasse Vergleichbares abgeleitet, kann eine Maximum-Funktion Werte vom Typ Vergleichbares verarbeiten. Dabei entstehen zwei Probleme: Ihr Rückgabetyp ist lediglich Vergleichbares, sodass beim Aufruf kenntlich gemacht werden muss, dass das Maximum zweier Längen wieder eine Länge ist, wenn deren besondere Eigenschaften verwendet werden sollen. Außerdem könnte dieselbe Funktion eine Länge mit einer Masse vergleichen, obwohl dies physikalisch keinen Sinn hat.
C++ löst dies mit Templates. Die Funktion kann mit einem Typparameter T definiert werden und nimmt zwei Werte dieses Typs entgegen. Sie gibt abhängig vom Ausdruck a < b entweder b oder a zurück. Dadurch verbindet sie Flexibilität mit Typsicherheit und kann für alle Typen eingesetzt werden, die einen passenden Vergleichsoperator besitzen.
Ein Typ muss dazu nicht ausdrücklich von einer Klasse für vergleichbare Objekte abgeleitet sein. Es genügt, dass er die verlangte Operation bereitstellt, hier einen operator < mit korrekter Semantik. Deshalb lassen sich auch eingebaute Typen und Typen verwenden, die ohne Kenntnis der Maximum-Funktion entwickelt wurden.
Generische Algorithmen sollten möglichst wenige Anforderungen an einen Typ stellen. Die Algorithmen der STL arbeiten daher nicht unmittelbar auf Containern, sondern mit Iteratoren. Iteratoren ermöglichen den schrittweisen Zugriff auf Elemente. So bleiben die Algorithmen weitgehend unabhängig vom konkreten Container und können teilweise sogar direkt mit Ein- und Ausgabeströmen arbeiten.
Generische Klassen und Methoden in C#
Die generische Programmierung in C# ähnelt den Templates von C++. Unter anderem verwenden HashSet, List und Dictionary generische Typen. Die Typparameter einer generischen Klasse stehen in spitzen Klammern.
Eine Klasse Test<T, U> kann beispielsweise zwei Objekte der zunächst nicht festgelegten Typen T und U speichern. Beim Erzeugen eines konkreten Objekts werden die Typen eingesetzt, etwa Test<string, DateTime>. Die Klasse kann dann einen Text und einen Zeitpunkt aufnehmen und beide Werte ausgeben.
Als generische Methode bezeichnet der Artikel eine Methode, die mit Typparametern für ihren Rückgabetyp oder ihren Rückgabeparameter deklariert wird. Das gezeigte Beispiel verwendet eine Klasse Test<T> mit einer internen List<T>. Die Methode Insert(int index, T item) fügt am angegebenen Index ein Element des Typs T ein. Die Methode T Get(int index) gibt das Element an diesem Index zurück. Liegt der Index nicht zwischen 0 einschließlich und list.Count ausschließlich, liefert sie default(T), also den Standardwert des jeweiligen Typs. Wird Test<string> verwendet, verwalten diese Methoden entsprechend Zeichenketten.
Unterstützung in Programmiersprachen
Verschiedene Programmiersprachen unterstützen generische Programmierung auf unterschiedliche Weise:
- C++ verwendet Templates.
- Ada besaß generische Typen bereits vor der Einführung der Templates in C++. C++-Templates wurden von Ada beeinflusst, unterscheiden sich aber in vielen Punkten.
- ABAP bietet generische Datentypen.
- Java unterstützt generische Typen ab Version 1.5.
- TypeScript sowie die .NET-Sprachen C# und VB .NET bieten generische Typen; bei den .NET-Sprachen gilt dies ab .NET 2.0.
- Eiffel besitzt generische Klassen.
- In ML, dessen Abkömmlingen wie OCaml und in Haskell bildet Typ-Polymorphie eine konzeptuelle Grundlage. Dort können auch Module generiert werden. Solche generischen beziehungsweise parametrischen Module heißen Funktoren: Sie erhalten Module als Parameter und liefern ein neues Modul als Ergebnis.
- In dynamisch typisierten Sprachen wie Python, die außerdem Operatoren unterstützen, kann praktisch jede Methode im Sinne der generischen Programmierung eingesetzt werden.
- In C können Makros für generische Programmierung genutzt werden, obwohl sie ursprünglich nicht dafür eingeführt wurden.
Generische Modelle in der Softwaretechnik
Der Begriff „generisch“ wird in der Softwaretechnik auch auf Modelle angewendet. Ein generisches Modell lässt sich an die besonderen Bedingungen einer konkreten Situation anpassen. Beispielsweise kann ein generisches Vorgehensmodell wie das Wasserfall- oder Spiralmodell auf ein bestimmtes Projekt zugeschnitten werden. Dieser Anpassungsvorgang heißt im Software-Engineering Tailoring.
Wenn sich das generische Ausgangsmodell ändert, können seine konkreten Ausprägungen leicht angepasst werden, sofern der Weg vom Modell zur jeweiligen Ausprägung detailliert beschrieben ist. Dann müssen nur die geänderten Elemente verfolgt werden. Bei nicht-generischen Modellen spricht man in diesem Zusammenhang auch von Überanpassung.