Quelle est la raison d'utiliser une interface par rapport à un type contraint génériquement

15

Dans les langages orientés objet qui prennent en charge les paramètres de type génériques (également appelés modèles de classe et polymorphisme paramétrique, bien que chaque nom porte bien sûr des connotations différentes), il est souvent possible de spécifier une contrainte de type sur le paramètre de type, de sorte qu'il soit descendu d'un autre type. Par exemple, voici la syntaxe en C #:

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

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

Quelles sont les raisons d'utiliser les types d'interface réels sur les types contraints par ces interfaces? Par exemple, quelles sont les raisons de la signature de la méthode I2 ExampleMethod(I2 value)?

GregRos
la source
4
les modèles de classe (C ++) sont quelque chose de complètement différent et beaucoup plus puissant que les génériques misérables. Même si les langages ayant des génériques ont emprunté la syntaxe des modèles pour eux.
Déduplicateur
Les méthodes d'interface sont des appels indirects, tandis que les méthodes de type peuvent être des appels directs. Ainsi, ce dernier peut être plus rapide que le premier et, dans le cas des refparamètres de type de valeur, peut en fait modifier le type de valeur.
user541686
@Deduplicator: Étant donné que les génériques sont plus anciens que les modèles, je ne vois pas comment les génériques auraient pu emprunter quoi que ce soit aux modèles, de syntaxe ou autre.
Jörg W Mittag
3
@ JörgWMittag: Je soupçonne que par "langages orientés objet qui prennent en charge les génériques", Deduplicator aurait pu comprendre "Java et C #" plutôt que "ML et Ada". Ensuite, l'influence du C ++ sur le premier est claire, malgré le fait que tous les langages ayant des génériques ou un polymorphisme paramétrique empruntés au C ++.
Steve Jessop
2
@SteveJessop: ML, Ada, Eiffel, Haskell sont antérieurs aux modèles C ++, Scala, F #, OCaml sont venus après, et aucun d'entre eux ne partage la syntaxe de C ++. (Fait intéressant, même D, qui emprunte beaucoup au C ++, en particulier aux modèles, ne partage pas la syntaxe de C ++.) "Java et C #" est une vue assez étroite des "langages ayant des génériques", je pense.
Jörg W Mittag

Réponses:

21

L'utilisation de la version paramétrique donne

  1. Plus d'informations aux utilisateurs de la fonction
  2. Limite le nombre de programmes que vous pouvez écrire (vérification gratuite des bogues)

À titre d'exemple aléatoire, supposons que nous ayons une méthode qui calcule les racines d'une équation quadratique

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

Et puis vous voulez qu'il fonctionne sur d'autres sortes de nombres comme des choses en plus int. Vous pouvez écrire quelque chose comme

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

Le problème est que cela ne dit pas ce que vous voulez. Ça dit

Donnez-moi 3 choses qui sont comme des nombres (pas nécessairement de la même manière) et je vous donnerai une sorte de nombre

Nous ne pouvons pas faire quelque chose comme int sol = solve(a, b, c)if a, bet csont ints parce que nous ne savons pas que la méthode va retourner un intà la fin! Cela conduit à des danses maladroites avec abattage et prière si nous voulons utiliser la solution dans une expression plus large.

À l'intérieur de la fonction, quelqu'un pourrait nous remettre un flotteur, un bigint et des degrés et nous devrions les ajouter et les multiplier ensemble. Nous aimerions rejeter statiquement cela parce que les opérations entre ces 3 classes vont être du charabia. Les degrés sont mod 360 donc ce ne sera pas le cas a.plus(b) = b.plus(a)et des hilarités similaires se produiront.

Si nous utilisons le polymorphisme paramétrique avec le sous-typage, nous pouvons exclure tout cela parce que notre type dit réellement ce que nous voulons dire

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

Ou en mots "Si vous me donnez un type qui est un nombre, je peux résoudre des équations avec ces coefficients".

Cela se produit également dans de nombreux autres endroits. Une autre bonne source d'exemples sont des fonctions qui font abstraction sur une sorte de récipient, ala reverse, sort, map, etc.

