Sprechen wir zuerst über den reinen parametrischen Polymorphismus und später über den beschränkten Polymorphismus.
Was bedeutet parametrischer Polymorphismus? Nun, es bedeutet, dass ein Typ oder vielmehr ein Typkonstruktor durch einen Typ parametrisiert wird. Da der Typ als Parameter übergeben wird, können Sie nicht im Voraus wissen, um welchen Typ es sich handelt. Darauf aufbauend können Sie keine Annahmen treffen. Nun, wenn Sie nicht wissen, was es sein könnte, wozu dient es dann? Was kannst du damit machen?
Nun, Sie können es zum Beispiel speichern und abrufen. Das ist der Fall, den Sie bereits erwähnt haben: Sammlungen. Um ein Element in einer Liste oder einem Array zu speichern, muss ich nichts über das Element wissen. Die Liste oder das Array kann den Typ völlig vergessen.
Aber was ist mit dem Maybe
Typ? Wenn Sie nicht damit vertraut sind, Maybe
handelt es sich um einen Typ, der vielleicht einen Wert hat und vielleicht auch nicht. Wo würdest du es verwenden? Nun, zum Beispiel, wenn Sie ein Element aus einem Wörterbuch entfernen: Die Tatsache, dass ein Element möglicherweise nicht im Wörterbuch enthalten ist, ist keine Ausnahmesituation. Sie sollten also wirklich keine Ausnahme auslösen, wenn das Element nicht vorhanden ist. Stattdessen geben Sie eine Instanz eines Subtyps von zurück Maybe<T>
, der genau zwei Subtypen hat: None
und Some<T>
. int.Parse
ist ein weiterer Kandidat für etwas, das wirklich Maybe<int>
eine Ausnahme oder den ganzen int.TryParse(out bla)
Tanz zurückgeben sollte, anstatt zu werfen .
Nun könnte man argumentieren, dass dies ein Maybe
bisschen wie eine Liste ist, die nur null oder ein Element enthalten kann. Und so irgendwie eine Sammlung.
Was ist dann mit Task<T>
? Es ist ein Typ, der verspricht, irgendwann in der Zukunft einen Wert zurückzugeben, aber momentan nicht unbedingt einen Wert hat.
Oder was ist mit Func<T, …>
? Wie würden Sie das Konzept einer Funktion von einem Typ zu einem anderen Typ darstellen, wenn Sie nicht über Typen abstrahieren können?
Oder allgemeiner: Wenn man bedenkt, dass Abstraktion und Wiederverwendung die beiden grundlegenden Operationen des Software-Engineerings sind, warum möchten Sie dann nicht in der Lage sein, über Typen hinweg zu abstrahieren?
Sprechen wir jetzt über den beschränkten Polymorphismus. Bei begrenztem Polymorphismus treffen parametrischer Polymorphismus und Subtyp-Polymorphismus aufeinander: Anstatt dass ein Typkonstruktor seinen Typparameter vollständig ignoriert, können Sie den Typ so binden (oder einschränken), dass er ein Subtyp eines bestimmten Typs ist.
Kehren wir zu den Sammlungen zurück. Nimm einen Hash-Tisch. Wir haben oben gesagt, dass eine Liste nichts über ihre Elemente wissen muss. Nun, eine Hashtabelle tut es: Sie muss wissen, dass sie sie hashen kann. (Hinweis: In C # sind alle Objekte hashbar, genau wie alle Objekte auf Gleichheit verglichen werden können. Dies gilt jedoch nicht für alle Sprachen und wird manchmal sogar in C # als Konstruktionsfehler angesehen.)
Sie möchten also Ihren Typparameter auf den Schlüsseltyp in der Hashtabelle beschränken, um eine Instanz von IHashable
:
class HashTable<K, V> where K : IHashable
{
Maybe<V> Get(K key);
bool Add(K key, V value);
}
Stellen Sie sich vor, Sie hätten stattdessen Folgendes:
class HashTable
{
object Get(IHashable key);
bool Add(IHashable key, object value);
}
Was würdest du damit machen, wenn du da value
rauskommst? Sie können nichts damit anfangen, Sie wissen nur, dass es sich um ein Objekt handelt. Und wenn du darüber iterierst, erhältst du nur ein Paar von etwas, von dem du weißt, dass es ein ist IHashable
(was dir nicht viel hilft, weil es nur eine Eigenschaft hat Hash
) und etwas, von dem du weißt, dass es ein ist object
(was dir noch weniger hilft).
Oder etwas basierend auf Ihrem Beispiel:
class Repository<T> where T : ISerializable
{
T Get(int id);
void Save(T obj);
void Delete(T obj);
}
Das Element muss serialisierbar sein, da es auf der Festplatte gespeichert werden soll. Aber was ist, wenn Sie dies stattdessen haben:
class Repository
{
ISerializable Get(int id);
void Save(ISerializable obj);
void Delete(ISerializable obj);
}
Mit dem allgemeinen Fall, wenn Sie ein setzten BankAccount
in, erhalten Sie einen BankAccount
zurück, mit Methoden und Eigenschaften wie Owner
, AccountNumber
, Balance
, Deposit
, Withdraw
etc. Etwas , das man arbeiten kann. Nun, der andere Fall? Sie setzen in ein , BankAccount
aber man bekommt wieder ein Serializable
, die nur eine der Eigenschaften hat: AsString
. Was wirst du damit machen?
Es gibt auch einige nette Tricks, die Sie mit beschränktem Polymorphismus machen können:
Bei der f-gebundenen Quantifizierung erscheint die Typvariable im Prinzip wieder in der Nebenbedingung. Dies kann unter bestimmten Umständen nützlich sein. Wie schreibt man zB ein ICloneable
Interface? Wie schreibt man eine Methode, bei der der Rückgabetyp der Typ der implementierenden Klasse ist? In einer Sprache mit einer MyType- Funktion ist das ganz einfach:
interface ICloneable
{
public this Clone(); // syntax I invented for a MyType feature
}
In einer Sprache mit beschränktem Polymorphismus können Sie stattdessen Folgendes tun:
interface ICloneable<T> where T : ICloneable<T>
{
public T Clone();
}
class Foo : ICloneable<Foo>
{
public Foo Clone()
{
// …
}
}
Beachten Sie, dass dies nicht so sicher ist wie die MyType-Version, da nichts jemanden davon abhält, einfach die "falsche" Klasse an den Typkonstruktor zu übergeben:
class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
public SomethingTotallyUnrelatedToBar Clone()
{
// …
}
}
Abstract-Typ-Mitglieder
Wie sich herausstellt, können Sie mit abstrakten Typmitgliedern und Subtypisierung sogar ganz ohne parametrischen Polymorphismus auskommen und trotzdem die gleichen Dinge tun. Scala geht in diese Richtung. Es ist die erste Hauptsprache, die mit Generika begann und dann versuchte, sie zu entfernen, was genau umgekehrt ist, z. B. bei Java und C #.
Grundsätzlich können Sie in Scala, genau wie Sie Felder, Eigenschaften und Methoden als Mitglieder haben können, auch Typen haben. Ebenso wie Felder, Eigenschaften und Methoden abstrakt bleiben können, um später in einer Unterklasse implementiert zu werden, können auch Typmember abstrakt bleiben. Kehren wir zurück zu den Kollektionen, einer einfachen List
, die ungefähr so aussehen würde, wenn sie in C # unterstützt würden:
class List
{
T; // syntax I invented for an abstract type member
T Get(int index) { /* … */ }
void Add(T obj) { /* … */ }
}
class IntList : List
{
T = int;
}
// this is equivalent to saying `List<int>` with generics
interface IFoo<T> where T : IFoo<T>
auch erinnerte . Das ist offensichtlich eine reale Anwendung. Das Beispiel ist großartig. aber aus irgendeinem grund bin ich nicht zufrieden. Ich möchte lieber darüber nachdenken, wann es angebracht ist und wann nicht. Antworten hier haben einen gewissen Beitrag zu diesem Prozess, aber ich fühle mich immer noch unwohl. Es ist seltsam, weil mich Sprachprobleme schon lange nicht mehr stören.