Est-ce une bonne pratique d'hériter de types génériques?

35

Est-il préférable d'utiliser List<string>dans les annotations de type ou StringListStringList

class StringList : List<String> { /* no further code!*/ }

Je suis tombé sur plusieurs d'entre eux dans Irony .

Ruudjah
la source
Si vous n'héritez pas de types génériques, alors pourquoi sont-ils là?
Mawg
7
@Mawg Ils ne peuvent être disponibles que pour l'instanciation.
Theodoros Chatzigiannakis
3
C’est l’une des choses que je préfère le moins à voir dans le code
Kik
4
Beurk. Non sérieusement. Quelle est la différence sémantique entre StringListet List<string>? Il n'y en a pas, et pourtant vous avez (le "générique" vous) réussi à ajouter encore un autre détail à la base de code qui est unique à votre projet et qui ne signifie rien à personne d'autre qu'après avoir étudié le code. Pourquoi masquer le nom d'une liste de chaînes simplement pour continuer à l'appeler liste de chaînes? D'autre part, si vous vouliez le faire, BagOBagels : List<string>je pense que cela a plus de sens. Vous pouvez le parcourir de la même manière que IEnumerable, les consommateurs externes ne se soucient pas de la mise en œuvre - du bien à tout prix.
Craig
1
XAML a un problème où vous ne pouvez pas utiliser de génériques et vous deviez créer un type comme celui-ci pour créer une liste
Daniel Little

Réponses:

27

Il y a une autre raison pour laquelle vous pouvez souhaiter hériter d'un type générique.

Microsoft recommande d' éviter l'imbrication de types génériques dans les signatures de méthodes .

Il est donc judicieux de créer des types nommés business-domain pour certains de vos génériques. Au lieu d’avoir un IEnumerable<IDictionary<string, MyClass>>, créez un type MyDictionary : IDictionary<string, MyClass>, puis votre membre devient un IEnumerable<MyDictionary>, ce qui est beaucoup plus lisible. Il vous permet également d’utiliser MyDictionary à plusieurs endroits, augmentant ainsi le caractère explicite de votre code.

Cela peut ne pas sembler être un avantage énorme, mais dans le code du monde des affaires, j'ai vu des génériques qui feraient dresser vos cheveux. Des choses comme List<Tuple<string, Dictionary<int, List<Dictionary<string, List<double>>>>>. La signification de ce générique n’est nullement évidente. Cependant, s'il était construit avec une série de types explicitement créés, l'intention du développeur d'origine serait probablement beaucoup plus claire. En outre, si ce type générique devait changer pour une raison quelconque, la recherche et le remplacement de toutes les instances de ce type générique pourraient être un véritable casse-tête, par rapport à sa modification dans la classe dérivée.

Enfin, il existe une autre raison pour laquelle une personne peut souhaiter créer ses propres types dérivés de génériques. Considérez les classes suivantes:

public class MyClass
{
    public ICollection<MyItems> MyItems { get; private set; }
    // plumbing code here
}

public class MyOtherClass
{
    public ICollection<MyItems> MyItemCache { get; private set; }
    // plumbing code here
}

public class MyConsumerClass
{
    public MyConsumerClass(ICollection<MyItems> myItems)
    {
        // use the collection
    }
}

Maintenant, comment savons-nous qui ICollection<MyItems>doit passer dans le constructeur de MyConsumerClass? Si nous créons des classes dérivées class MyItems : ICollection<MyItems>et que class MyItemCache : ICollection<MyItems>MyConsumerClass peut alors être extrêmement spécifique sur celle des deux collections qu’elle souhaite réellement. Cela fonctionne alors très bien avec un conteneur IoC, qui peut résoudre les deux collections très facilement. Cela permet également au programmeur d’être plus précis: s’il est judicieux MyConsumerClassde travailler avec n'importe quel ICollection, ils peuvent utiliser le constructeur générique. Si, toutefois, il n’est jamais logique d’utiliser un des deux types dérivés, le développeur peut restreindre le paramètre constructeur au type spécifique.

En résumé, il est souvent intéressant de dériver des types à partir de types génériques - cela permet un code plus explicite, le cas échéant, et facilite la lisibilité et la maintenabilité.