Daniel Gratzer
la source
8
En résumé, la version générique garantit que les trois entrées (et la sortie) seront du même type de numéro.
MathematicalOrchid
Cependant, cela ne suffit pas lorsque vous ne contrôlez pas le type en question (et que vous ne pouvez donc pas y ajouter d'interface). Pour une généralité maximale, vous devez accepter une interface paramétrée par le type d'argument (par exemple Num<int>) comme argument supplémentaire. Vous pouvez toujours implémenter l'interface pour tout type par délégation. C'est essentiellement ce que sont les classes de types de Haskell, sauf beaucoup plus fastidieuses à utiliser puisque vous devez passer explicitement autour de l'interface.
Doval
16

Quelles sont les raisons d'utiliser les types d'interface réels sur les types contraints par ces interfaces?

Parce que c'est ce dont vous avez besoin ...

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

sont deux signatures résolument différentes. La première prend tout type implémentant l'interface et la seule garantie qu'elle fait est que la valeur de retour satisfait l'interface.

Le second prend tout type implémentant l'interface et garantit qu'il renverra au moins ce type (plutôt que quelque chose qui satisfait l'interface moins restrictive).

Parfois, vous voulez une garantie plus faible. Parfois, vous voulez le plus fort.

Telastyn
la source
Pouvez-vous donner un exemple de la façon dont vous utiliseriez la version à garantie plus faible?
GregRos
4
@GregRos - Par exemple, dans du code analyseur que j'ai écrit. J'ai une fonction Orqui prend deux Parserobjets (une classe de base abstraite, mais le principe est vrai) et renvoie une nouvelle Parser(mais avec un type différent). L'utilisateur final ne doit pas savoir ou se soucier du type de béton.
Telastyn
En C #, j'imagine que rendre un T autre que celui qui a été transmis est presque impossible (sans douleur de réflexion) sans la nouvelle contrainte, ce qui rend votre garantie forte assez inutile en soi.
NtscCobalt
1
@NtscCobalt: C'est plus utile lorsque vous combinez une programmation générique paramétrique et d'interface. Par exemple, ce que LINQ fait tout le temps (accepte un IEnumerable<T>, renvoie un autre IEnumerable<T>qui est par exemple en fait un OrderedEnumerable<T>)
Ben Voigt
2

L'utilisation de génériques contraints pour les paramètres de méthode peut permettre à une méthode de retrouver son type de retour en fonction de celui de la chose transmise. Dans .NET, ils peuvent également présenter des avantages supplémentaires. Parmi eux:

  1. Une méthode qui accepte un générique contraint en tant que paramètre refou outpeut recevoir une variable qui satisfait la contrainte; en revanche, une méthode non générique avec un paramètre de type interface se limiterait à accepter des variables de ce type d'interface exact.

  2. Une méthode avec le paramètre de type générique T peut accepter des collections génériques de T. Une méthode qui accepte un IList<T> where T:IAnimalpourra accepter un List<SiameseCat>, mais une méthode qui voulait un IList<Animal>ne pourra pas le faire.

  3. Une contrainte peut parfois spécifier une interface en termes de type générique, par exemple where T:IComparable<T>.

  4. Une structure qui implémente une interface peut être conservée en tant que type de valeur lorsqu'elle est transmise à une méthode acceptant un paramètre générique contraint, mais doit être encadrée lorsqu'elle est transmise en tant que type d'interface. Cela peut avoir un effet énorme sur la vitesse.

  5. Un paramètre générique peut avoir plusieurs contraintes, alors qu'il n'existe aucun autre moyen de spécifier un paramètre de "un type qui implémente à la fois IFoo et IBar". Parfois, cela peut être une épée à double tranchant, car le code qui a reçu un paramètre de type IFooaura du mal à le passer à une telle méthode en attendant un générique à double contrainte, même si l'instance en question satisferait toutes les contraintes.

Si, dans une situation particulière, l'utilisation d'un générique ne présente aucun avantage, il suffit d'accepter un paramètre du type d'interface. L'utilisation d'un générique forcera le système de types et JITter à faire un travail supplémentaire, donc s'il n'y a aucun avantage, il ne faut pas le faire. D'un autre côté, il est très courant qu'au moins un des avantages ci-dessus s'applique.

supercat
la source