Polymorphie (Programmierung)

aus Wikipedia, der freien Enzyklopädie
(Weitergeleitet von Späte Bindung)
Zur Navigation springen Zur Suche springen

Polymorphie oder Polymorphismus (griechisch für Vielgestaltigkeit) ist ein Konzept in der objektorientierten Programmierung, das ermöglicht, dass ein Bezeichner abhängig von seiner Verwendung Objekte unterschiedlichen Datentyps annimmt. In älteren typisierten Programmiersprachen wird dagegen jedem Namen und jedem Wert im Quelltext eines Programms höchstens ein Typ zugeordnet. Dies bezeichnet man als Monomorphie.

Arten der Polymorphie

[Bearbeiten | Quelltext bearbeiten]

Polymorphie überladener Operatoren

[Bearbeiten | Quelltext bearbeiten]

Ein Bezeichner, der für einen Operator steht (bspw. „+“, „−“), kann mehrmals mit anderer Bedeutung implementiert werden. Für jeden Kontext, in dem der Operator neu deklariert wurde, muss die Implementierung immer eindeutig sein.

Polymorphie der objektorientierten Programmierung

[Bearbeiten | Quelltext bearbeiten]

Die Polymorphie der objektorientierten Programmierung ist eine Eigenschaft, die immer im Zusammenhang mit Vererbung und Schnittstellen (Interfaces) auftritt. Eine Methode ist polymorph, wenn sie in verschiedenen Klassen die gleiche Signatur hat, jedoch erneut implementiert ist.

Gibt es in einem Vererbungszweig einer Klassenhierarchie mehrere Methoden auf unterschiedlicher Hierarchieebene, jedoch mit gleicher Signatur, wird erst zur Laufzeit bestimmt, welche der Methoden für ein gegebenes Objekt verwendet wird (Dynamisches Binden). Bei einer mehrstufigen Vererbung wird jene Methode verwendet, die direkt in der Objektklasse (d. h. jene Klasse, von der das Objekt ein Exemplar ist) definiert ist, oder jene, die im Vererbungszweig am weitesten „unten“ liegt (d. h. die Methode, die von der Vererbung her am nächsten ist).

Moderne Konzepte kennen jedoch auch Polymorphie über Klassengrenzen hinaus. So erlaubt die Programmiersprache Objective-C die Polymorphie zwischen zwei gleichnamigen Methoden, die in verschiedenen Klassen erstmals definiert sind.

// NSObject kennt nicht -doSomething
@interface KlasseA : NSObject {
    
}
- (void) doSomething;
@end

@interface KlasseB : NSObject {
    
}
- (void) doSomething;
@end
// irgendwo
id object =  // Ein Objekt einer beliebigen Klasse
[object doSomething]; // polymorph zwischen KlasseA und KlasseB

Die Subklasse-vor-Basisklasse-Regel gilt auch hier: Wenn die KlasseB zur KlasseC abgeleitet wird, würde die entsprechende Methode der KlasseC ausgeführt.

Polymorphie einer Funktion bzw. Prozedur

[Bearbeiten | Quelltext bearbeiten]

Ist der Rückgabewert oder ein Argument einer Funktion polymorph, so heißt die Funktion polymorphe Funktion. Mit Hilfe polymorpher Funktionen kann die Generizität von Datenstrukturen auch in Algorithmen angewandt werden.[1]

Polymorphie von Datentypen oder Klassen

[Bearbeiten | Quelltext bearbeiten]

Wird für eigene Datentypen bzw. Klassen bei der Instanziierung bzw. beim Konstruktoraufruf ein Parameter für den tatsächlich verwendeten Datentyp übergeben, spricht man von parametrischer Polymorphie, welche semantisch mit Generizität übereinstimmt.

Weitere Unterteilungen

[Bearbeiten | Quelltext bearbeiten]