Stephen
la source
12
C'est une recommandation absolument ridicule de Microsoft.
Thomas Eding
7
Je ne pense pas que ce soit ridicule pour les API publiques. Les génériques imbriqués sont difficiles à comprendre d’un coup d’œil et un nom de type plus explicite peut souvent améliorer la lisibilité.
Stephen
4
Je ne pense pas que ce soit ridicule, et c'est certainement correct pour les API privées ... mais pour les API publiques, je ne pense pas que ce soit une bonne idée. Même Microsoft recommande d’accepter et de renvoyer les types les plus bas lorsque vous écrivez des API destinées au public.
Paul d'Aoust
35

En principe, il n'y a pas de différence entre hériter de types génériques et hériter de rien d'autre.

Cependant, pourquoi diable tireriez-vous de n'importe quelle classe sans rien ajouter de plus?

Julia Hayward
la source
17
Je suis d'accord. Sans rien apporter, je lisais "StringList" et je me disais "Qu'est-ce que ça fait vraiment ?"
Neil
1
Je conviens également que, dans le cas d’une langue à laquelle vous héritez, vous utilisiez le mot clé "extend", c’est un bon rappel que si vous héritez d’une classe, vous devriez l’étendre avec des fonctionnalités supplémentaires.
Elliot Blackburn
14
-1, pour ne pas comprendre que c'est juste un moyen de créer une abstraction en choisissant un nom différent - la création d'une abstraction peut être une très bonne raison pour ne rien ajouter à cette classe.
Doc Brown
1
Considérez ma réponse, je vais dans certaines des raisons pour lesquelles quelqu'un pourrait décider de le faire.
NPSF3000
1
"Pourquoi diable tireriez-vous de n'importe quelle classe sans rien ajouter de plus?" Je l'ai fait pour les contrôles utilisateur. Vous pouvez créer un contrôle utilisateur générique, mais vous ne pouvez pas l'utiliser dans un concepteur de formulaire Windows. Vous devez donc en hériter, en spécifiant le type et l'utiliser.
George T
13

Je ne connais pas très bien le langage C #, mais je pense que c’est le seul moyen d’implémenter un typedeflogiciel couramment utilisé par les programmeurs C ++ pour plusieurs raisons:

  • Cela empêche votre code de ressembler à une mer de chevrons.
  • Il vous permet d’échanger différents conteneurs sous-jacents si vos besoins changent ultérieurement, et vous permet de vous réserver le droit de le faire.
  • Il vous permet de donner à un type un nom plus pertinent dans son contexte.

Ils ont essentiellement rejeté le dernier avantage en choisissant des noms génériques ennuyeux, mais les deux autres raisons sont valables.

Karl Bielefeldt
la source
7
Eh ... Ils ne sont pas tout à fait les mêmes. En C ++, vous avez un alias littéral. StringList est un List<string>- ils peuvent être utilisés de manière interchangeable. Ceci est important lorsque vous travaillez avec d'autres API (ou lorsque vous exposez votre API), car tous les autres utilisateurs du monde l'utilisent List<string>.
Telastyn
2
Je réalise qu'ils ne sont pas exactement la même chose. Aussi proche que possible. La version plus faible présente certainement des inconvénients.
Karl Bielefeldt
24
Non, using StringList = List<string>;est l'équivalent C # le plus proche d'un typedef. Ce sous-classement empêchera l'utilisation de constructeurs avec des arguments.
Pete Kirkham
4
@PeteKirkham: définir une StringList en le usinglimitant au fichier de code source où usingest placé le. De ce point de vue, l'héritage est plus proche de typedef. Et créer une sous-classe n’empêche pas d’ajouter tous les constructeurs nécessaires (bien que cela puisse être fastidieux).
Doc Brown
2
@ Random832: permettez-moi de vous exprimer ceci différemment: en C ++, un typedef peut être utilisé pour attribuer le nom à un endroit donné , pour en faire une "source unique de vérité". En C #, usingne peut pas être utilisé à cette fin, mais l'héritage le peut. Cela montre pourquoi je ne suis pas d'accord avec le commentaire voté de Pete Kirkham - je ne considère pas usingcomme une véritable alternative au C ++ typedef.
Doc Brown
9

