web-dev-qa-db-fra.com

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

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 sur ces dans Ironie .

35
Ruudjah

Il existe une autre raison pour laquelle vous souhaiterez peut-être hériter d'un type générique.

Microsoft recommande éviter d'imbriquer des types génériques dans les signatures de méthode .

Il est donc judicieux de créer des types nommés de domaine métier 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 IEnumerable<MyDictionary>, qui est beaucoup plus lisible. Il vous permet également d'utiliser MyDictionary à plusieurs endroits, augmentant ainsi la justesse de votre code.

Cela peut ne pas sembler être un énorme avantage, mais dans le code des affaires du monde réel, j'ai vu des génériques qui rendront vos cheveux indulgents. Des choses comme List<Tuple<string, Dictionary<int, List<Dictionary<string, List<double>>>>>. La signification de ce générique n'est en aucun cas évidente. Pourtant, s'il était construit avec une série de types créés explicitement, l'intention du développeur d'origine serait probablement beaucoup plus claire. De plus, si ce type générique devait changer pour une raison quelconque, trouver et remplacer toutes les instances de ce type générique pourrait être une vraie difficulté, par rapport à le changer dans la classe dérivée.

Enfin, il y a une autre raison pour laquelle quelqu'un pourrait 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 quels ICollection<MyItems> pour passer dans le constructeur de MyConsumerClass? Si nous créons des classes dérivées class MyItems : ICollection<MyItems> et class MyItemCache : ICollection<MyItems>, MyConsumerClass peut alors être extrêmement précis sur laquelle des deux collections il veut 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 logique que MyConsumerClass fonctionne avec n'importe quel ICollection, il peut prendre le constructeur générique. Si, cependant, il n'est jamais judicieux d'utiliser l'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 utile de dériver des types à partir de types génériques - cela permet un code plus explicite le cas échéant et aide à la lisibilité et à la maintenabilité.

27
Stephen

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

Cependant, pourquoi diable dériveriez-vous de la classe any sans rien ajouter de plus?

35
Julia Hayward

Je ne connais pas très bien C #, mais je pense que c'est la seule façon d'implémenter un typedef, que les programmeurs C++ utilisent couramment pour plusieurs raisons:

  • Il empêche votre code de ressembler à une mer de parenthèses angulaires.
  • Il vous permet d'échanger différents conteneurs sous-jacents si vos besoins changent plus tard, et indique clairement que vous vous en réservez le droit.
  • 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 toujours valables.

13
Karl Bielefeldt

Je peux penser à au moins une raison pratique.

La déclaration de types non génériques qui héritent de types génériques (même sans rien ajouter), permet à ces types d'être référencés à partir d'endroits où ils seraient autrement indisponibles ou disponibles d'une manière plus compliquée qu'ils ne devraient l'être.

Un tel cas est lors de l'ajout de paramètres via l'éditeur de paramètres dans Visual Studio, où seuls les types non génériques semblent être disponibles. Si vous y allez, vous remarquerez que tous les System.Collections les classes sont disponibles, mais aucune collection de System.Collections.Generic apparaît comme applicable.

Un autre cas est celui de l'instanciation de types via XAML dans WPF. Il n'y a pas de prise en charge pour le passage d'arguments de type dans la syntaxe XML 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.

Je pense que vous devez examiner certains historique.

  • C # est utilisé depuis beaucoup plus longtemps qu'il n'en a eu pour les types génériques.
  • Je m'attends à ce que le logiciel donné ait sa propre StringList à un moment donné de l'histoire.
  • Cela pourrait bien avoir été implémenté en encapsulant une ArrayList.
  • Puis quelqu'un a refactorisé pour faire avancer la base de code.
  • Mais je ne voulais pas toucher 1001 fichiers différents, alors finissez le travail.

Sinon, Theodoros Chatzigiannakis a eu les meilleures réponses, par ex. il y a encore beaucoup d'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.

7
Ian

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 la StringList comme paramètre d'entrée d'une méthode, vos appelants ne peuvent pas passer un List<string>.

En outre, vous devez copier explicitement tous les constructeurs que vous souhaitez utiliser, dans l'exemple StringList, vous ne pouvez pas dire new StringList(new[]{"a", "b", "c"}), même si List<T> Définit 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 suis d'accord qu'ils peuvent potentiellement être utiles dans ce cas, mais je les éviterais personnellement même là-bas.

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

4
Lukas Rieger

Pour ce que ça vaut, 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, et sans même changer le nom de la classe. (J'étais tellement déconcerté par cela 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 façon dont il devrait être utilisé, je pense que c'est une recommandation de la technique.

2
RenniePet

Il y a trois raisons possibles pour lesquelles quelqu'un essaierait de faire ça du haut de ma tête:

1) Pour simplifier les déclarations:

Bien sûr, StringList et ListString sont à peu près de la même longueur, mais imaginez plutôt que vous travaillez avec un UserCollection qui est en fait Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>> ou un autre grand exemple générique.

2) Pour aider AOT:

Parfois, vous devez utiliser la compilation Ahead Of Time et pas seulement la compilation In Time - par exemple développement pour iOS. Parfois, le compilateur AOT a du mal à déterminer tous les types génériques à l'avance, et cela pourrait être une tentative de lui fournir un indice.

3) Pour ajouter des fonctionnalités ou supprimer des dépendances:

Peut-être qu'ils ont des fonctionnalités spécifiques qu'ils souhaitent implémenter pour StringList (pourraient être cachés dans une méthode d'extension, pas encore ajoutés ou historiques) qu'ils ne peuvent pas ajouter à List<string> sans en hériter, ou qu'ils ne veulent pas polluer List<string> avec.

Alternativement, ils peuvent souhaiter changer l'implémentation de StringList sur la piste, alors venez d'utiliser la classe comme marqueur pour l'instant (généralement vous feriez/utiliseriez des interfaces pour cela, par exemple IList).


Je noterais également que vous avez trouvé ce code dans Irony, qui est 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 d'écrire une telle déclaration - même si elle n'est pas trop courante. Comme pour tout, envisagez plusieurs options et choisissez celle qui vous convient le mieux à l'époque.


Si vous regardez le code source il semble que l'option 3 soit la raison - ils utilisent cette technique pour aider à ajouter des collections de fonctionnalité/spécialisation:

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; 
    }
1
NPSF3000