Folgende weitere Unterteilung ist möglich:

  • universelle Polymorphie
    • parametrische Polymorphie
    • Inklusions-/Vererbungspolymorphie
  • Ad-hoc-Polymorphie
    • Coercion
    • Überladung

Manchmal wird Ad-hoc-Polymorphie gleichgesetzt mit Überladen. Das ist auf Christopher Strachey zurückzuführen,[2] der als Erster Polymorphie in parametrische und Ad-hoc-Polymorphie unterteilte.

Luca Cardelli und Peter Wegner erweiterten Stracheys Konzept um die Inklusionspolymorphie,[3] um Subtypen und Vererbung zu modellieren. Die obige Auflistung spiegelt also Stracheys Einteilung wider, erweitert um die Inklusionspolymorphie von Cardelli und Wegner.

Universelle und Ad-hoc-Polymorphie

[Bearbeiten | Quelltext bearbeiten]

Universelle Polymorphie unterscheidet sich von Ad-hoc-Polymorphie in mehreren Aspekten. Bei Ad-hoc-Polymorphie kann der einem Namen zugeordnete Wert nur endlich vielen verschiedene Typen angehören. Diese sind zudem während der Kompilierung bekannt. Universelle Polymorphie dagegen erlaubt es, jedweden bekannten Typ zuzuordnen; auch solche, die vielleicht erst später definiert werden.

Ein weiterer Unterschied liegt darin, dass die Implementierung einer universell-polymorphen Funktion generell den gleichen Code unabhängig von den Typen ihrer Argumente ausführt, während ad-hoc-polymorphe (also überladene) Funktionen abhängig von den Typen ihrer Argumente völlig unterschiedlich implementiert sein können.

Überladen und Coercion

[Bearbeiten | Quelltext bearbeiten]

Funktionen sind überladen, wenn unterschiedliches Verhalten mit demselben Namen verbunden ist. Dies gilt analog für Bezeichner von Operatoren: Beispielsweise ist der Operator + in vielen Programmiersprachen von vornherein überladen. So können mit ihm sowohl ganze Zahlen als auch Gleitkommazahlen addiert werden. Oft wird er auch zur Stringkonkatenierung verwendet:

 42 + 3              (1)
 3.14 + 1.0          (2)
 "Hallo" + " Welt!"  (3)

Einige Programmiersprachen unterscheiden dabei, welche Namen überladen werden dürfen und welche nicht. In Java ist Methodenüberladung erlaubt, Operatorüberladung außer den schon eingebauten Überladungen wie der des +-Operators aber nicht. Auch in C# sind nicht alle Operatoren überladbar. C++ und manche andere Sprachen erlauben generell beides.

Coercion ist eine Art implizite Typumwandlung, sozusagen das Anwenden einer unsichtbaren, automatisch eingefügten Funktion, um zum Beispiel Argumente eines Unterprogramms oder einer Funktion in die erwarteten Typen umzuwandeln. Coercion ist mit dem Überladen eng verknüpft, und die Unterschiede sind für den Programmierer nicht unbedingt gleich ersichtlich.

Beispiel:

 3.14 + 2            (4)
 3 + 2.14            (5)

In einer Sprache könnte der Additionsoperator lediglich für zwei reelle Zahlen definiert sein. Coercion würde dann dafür sorgen, dass ganze Zahlen zuerst in Gleitkommazahlen umgewandelt werden. In (4) und (5) würde dann Coercion zum Einsatz kommen. Es ist aber auch denkbar, dass der Additionsoperator für mehrere Varianten definiert ist.

Bei der Überladung handelt es sich offenbar nicht um eine echte Form von Polymorphie, da man sich vorstellen könnte, der Compiler werde die Uneindeutigkeit durch die mehrfache Benutzung eines Symboles zur Kompilationszeit wieder auflösen. Wir erlauben also nur einem Symbol, verschiedene (funktionsartige) Werte zu denotieren, die allerdings unterschiedliche und möglicherweise zueinander inkompatible Typen haben.