Je peux penser à au moins une raison pratique.

La déclaration de types non génériques héritant de types génériques (même sans rien ajouter) permet de référencer ces types à partir d’endroits où ils ne seraient pas disponibles ou disponibles de manière plus compliquée qu’ils ne le devraient.

C'est le cas, par exemple, lorsque vous ajoutez des paramètres via l'éditeur de paramètres dans Visual Studio, où seuls les types non génériques semblent disponibles. Si vous y allez, vous remarquerez que toutes les System.Collectionsclasses sont disponibles, mais aucune collection System.Collections.Genericn'apparaît comme applicable.

Un autre cas concerne l'instanciation de types via XAML dans WPF. La prise en charge des arguments de type dans la syntaxe XML n'est pas prise en charge lors de l'utilisation d'un schéma antérieur à 2009. Cela est aggravé par le fait que le schéma de 2006 semble être la valeur par défaut pour les nouveaux projets WPF.

Theodoros Chatzigiannakis
la source
1
Dans les interfaces de service Web WCF, vous pouvez utiliser cette technique pour fournir de meilleurs noms de types de collection (par exemple, PersonCollection au lieu de ArrayOfPerson ou autre)
Rico Suter
7

Je pense que vous devez regarder dans une histoire .

  • C # est utilisé depuis bien plus longtemps que les types génériques.
  • Je m'attends à ce que le logiciel donné ait sa propre StringList à un moment donné de l'histoire.
  • Cela a peut-être été implémenté en encapsulant une ArrayList.
  • Ensuite, une personne à refactoriser pour faire avancer la base de code.
  • Mais ne souhaitons pas toucher 1001 fichiers différents, alors terminez le travail.

Sinon, Theodoros Chatzigiannakis avait les meilleures réponses. Par exemple, il existe encore de nombreux outils qui ne comprennent pas les types génériques. Ou peut-être même un mélange de sa réponse et de son histoire.

Ian
la source
3
"C # est utilisé depuis bien plus longtemps que pour les types génériques." Pas vraiment, C # sans génériques existait depuis environ 4 ans (janvier 2002 - novembre 2005), alors que C # avec génériques a maintenant 9 ans.
svick
4

Dans certains cas, il est impossible d'utiliser des types génériques, par exemple, vous ne pouvez pas référencer un type générique dans XAML. Dans ces cas, vous pouvez créer un type non générique qui hérite du type générique que vous vouliez utiliser à l'origine.

Si possible, je l'éviterais.

Contrairement à un typedef, vous créez un nouveau type. Si vous utilisez StringList en tant que paramètre d'entrée pour une méthode, vos appelants ne peuvent pas passer un List<string>.

En outre, vous devez explicitement copier tous les constructeurs que vous souhaitez utiliser. Dans l'exemple StringList, vous ne pouvez pas le dire new StringList(new[]{"a", "b", "c"}), même si vous List<T>définissez un tel constructeur.

Modifier:

La réponse acceptée se concentre sur les professionnels lors de l'utilisation des types dérivés dans votre modèle de domaine. Je conviens qu'ils peuvent potentiellement être utiles dans ce cas, mais personnellement, je les éviterais même là.

Mais vous devriez (à mon avis) ne jamais les utiliser dans une API publique, soit parce que vous rendez la vie de votre appelant plus difficile ou que vous gaspillez les performances (je n'aime pas vraiment les conversions implicites en général).

Lukas Rieger
la source
3
Vous dites " vous ne pouvez pas référencer un type générique en XAML ", mais je ne suis pas sûr de savoir de quoi vous parlez. Voir Génériques dans XAML
pswg
1
C'était destiné à titre d'exemple. Oui, cela est possible avec le schéma XAML 2009, mais pas autant que je sache avec le schéma 2006, qui est toujours celui par défaut du système virtuel.
Lukas Rieger
1
Votre troisième paragraphe est juste à moitié vrai, vous pouvez réellement permettre cela avec des convertisseurs implicites;)
Knerd
1
@Knerd, vrai, mais vous créez «implicitement» un nouvel objet. À moins que vous ne travailliez beaucoup plus, cela créera également une copie des données. Peu importe probablement avec les petites listes, mais amusez-vous, si quelqu'un passe une liste avec quelques milliers d'éléments =)
Lukas Rieger
@LukasRieger vous avez raison: PI voulait juste signaler cela: P
Knerd
2

