Was ist der Grund für die Verwendung einer Schnittstelle gegenüber einem generisch eingeschränkten Typ?

15

In objektorientierten Sprachen, die generische Typparameter unterstützen (auch als Klassenvorlagen und parametrischer Polymorphismus bezeichnet), ist es häufig möglich, eine Typbeschränkung für den Typparameter anzugeben, sodass er abgeleitet wird von einem anderen Typ. Dies ist beispielsweise die Syntax in C #:

//for classes:
class ExampleClass<T> where T : I1 {

}
//for methods:
S ExampleMethod<S>(S value) where S : I2 {
        ...
}

Welche Gründe sprechen dafür, tatsächliche Schnittstellentypen gegenüber Typen zu verwenden, für die diese Schnittstellen gelten? Was sind zum Beispiel die Gründe für die Signatur der Methode I2 ExampleMethod(I2 value)?

GregRos
quelle
4
Klassenvorlagen (C ++) sind etwas völlig anderes und weitaus leistungsfähiger als dürftige Generika. Obwohl Sprachen, für die Generika eine Vorlagensyntax entlehnt haben.
Deduplizierer
Schnittstellenmethoden sind indirekte Aufrufe, während Typmethoden direkte Aufrufe sein können. Letzteres kann also schneller sein als Ersteres, und im Fall von refWerttypparametern kann der Werttyp tatsächlich geändert werden.
Mehrdad
@ Deduplicator: Wenn man bedenkt, dass Generika älter als Vorlagen sind, kann ich nicht erkennen, wie Generika überhaupt etwas von Vorlagen entlehnt haben könnten, sei es in der Syntax oder auf andere Weise.
Jörg W Mittag
3
@ JörgWMittag: Ich vermute, dass Deduplicator unter "objektorientierten Sprachen, die Generika unterstützen" "Java und C #" und nicht "ML und Ada" verstanden hat. Dann ist der Einfluss von C ++ auf erstere klar, obwohl nicht alle Sprachen mit generischen oder parametrischen Polymorphismen von C ++ übernommen wurden.
Steve Jessop
2
@SteveJessop: ML, Ada, Eiffel, Haskell älter als C ++ - Vorlagen, Scala, F # und OCaml folgten, und keiner von ihnen teilt die Syntax von C ++. (Interessanterweise teilt auch D, das sich stark von C ++, insbesondere von Vorlagen, leiht, nicht die C ++ - Syntax.) "Java und C #" ist meiner Meinung nach eine eher enge Sicht auf "Sprachen mit Generika".
Jörg W Mittag

Antworten:

21

Mit der parametrischen Version gibt

  1. Weitere Informationen für die Benutzer der Funktion
  2. Beschränkt die Anzahl der Programme, die Sie schreiben können (kostenlose Fehlerprüfung)

Nehmen wir als zufälliges Beispiel an, wir haben eine Methode, die die Wurzeln einer quadratischen Gleichung berechnet

int solve(int a, int b, int c) {
  // My 7th grade math teacher is laughing somewhere
}

Und dann möchten Sie, dass es auch mit anderen Arten von Zahlen wie anderen Dingen funktioniert int. Sie können so etwas schreiben

Num solve(Num a, Num b, Num c){
  ...
}

Das Problem ist, dass dies nicht sagt, was Sie wollen. Es sagt

Gib mir 3 beliebige Dinge, die nummeriert sind (nicht unbedingt auf die gleiche Weise) und ich gebe dir eine Art Nummer zurück

Wir können so etwas wie int sol = solve(a, b, c)if a, nicht machen bund csind ints, weil wir nicht wissen, dass die Methode intam Ende eine zurückgibt! Dies führt zu etwas umständlichem Tanzen mit Niederwerfen und Beten, wenn wir die Lösung in einem größeren Ausdruck verwenden möchten.

Innerhalb der Funktion könnte uns jemand einen Float, einen Bigint und Grade geben, und wir müssten sie addieren und miteinander multiplizieren. Wir möchten dies statisch ablehnen, da die Operationen zwischen diesen drei Klassen nur Kauderwelsch sind. Grad sind mod 360, also wird es nicht so sein, dass a.plus(b) = b.plus(a)ähnliche Heiterkeiten auftauchen.

Wenn wir den parametrischen Polymorphismus mit Subtypisierung verwenden, können wir dies alles ausschließen, da unser Typ tatsächlich sagt, was wir meinen

<T : Num> T solve(T a, T b, T c)

Oder in Worten "Wenn Sie mir einen Typ geben, der eine Zahl ist, kann ich Gleichungen mit diesen Koeffizienten lösen".

Dies kommt auch an vielen anderen Orten vor. Eine weitere gute Quelle für Beispiele sind Funktionen , die abstrakt über eine Art Container, ala reverse, sort, mapetc.