Mit Coercions verhält es sich ähnlich. Man könnte meinen, ein Operator akzeptiere Operanden verschiedenen Typs (wie das + oben), jedoch müssen die Typen erst für den Operator gewandelt werden. Der Ausgabetyp des Operators hängt also nicht mehr mit den Typen der Operanden zusammen (oder nur partiell), daher kann keine echte Polymorphie vorliegen.

Parametrische Polymorphie

[Bearbeiten | Quelltext bearbeiten]

Parametrisierte Polymorphie repräsentiert Typen, deren Definitionen Typvariablen enthalten. In Java spricht man auch von generischen Typen oder Generics. Die meisten modernen objektorientierten Programmiersprachen unterstützen parametrische Typdefinitionen, darunter auch Strongtalk (eine Variante von Smalltalk mit Typsystem), C# oder Eiffel. In C++ können generische Typen mit Hilfe sogenannter Templates nachgebildet werden.

Beispiel:

  • monomorph
TYPE iContainer IS ARRAY OF INTEGER;
  • polymorph durch Typvariable
TYPE Stack IS ARRAY OF [TYPVARIABLE]

Beschränkter parametrischer Polymorphismus

[Bearbeiten | Quelltext bearbeiten]

Man unterscheidet grundsätzlich

  • einfachen parametrischen Polymorphismus und
  • beschränkten parametrischen Polymorphismus.

Letzterer behebt die Probleme der Typsicherheit, die innerhalb von Typdefinitionen dadurch entstehen, dass beim Erstellen der Typdefinition auf Grund der Parametrisierung noch nicht klar ist, Objekte welchen Typs eigentlich Gegenstand der Typ-Operationen (des Protokolls, der Methoden, die Terminologie variiert hier je nach Programmiersprache) sind. Wird durch einen Typ beispielsweise eine numerische Operation definiert, die auf den Elementen des Typs ausführbar sein soll, seine Typvariable dann aber mit einem nichtnumerischen Typen belegt, so würde es zu Laufzeitfehlern kommen. In der Regel verwendet man daher beschränkte parametrische Typen, die für ihre Typvariablen eine Beschränkung auf bestimmte Typen angeben. In Strongtalk wird hierzu bspw. die Typvariable mittels T < Supertyp angegeben, wobei Supertyp die Einschränkung der Typen angibt, die in die Typvariable T eingesetzt werden können. Java ermöglicht die Angabe solcher Einschränkungen mittels der Schreibweise <T extends Supertyp>. In C# gibt es verschiedene Arten von Einschränkungen ("Constraints"), die man für einen Typparameter verlangen kann: Abgeleitet von einer bestimmten Basisklasse, Vorhandensein eines bestimmten Interfaces oder eines parameterlosen Konstruktors sowie weitere. Kombinationen mehrerer Constraints sind möglich, die dann alle erfüllt sein müssen.

Inklusionspolymorphie

[Bearbeiten | Quelltext bearbeiten]

Inklusionspolymorphie bezeichnet die Eigenschaft, jede Methode statt auf einem Subtyp auch auf einem Basistypen ausführen zu können. Subtyping ist demnach eine Form der Inklusionspolymorphie.

Man unterscheidet zwischen:

  • Kompilationszeit-Polymorphie (statisches Binden)
    Es kann zur Kompilationszeit der Typ des Objekts und somit die aufgerufene Funktion (auch „Methode“ genannt) bestimmt werden.
  • Laufzeit-Polymorphie (dynamisches Binden).
    Erst zur Laufzeit kann bestimmt werden, welche Methode aufzurufen ist (späte Bindung). Es kann also vom Programmlauf abhängig sein, welche Methode zur Anwendung kommt. Die Laufzeit-Polymorphie ist einer der wichtigsten Bestandteile der objektorientierten Programmierung und wurde zuerst in der Programmiersprache Smalltalk umgesetzt und zum Beispiel in Objective-C eingesetzt. Ein weiteres Beispiel für späte Bindung sind generische Methoden wie im Common Lisp Object System.