Voici un exemple de la façon dont l'héritage d'une classe générique est utilisé dans le compilateur Roslyn de Microsoft, sans même changer le nom de la classe. (J'étais tellement décontenancé par ça que je me suis retrouvé ici dans ma recherche pour voir si c'était vraiment possible.)

Dans le projet CodeAnalysis, vous pouvez trouver cette définition:

/// <summary>
/// Common base class for C# and VB PE module builder.
/// </summary>
internal abstract class PEModuleBuilder<TCompilation, TSourceModuleSymbol, TAssemblySymbol, TTypeSymbol, TNamedTypeSymbol, TMethodSymbol, TSyntaxNode, TEmbeddedTypesManager, TModuleCompilationState> : CommonPEModuleBuilder, ITokenDeferral
    where TCompilation : Compilation
    where TSourceModuleSymbol : class, IModuleSymbol
    where TAssemblySymbol : class, IAssemblySymbol
    where TTypeSymbol : class
    where TNamedTypeSymbol : class, TTypeSymbol, Cci.INamespaceTypeDefinition
    where TMethodSymbol : class, Cci.IMethodDefinition
    where TSyntaxNode : SyntaxNode
    where TEmbeddedTypesManager : CommonEmbeddedTypesManager
    where TModuleCompilationState : ModuleCompilationState<TNamedTypeSymbol, TMethodSymbol>
{
  ...
}

Ensuite, dans le projet CSharpCodeanalysis, il y a cette définition:

internal abstract class PEModuleBuilder : PEModuleBuilder<CSharpCompilation, SourceModuleSymbol, AssemblySymbol, TypeSymbol, NamedTypeSymbol, MethodSymbol, SyntaxNode, NoPia.EmbeddedTypesManager, ModuleCompilationState>
{
  ...
}

Cette classe PEModuleBuilder non générique est largement utilisée dans le projet CSharpCodeanalysis et plusieurs classes de ce projet en héritent directement ou indirectement.

Et puis dans le projet BasicCodeanalysis, il y a cette définition:

Partial Friend MustInherit Class PEModuleBuilder
    Inherits PEModuleBuilder(Of VisualBasicCompilation, SourceModuleSymbol, AssemblySymbol, TypeSymbol, NamedTypeSymbol, MethodSymbol, SyntaxNode, NoPia.EmbeddedTypesManager, ModuleCompilationState)

Puisque nous pouvons (espérons-le) supposer que Roslyn a été écrit par des personnes ayant une connaissance approfondie de C # et de la manière dont il devrait être utilisé, je pense qu'il s'agit d'une recommandation de la technique.

RenniePet
la source
Oui. Ils peuvent également être utilisés pour affiner la clause where directement lors de la déclaration.
Sentinel
1

Il y a trois raisons possibles pour lesquelles quelqu'un essaierait de le faire par hasard:

1) Pour simplifier les déclarations:

Bien sûr , StringListet ListStringsont sur la même longueur mais imaginez au lieu que vous travaillez avec un UserCollectionqui est en fait Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>>ou d' un autre grand exemple générique.

2) Pour aider AOT:

Parfois, vous devez utiliser la compilation Ahead Of Time et non la compilation Just In Time - par exemple, le développement pour iOS. Parfois, le compilateur AOT a du mal à comprendre tous les types génériques à l’avance, ce qui peut être une tentative de lui donner un indice.

3) Ajouter des fonctionnalités ou supprimer des dépendances:

