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.Nein. Du denkst zu viel darüber nach
Repository
, wo es so ziemlich dasselbe ist. Aber dafür sind die Generika nicht da. Sie sind für die Benutzer da .Der entscheidende Punkt hierbei ist nicht, dass das Repository selbst allgemeiner ist. Es ist , dass die Benutzer sind besser specialized- dh dass
Repository<BusinessObject1>
undRepository<BusinessObject2>
verschiedene Arten sind, und darüber hinaus, dass , wenn ich eine nehmenRepository<BusinessObject1>
, ich weiß , dass ich bekommenBusinessObject1
von der Rückseite ausGet
.Sie können diese starke Typisierung durch einfache Vererbung nicht anbieten. Die von Ihnen vorgeschlagene Repository-Klasse hindert die Benutzer nicht daran, Repositorys für verschiedene Arten von Geschäftsobjekten zu verwechseln oder sicherzustellen, dass die richtige Art von Geschäftsobjekt wiederhergestellt wird.
quelle
Object
" gesagt . Ich verstehe auch, warum beim Schreiben von Sammlungen Generika verwendet werden (DRY-Prinzip). Wahrscheinlich sollte meine erste Frage etwas über die Verwendung von Generika außerhalb des Sammlungskontexts gewesen sein.IBusinessObject
aBusinessObject1
oder a istBusinessObject2
. Es kann keine Überlastungen basierend auf dem abgeleiteten Typ beheben, den es nicht kennt. Es kann keinen Code ablehnen, der den falschen Typ übergibt. Es gibt eine Million Bits der stärkeren Typisierung, gegen die Intellisense absolut nichts ausrichten kann. Bessere Werkzeugunterstützung ist ein netter Vorteil, hat aber nichts mit den Hauptgründen zu tun."Sehr wahrscheinlich möchten Kunden dieses Repository Objekte über die IBusinessObject-Schnittstelle abrufen und verwenden."
Nein, werden sie nicht.
Angenommen, das IBusinessObject hat die folgende Definition:
Sie definiert lediglich die ID, da dies die einzige gemeinsame Funktionalität aller Geschäftsobjekte ist. Und Sie haben zwei eigentliche Geschäftsobjekte: Person und Adresse (da Personen keine Straßen und Adressen haben, die keine Namen haben, können Sie beide nicht auf eine gemeinsame Schnittstelle mit der Funktionalität von beiden beschränken. Das wäre ein schreckliches Design, das die Prinzip der Schnittstellenverschlechterung (das "I" in SOLID )
Was passiert nun, wenn Sie die generische Version des Repositorys verwenden:
Wenn Sie die Get-Methode für das generische Repository aufrufen, wird das zurückgegebene Objekt stark typisiert, sodass Sie auf alle Klassenmitglieder zugreifen können.
Auf der anderen Seite, wenn Sie das nicht generische Repository verwenden:
Sie können nur über die IBusinessObject-Oberfläche auf die Mitglieder zugreifen:
Daher wird der vorherige Code aufgrund der folgenden Zeilen nicht kompiliert:
Sicher, Sie können das IBussinesObject in die eigentliche Klasse umwandeln, aber Sie verlieren die gesamte Kompilierungszeit, die Generika erlauben (was später zu InvalidCastExceptions führt), und leiden unnötig unter unnötigem Overhead ... und selbst wenn Sie dies nicht tun Achten Sie darauf, dass Sie weder die Leistung noch die Kompilierungszeit überprüfen (Sie sollten dies tun). Ein Casting nach wird Ihnen definitiv keinen Vorteil gegenüber der erstmaligen Verwendung von Generika bringen.
quelle