Parlons d'abord du polymorphisme paramétrique pur et abordons le polymorphisme borné plus tard.
Que signifie le polymorphisme paramétrique? Eh bien, cela signifie qu'un type, ou plutôt un constructeur de type, est paramétré par un type. Étant donné que le type est transmis en tant que paramètre, vous ne pouvez pas savoir à l'avance ce qu'il pourrait être. Vous ne pouvez faire aucune hypothèse sur cette base. Maintenant, si vous ne savez pas ce que cela pourrait être, alors à quoi ça sert? Que pouvez-vous en faire?
Eh bien, vous pouvez le stocker et le récupérer, par exemple. C'est le cas que vous avez déjà mentionné: les collections. Afin de stocker un élément dans une liste ou un tableau, je n'ai besoin de rien savoir sur l'élément. La liste ou le tableau peut être complètement inconscient du type.
Mais qu'en est-il du Maybe
type? Si vous ne le connaissez pas, Maybe
c'est un type qui a peut-être une valeur et peut-être pas. Où l'utiliseriez-vous? Eh bien, par exemple, lorsque vous extrayez un élément d'un dictionnaire: le fait qu'un élément ne soit pas dans le dictionnaire n'est pas une situation exceptionnelle, donc vous ne devriez vraiment pas lever d'exception si l'élément n'est pas là. Au lieu de cela, vous renvoyez une instance d'un sous-type de Maybe<T>
, qui a exactement deux sous-types: None
et Some<T>
. int.Parse
est un autre candidat de quelque chose qui devrait vraiment retourner au Maybe<int>
lieu de lancer une exception ou la int.TryParse(out bla)
danse entière .
Maintenant, vous pourriez dire que Maybe
c'est un peu comme une liste qui ne peut avoir que zéro ou un élément. Et donc un peu une collection.
Et alors Task<T>
? C'est un type qui promet de renvoyer une valeur à un moment donné dans le futur, mais qui n'a pas nécessairement de valeur pour le moment.
Ou alors Func<T, …>
? Comment représenteriez-vous le concept d'une fonction d'un type à un autre si vous ne pouvez pas résumer les types?
Ou, plus généralement: considérant que l'abstraction et la réutilisation sont les deux opérations fondamentales de l'ingénierie logicielle, pourquoi ne voudriez- vous pas pouvoir abstraire sur des types?
Parlons maintenant du polymorphisme borné. Le polymorphisme borné est essentiellement le point de rencontre du polymorphisme paramétrique et du polymorphisme de sous-type: au lieu qu'un constructeur de type ne soit pas complètement conscient de son paramètre de type, vous pouvez lier (ou contraindre) le type à être un sous-type d'un type spécifié.
Revenons aux collections. Prenez une table de hachage. Nous avons dit plus haut qu'une liste n'a pas besoin de connaître quoi que ce soit sur ses éléments. Eh bien, une table de hachage le fait: elle doit savoir qu'elle peut les hacher. (Remarque: en C #, tous les objets sont hachables, tout comme tous les objets peuvent être comparés pour l'égalité. Ce n'est pas vrai pour tous les langages, cependant, et est parfois considéré comme une erreur de conception même en C #.)
Donc, vous voulez contraindre votre paramètre de type pour que le type de clé dans la table de hachage soit une instance de IHashable
:
class HashTable<K, V> where K : IHashable
{
Maybe<V> Get(K key);
bool Add(K key, V value);
}
Imaginez si vous aviez à la place ceci:
class HashTable
{
object Get(IHashable key);
bool Add(IHashable key, object value);
}
Que feriez-vous avec une value
sortie de là? Vous ne pouvez rien en faire, vous savez seulement que c'est un objet. Et si vous itérez dessus, tout ce que vous obtenez est une paire de quelque chose que vous savez est un IHashable
(qui ne vous aide pas beaucoup car il n'a qu'une propriété Hash
) et quelque chose que vous savez est un object
(qui vous aide encore moins).
Ou quelque chose basé sur votre exemple:
class Repository<T> where T : ISerializable
{
T Get(int id);
void Save(T obj);
void Delete(T obj);
}
L'élément doit être sérialisable car il va être stocké sur le disque. Mais que se passe-t-il si vous avez ceci à la place:
class Repository
{
ISerializable Get(int id);
void Save(ISerializable obj);
void Delete(ISerializable obj);
}
Avec le cas générique, si vous mettez un BankAccount
, vous obtenez un BankAccount
retour, avec des méthodes et des propriétés telles que Owner
, AccountNumber
, Balance
, Deposit
, Withdraw
, etc. Quelque chose que vous pouvez travailler avec. Maintenant, l'autre cas? Vous mettez dans un BankAccount
mais vous récupérer un Serializable
, qui vient d' une propriété: AsString
. Qu'allez-vous faire avec ça?
Il y a aussi quelques astuces que vous pouvez faire avec le polymorphisme borné:
La quantification délimitée par F est essentiellement l'endroit où la variable de type apparaît à nouveau dans la contrainte. Cela peut être utile dans certaines circonstances. Par exemple, comment écrivez-vous une ICloneable
interface? Comment écrivez-vous une méthode où le type de retour est le type de la classe d'implémentation? Dans une langue avec une fonctionnalité MyType , c'est simple:
interface ICloneable
{
public this Clone(); // syntax I invented for a MyType feature
}
Dans un langage avec polymorphisme borné, vous pouvez faire quelque chose comme ceci à la place:
interface ICloneable<T> where T : ICloneable<T>
{
public T Clone();
}
class Foo : ICloneable<Foo>
{
public Foo Clone()
{
// …
}
}
Notez que ce n'est pas aussi sûr que la version MyType, car rien n'empêche quelqu'un de simplement passer la "mauvaise" classe au constructeur de type:
class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
public SomethingTotallyUnrelatedToBar Clone()
{
// …
}
}
Membres de type abstrait
Il s'avère que si vous avez des membres de type abstrait et un sous-typage, vous pouvez réellement vous passer complètement du polymorphisme paramétrique et toujours faire les mêmes choses. Scala se dirige dans cette direction, étant le premier langage majeur qui a commencé avec des génériques et essayant ensuite de les supprimer, ce qui est exactement l'inverse de Java et C # par exemple.
Fondamentalement, dans Scala, tout comme vous pouvez avoir des champs, des propriétés et des méthodes en tant que membres, vous pouvez également avoir des types. Et tout comme les champs, les propriétés et les méthodes peuvent être laissés abstraits pour être implémentés dans une sous-classe plus tard, les membres de type peuvent également être laissés abstraits. Revenons aux collections, un simple List
, qui ressemblerait à quelque chose comme ça, s'il était pris en charge en C #:
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>
aussi. c'est évidemment une application réelle. l'exemple est génial. mais pour une raison quelconque, je ne me sens pas satisfait. je veux plutôt me faire une idée quand c'est approprié et quand ça ne l'est pas. les réponses ici ont une certaine contribution à ce processus, mais je me sens toujours incomfy autour de tout cela. c'est étrange parce que les problèmes de langue ne me dérangent pas depuis si longtemps déjà.Non . Vous pensez trop à ce sujet
Repository
, où il est à peu près la même chose. Mais ce n'est pas pour ça que les génériques sont là. Ils sont là pour les utilisateurs .Le point clé ici n'est pas que le référentiel lui-même est plus générique. C'est que les utilisateurs sont plus spécialisés - c'est-à-dire que
Repository<BusinessObject1>
etRepository<BusinessObject2>
sont de types différents, et en plus, si je prends unRepository<BusinessObject1>
, je sais que je vaisBusinessObject1
en sortirGet
.Vous ne pouvez pas proposer ce typage fort à partir d'un simple héritage. Votre classe de référentiel proposée ne fait rien pour empêcher les gens de confondre les référentiels pour différents types d'objet métier ou de garantir le bon type d'objet métier à revenir.
la source
Object
". Je comprends aussi pourquoi utiliser des génériques lors de l'écriture de collections (principe DRY). Probablement, ma question initiale aurait dû porter sur l'utilisation de génériques en dehors du contexte des collections.IBusinessObject
est unBusinessObject1
ou unBusinessObject2
. Il ne peut pas résoudre les surcharges en fonction du type dérivé qu'il ne connaît pas. Il ne peut pas rejeter le code qui passe dans le mauvais type. Il y a un million de bits de typage plus fort sur lesquels Intellisense ne peut absolument rien faire. Un meilleur support d'outillage est un bel avantage mais n'a rien à voir avec les raisons principales."les clients les plus susceptibles de ce référentiel voudront obtenir et utiliser des objets via l'interface IBusinessObject".
Non, ils ne le feront pas.
Considérons que l'IBusinessObject a la définition suivante:
Il définit simplement l'ID car il s'agit de la seule fonctionnalité partagée entre tous les objets métier. Et vous avez deux objets commerciaux réels: personne et adresse (puisque les personnes n'ont pas de rues et les adresses n'ont pas de noms, vous ne pouvez pas les contraindre à une interface commune avec une fonctionnalité des deux. Ce serait une conception terrible, violant la Principe de ségrégation des interfaces , le "je" dans SOLID )
Maintenant, que se passe-t-il lorsque vous utilisez la version générique du référentiel:
Lorsque vous appelez la méthode Get sur le référentiel générique, l'objet renvoyé sera fortement typé, vous permettant d'accéder à tous les membres de la classe.
En revanche, lorsque vous utilisez le référentiel non générique:
Vous ne pourrez accéder aux membres qu'à partir de l'interface IBusinessObject:
Ainsi, le code précédent ne se compilera pas en raison des lignes suivantes:
Bien sûr, vous pouvez lancer l'IBussinesObject dans la classe réelle, mais vous perdrez toute la magie de temps de compilation que les génériques autorisent (conduisant à des InvalidCastExceptions plus tard), souffrira d'un cast inutilement ... Et même si vous ne le faites pas ne vous souciez pas du temps de compilation en vérifiant ni les performances (vous devriez), le casting après ne vous donnera certainement aucun avantage sur l'utilisation de génériques en premier lieu.
la source