Angenommen, eine Anwendung soll statistische Daten sowohl grafisch als auch schriftlich in Tabellenform darstellen. Außerdem soll es möglich sein, die Darstellungsmethoden über Plug-ins zu erweitern. Dann erlaubt das Konzept der Polymorphie über das VisualizationPlugin Interface jede beliebige Implementierung (hier GraphDisplayPlugin, TextDisplayPlugin, HistogramDisplayPlugin) aufzurufen.

Die Anwendung selbst muss bei neuen Plug-ins nicht geändert werden und kann diese einfach über das Interface mit dem Aufruf von setData und display starten.

interface VisualizationPlugin {
    public void setData(DisplayData data);
    public void display();
}

class GraphDisplayPlugin implements VisualizationPlugin {
    public void setData(DisplayData data) { /* set data to be displayed */ }
    public void display() { /* Show Data as Graph */ }
}

class TextDisplayPlugin implements VisualizationPlugin {
    public void setData(DisplayData data) { /* set data to be displayed */ }
    public void display() { /* Show Data as table */ }
}

class HistogramDisplayPlugin implements VisualizationPlugin {
    public void setData(DisplayData data) { /* set data and calculate history data */ }
    public void display() { /* Show history data as Graph */ }
}

Das folgende Beispiel in der Programmiersprache C# zeigt Subtyping für die Methode berechneFlaecheninhalt(). Die Klassen Rechteck und Dreieck implementieren die Methode, die das Interface Polygon deklariert. Die Methode gibFlaecheninhalt(Polygon polygon) kann die Implementierung der Methode berechneFlaecheninhalt() für jeden Subtyp des Interface Polygon (siehe abgeleitete Klasse) aufrufen.

public interface Polygon
{
	double berechneFlaecheninhalt();
}

public class Rechteck : Polygon
{
	private double breite, hoehe;
	
	// Konstruktor
	public Rechteck(double breite, double hoehe)
	{
		this.breite = breite;
		this.hoehe = hoehe;
	}
	
	public double berechneFlaecheninhalt()
	{
		return breite * hoehe;
	}
}

public class Dreieck : Polygon
{
	private double a, b, c;
	
	// Konstruktor
	public Dreieck(double a, double b, double c)
	{
		this.a = a;
		this.b = b;
		this.c = c;
	}
	
	public double berechneFlaecheninhalt()
	{
		// Formel des Heron für den Flächeninhalt des allgemeinen Dreiecks
		return 0.25 * Math.Sqrt((a + b + c) * (-a + b + c) * (a - b + c) * (a + b - c));
	}
}

public static double gibFlaecheninhalt(Polygon polygon)
{
	return polygon.berechneFlaecheninhalt();
}

public static void Main(string[] args)
{
	double flaecheninhalt = gibFlaecheninhalt(new Rechteck(12, 16));  // flaecheninhalt = 192
	Console.WriteLine("Das Rechteck hat den Flächeninhalt " + flaecheninhalt);
	
	flaecheninhalt = gibFlaecheninhalt(new Dreieck(4, 13, 15));  // flaecheninhalt = 24
	Console.WriteLine("Das Dreieck hat den Flächeninhalt " + flaecheninhalt);
}

Einzelnachweise

[Bearbeiten | Quelltext bearbeiten]
  1. Informatik. In: Duden. Mannheim 2001, ISBN 978-3-411-10023-1, S. 496.
  2. Fundamental concepts in programming languages. Lecture notes for International Summer School in Computer Programming, Copenhagen, August 1967.
  3. On Understanding Types, Data Abstraction, and Polymorphism. In: ACM Computing Surveys. Band 17, Nr. 4, 1985, S. 471–522.