Daniel Gratzer
quelle
8
Zusammenfassend garantiert die generische Version, dass alle drei Eingaben (und die Ausgabe) dieselbe Art von Nummer sein werden.
MathematicalOrchid
Dies reicht jedoch nicht aus, wenn Sie den betreffenden Typ nicht steuern (und ihm daher keine Schnittstelle hinzufügen können). Für eine maximale Allgemeinheit müsste eine durch den Argumenttyp (z. B. Num<int>) parametrisierte Schnittstelle als zusätzliches Argument akzeptiert werden . Sie können die Schnittstelle jederzeit für jeden Typ durch Delegierung implementieren. Dies ist im Wesentlichen das, was Haskells Typklassen sind, mit der Ausnahme, dass die Verwendung sehr viel langwieriger ist, da Sie die Schnittstelle explizit weitergeben müssen.
Doval,
16

Welche Gründe sprechen dafür, tatsächliche Schnittstellentypen gegenüber Typen zu verwenden, für die diese Schnittstellen gelten?

Denn genau das brauchen Sie ...

IFoo Fn(IFoo x);
T Fn<T>(T x) where T: IFoo;

sind zwei ausgesprochen unterschiedliche Unterschriften. Der erste nimmt einen beliebigen Typ an, der die Schnittstelle implementiert, und die einzige Garantie ist, dass der Rückgabewert der Schnittstelle entspricht.

Der zweite nimmt jeden Typ an, der die Schnittstelle implementiert, und garantiert, dass er mindestens diesen Typ erneut zurückgibt (und nicht etwas, das die weniger restriktive Schnittstelle erfüllt).

Manchmal möchten Sie die schwächere Garantie. Manchmal willst du den Stärkeren.

Telastyn
quelle
Können Sie ein Beispiel geben, wie Sie die schwächere Garantieversion verwenden würden?
GregRos
4
@ GregRos - Zum Beispiel in einem Parser-Code, den ich geschrieben habe. Ich habe eine Funktion Or, die zwei ParserObjekte nimmt (eine abstrakte Basisklasse, aber das Prinzip gilt) und eine neue zurückgibt Parser(aber mit einem anderen Typ). Der Endverbraucher sollte den konkreten Typ nicht kennen oder sich darum kümmern.
Telastyn
In C # stelle ich mir vor, dass es fast unmöglich ist, ein anderes T als das übergebene zurückzugeben (ohne Reflexionsschmerz), ohne die neue Einschränkung, was Ihre starke Garantie für sich genommen ziemlich nutzlos macht.
NtscCobalt
1
@NtscCobalt: Dies ist nützlicher, wenn Sie die parametrische und die generische Schnittstellenprogrammierung kombinieren. ZB was LINQ die ganze Zeit macht (akzeptiert ein IEnumerable<T>, gibt ein anderes zurück, IEnumerable<T>was zB tatsächlich ein ist OrderedEnumerable<T>)
Ben Voigt
2

Die Verwendung von eingeschränkten Generika für Methodenparameter kann es einer Methode ermöglichen, ihren Rückgabetyp genau auf den des übergebenen Objekts abzustimmen. In .NET können sie auch zusätzliche Vorteile bieten. Darunter:

  1. Einer Methode, die ein eingeschränktes generisches Element als Parameter refoder akzeptiert, outkann eine Variable übergeben werden, die die Einschränkung erfüllt. Im Gegensatz dazu ist eine nicht generische Methode mit einem Parameter vom Typ Schnittstelle darauf beschränkt, Variablen dieses genauen Schnittstellentyps zu akzeptieren.

  2. Eine Methode mit dem generischen Typparameter T kann generische Sammlungen von T akzeptieren. Eine Methode, die einen akzeptiert, kann einen IList<T> where T:IAnimalakzeptieren List<SiameseCat>, aber eine Methode, die einen wollte, kann IList<Animal>dies nicht.

  3. Eine Einschränkung kann manchmal eine Schnittstelle in Bezug auf den generischen Typ spezifizieren, z where T:IComparable<T>.

  4. Eine Struktur, die eine Schnittstelle implementiert, kann als Werttyp beibehalten werden, wenn sie an eine Methode übergeben wird, die einen eingeschränkten generischen Parameter akzeptiert, muss jedoch bei der Übergabe als Schnittstellentyp umrahmt werden. Dies kann eine enorme Auswirkung auf die Geschwindigkeit haben.

  5. Ein generischer Parameter kann mehrere Einschränkungen haben, während es keine andere Möglichkeit gibt, einen Parameter des Typs "Einige Typen, die sowohl IFoo als auch IBar implementieren" anzugeben. Manchmal kann dies ein zweischneidiges Schwert sein, da es für Code, der einen Parameter vom Typ erhalten IFoohat, sehr schwierig ist, ihn an eine solche Methode zu übergeben, die eine doppelte generische Einschränkung erwartet, selbst wenn die betreffende Instanz alle Einschränkungen erfüllen würde.

Wenn die Verwendung eines generischen Parameters in einer bestimmten Situation keinen Vorteil bietet, akzeptieren Sie einfach einen Parameter des Schnittstellentyps. Die Verwendung eines Generikums zwingt das Typsystem und JITter dazu, zusätzliche Arbeit zu leisten. Wenn es also keinen Nutzen gibt, sollte man dies nicht tun. Andererseits ist es sehr verbreitet, dass mindestens einer der oben genannten Vorteile zutrifft.

Superkatze
quelle