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)
?
object-oriented
type-systems
generics
GregRos
la source
la source
ref
paramètres de type de valeur, peut en fait modifier le type de valeur.Réponses:
L'utilisation de la version paramétrique donne
À titre d'exemple aléatoire, supposons que nous ayons une méthode qui calcule les racines d'une équation quadratique
Et puis vous voulez qu'il fonctionne sur d'autres sortes de nombres comme des choses en plus
int
. Vous pouvez écrire quelque chose commeLe problème est que cela ne dit pas ce que vous voulez. Ça dit
Nous ne pouvons pas faire quelque chose comme
int sol = solve(a, b, c)
ifa
,b
etc
sontint
s parce que nous ne savons pas que la méthode va retourner unint
à 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
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.la source
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.Parce que c'est ce dont vous avez besoin ...
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.
la source
Or
qui prend deuxParser
objets (une classe de base abstraite, mais le principe est vrai) et renvoie une nouvelleParser
(mais avec un type différent). L'utilisateur final ne doit pas savoir ou se soucier du type de béton.IEnumerable<T>
, renvoie un autreIEnumerable<T>
qui est par exemple en fait unOrderedEnumerable<T>
)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:
Une méthode qui accepte un générique contraint en tant que paramètre
ref
ouout
peut 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.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:IAnimal
pourra accepter unList<SiameseCat>
, mais une méthode qui voulait unIList<Animal>
ne pourra pas le faire.Une contrainte peut parfois spécifier une interface en termes de type générique, par exemple
where T:IComparable<T>
.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.
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
IFoo
aura 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.
la source