Peut-être qu'ils ont des fonctionnalités spécifiques qu'ils souhaitent mettre en œuvre pour StringList(peut être caché dans une méthode d'extension, pas encore ajouté ou historique) qu'ils soit ne peuvent pas ajouter List<string>sans hériter de lui, ou qu'ils ne veulent pas polluer List<string>avec .

Alternativement, ils peuvent souhaiter changer l’implémentation de StringListla piste, alors venez d’utiliser la classe comme marqueur (pour l’instant, vous feriez / utiliseriez des interfaces IList).


Je tiens également à noter que vous avez trouvé ce code dans Irony, un analyseur de langage, un type d'application assez inhabituel. Il peut y avoir une autre raison spécifique pour laquelle cela a été utilisé.

Ces exemples démontrent qu'il peut y avoir des raisons légitimes pour écrire une telle déclaration - même si elle n'est pas trop courante. Comme pour toute chose, envisagez plusieurs options et choisissez celle qui vous convient le mieux à ce moment-là.


Si vous examinez le code source, il apparaît que l'option 3 en est la raison - ils utilisent cette technique pour aider à ajouter des fonctionnalités / spécialiser des collections:

public class StringList : List<string> {
    public StringList() { }
    public StringList(params string[] args) {
      AddRange(args);
    }
    public override string ToString() {
      return ToString(" ");
    }
    public string ToString(string separator) {
      return Strings.JoinStrings(separator, this);
    }
    //Used in sorting suffixes and prefixes; longer strings must come first in sort order
    public static int LongerFirst(string x, string y) {
      try {//in case any of them is null
        if (x.Length > y.Length) return -1;
      } catch { }
      if (x == y) return 0;
      return 1; 
    }
NPSF3000
la source
1
Parfois, la quête pour simplifier les déclarations (ou simplifier quoi que ce soit) peut aboutir à une complexité accrue plutôt qu’à une complexité réduite. Tous ceux qui connaissent la langue moyennement bien savent ce qu’est un List<T>, mais ils doivent inspecter StringListdans le contexte pour réellement comprendre ce que c’est . En réalité, c'est juste List<string>, sous un nom différent. Il ne semble vraiment pas y avoir d’avantage sémantique à le faire dans un cas aussi simple. En fait, vous remplacez simplement un type bien connu par le même type sous un nom différent.
Craig
@Craig Je suis d'accord, comme indiqué par mon explication de cas d'utilisation (déclarations plus longues) et d'autres raisons possibles.
NPSF3000
-1

Je dirais que ce n'est pas une bonne pratique.

List<string>communique "une liste générique de chaînes". Les List<string> méthodes d'extension s'appliquent clairement . Moins de complexité est nécessaire pour comprendre; seule la liste est nécessaire.

StringListcommunique "le besoin d'une classe séparée". Peut être List<string> méthodes d’extension s’appliquent (vérifiez l’implémentation du type réel). Plus de complexité est nécessaire pour comprendre le code; Une connaissance de la liste et de la mise en œuvre de classe dérivée est requise

Ruudjah
la source
4
Les méthodes d'extension s'appliquent quand même.
Theodoros Chatzigiannakis
2
Bien sur. Mais vous ne savez pas que vous allez inspecter la StringList et en voir les résultats List<string>.
Ruudjah
@TheodorosChatzigiannakis Je me demande si Ruudjah pourrait préciser ce qu'il entend par "les méthodes d'extension ne s'appliquent pas". Les méthodes d'extension sont une possibilité avec n'importe quelle classe. Bien sûr, la bibliothèque .NET Framework est livrée avec un grand nombre de méthodes d’extension gratuites appelées LINQ qui s’appliquent aux classes de collection génériques, mais cela ne signifie pas pour autant que les méthodes d’extension ne s’appliquent à aucune autre classe? Peut-être que Ruudjah fait valoir un point qui n’est pas évident à première vue? ;-)
Craig
"les méthodes d'extension ne s'appliquent pas." Où lisez-vous cela? Je dis "les méthodes d'extension peuvent s'appliquer."
Ruudjah
1
L'implémentation de type ne vous dira pas si les méthodes d'extension s'appliquent ou non, n'est-ce pas? Juste le fait que ce soit un moyen de classe Méthodes d' extension ne s'appliquent. Tout consommateur de votre type peut créer des méthodes d'extension de manière totalement indépendante de votre code. Je suppose que c'est ce que je veux dire ou demander. Parlez-vous juste de LINQ? LINQ est implémenté à l' aide de méthodes d'extension. Mais l'absence de méthodes d'extension spécifiques à LINQ pour un type particulier ne signifie pas que les méthodes d'extension pour ce type n'existent pas ou n'existent pas. Ils pourraient être créés à tout moment par n'importe qui.
Craig