È buona prassi ereditare da tipi generici?


35

È meglio usare List<string>nelle annotazioni dei tipi o StringListdoveStringList

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

Mi sono imbattuto in molti di questi in Irony .


Se non erediti da tipi generici, perché sono lì?
Mawg,

7
@Mawg Possono essere disponibili solo per l'istanza.
Theodoros Chatzigiannakis,

3
Questa è una delle mie cose meno preferite da vedere nel codice
Kik,

4
Che schifo. No sul serio. Qual è la differenza semantica tra StringListe List<string>? Non ce ne sono, eppure tu (il "tu" generico) sei riuscito ad aggiungere ancora un altro dettaglio alla base di codice che è unico solo per il tuo progetto e non significa niente per nessun altro fino a quando non hanno studiato il codice. Perché mascherare il nome di un elenco di stringhe solo per continuare a chiamarlo un elenco di stringhe? D'altra parte, se tu volessi farlo, BagOBagels : List<string>penso che abbia più senso. Puoi iterarlo come IEnumerable, i consumatori esterni non si preoccupano dell'implementazione - bontà a tutto tondo.
Craig,

1
XAML ha un problema in cui non è possibile utilizzare i generici ed è necessario creare un tipo come questo per popolare un elenco
Daniel Little,

Risposte:


27

C'è un altro motivo per cui potresti voler ereditare da un tipo generico.

Microsoft consiglia di evitare l'annidamento di tipi generici nelle firme dei metodi .

È una buona idea, quindi, creare tipi denominati di dominio aziendale per alcuni dei tuoi generici. Invece di avere un IEnumerable<IDictionary<string, MyClass>>, crea un tipo MyDictionary : IDictionary<string, MyClass>e il tuo membro diventa un IEnumerable<MyDictionary>, che è molto più leggibile. Inoltre, consente di utilizzare MyDictionary in diversi punti, aumentando l'esplicitazione del codice.

Questo potrebbe non sembrare un enorme vantaggio, ma nel codice aziendale del mondo reale ho visto generici che ti faranno rizzare i capelli. Cose come List<Tuple<string, Dictionary<int, List<Dictionary<string, List<double>>>>>. Il significato di quel generico non è affatto ovvio. Tuttavia, se fosse costruito con una serie di tipi creati esplicitamente, l'intenzione dello sviluppatore originale sarebbe molto più chiara. Inoltre, se quel tipo generico dovesse cambiare per qualche motivo, trovare e sostituire tutte le istanze di quel tipo generico potrebbe essere una vera seccatura, rispetto a cambiarlo nella classe derivata.

Infine, c'è un altro motivo per cui qualcuno potrebbe voler creare i propri tipi derivati ​​dai generici. Considera le seguenti classi:

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
    }
}

Ora come facciamo a sapere quale ICollection<MyItems>passaggio nel costruttore per MyConsumerClass? Se creiamo classi derivate class MyItems : ICollection<MyItems>e class MyItemCache : ICollection<MyItems>, MyConsumerClass può quindi essere estremamente specifico su quale delle due raccolte desidera effettivamente. Funziona quindi molto bene con un contenitore IoC, che può risolvere molto facilmente le due raccolte. Inoltre, consente al programmatore di essere più preciso: se ha senso MyConsumerClasslavorare con qualsiasi ICollection, può prendere il costruttore generico. Se, tuttavia, non ha mai senso dal punto di vista economico utilizzare uno dei due tipi derivati, lo sviluppatore può limitare il parametro del costruttore al tipo specifico.

In sintesi, spesso vale la pena derivare tipi da tipi generici: consente un codice più esplicito laddove appropriato e aiuta la leggibilità e la manutenibilità.


12
Questa è una raccomandazione Microsoft assolutamente ridicola.
Thomas Eding,

7
Non penso sia ridicolo per le API pubbliche. I generici nidificati sono difficili da capire a colpo d'occhio e un nome di tipo più significativo può spesso aiutare la leggibilità.
Stephen,

4
Non penso che sia ridicolo, e certamente va bene per le API private ... ma per le API pubbliche non penso sia una buona idea. Anche Microsoft consiglia di accettare e restituire i tipi più bassi quando si scrivono API destinate al consumo pubblico.
Paul d'Aoust,

35

In linea di principio non c'è differenza tra ereditare da tipi generici ed ereditare da qualsiasi altra cosa.

Tuttavia, perché mai dovresti derivare da una classe e non aggiungere altro?


17
Sono d'accordo. Senza contribuire, avrei letto "StringList" e avrei pensato: "Che cosa fa davvero ?"
Neil,

1
Sono anche d'accordo, in una lingua per ereditarti usare la parola chiave "estende", questo è un buon promemoria che se hai intenzione di ereditare una classe, dovresti estenderla con ulteriori funzionalità.
Elliot Blackburn,

14
-1, per non capire che questo è solo un modo per creare un'astrazione scegliendo un nome diverso: creare un'astrazione può essere un'ottima ragione per non aggiungere nulla a questa classe.
Doc Brown,

1
Considera la mia risposta, analizzo alcuni dei motivi per cui qualcuno potrebbe decidere di farlo.
NPSF3000,

1
"perché mai dovresti derivare da una classe e non aggiungere altro?" L'ho fatto per i controlli utente. È possibile creare un controllo utente generico ma non è possibile utilizzarlo in un designer di moduli Windows, quindi è necessario ereditarlo, specificando il tipo e utilizzarlo.
George T

13

Non conosco molto bene C #, ma credo che questo sia l'unico modo per implementare un typedef, che i programmatori C ++ usano comunemente per alcuni motivi:

  • Mantiene il tuo codice dall'aspetto come un mare di parentesi angolari.
  • Ti consente di scambiare diversi contenitori sottostanti se le tue esigenze cambiano in seguito, e chiarisce che ti riservi il diritto di farlo.
  • Ti consente di assegnare a un tipo un nome più pertinente nel contesto.

Fondamentalmente hanno gettato via l'ultimo vantaggio scegliendo nomi noiosi e generici, ma gli altri due motivi sono ancora validi.


7
Eh ... Non sono proprio gli stessi. In C ++ hai un alias letterale. StringList è un List<string>- possono essere usati in modo intercambiabile. Questo è importante quando si lavora con altre API (o quando si espone l'API), poiché tutti gli altri nel mondo lo usano List<string>.
Telastyn,

2
Mi rendo conto che non sono esattamente la stessa cosa. Il più vicino possibile. Ci sono sicuramente degli svantaggi nella versione più debole.
Karl Bielefeldt,

24
No, using StringList = List<string>;è l'equivalente C # più vicino di un typedef. Sottoclassare in questo modo impedirà l'uso di qualsiasi costruttore con argomenti.
Pete Kirkham,

4
@PeteKirkham: la definizione di StringList usinglo limita al file di codice sorgente in cui usingsi trova. Da questo punto di vista, l'eredità è più vicina typedef. E la creazione di una sottoclasse non impedisce a uno di aggiungere tutti i costruttori necessari (anche se può essere noioso).
Doc Brown,

2
@ Random832: fammi esprimere diversamente: in C ++, un typedef può essere usato per assegnare il nome in un posto , per renderlo una "singola fonte di verità". In C #, usingnon può essere utilizzato per questo scopo, ma l'eredità può. Questo dimostra perché non sono d'accordo con il commento votato di Pete Kirkham - non considero usinguna vera alternativa al C ++ typedef.
Doc Brown,

9

Mi viene in mente almeno un motivo pratico.

La dichiarazione di tipi non generici che ereditano da tipi generici (anche senza aggiungere nulla), consente a tali tipi di essere referenziati da luoghi in cui altrimenti non sarebbero disponibili o disponibili in un modo più contorto di quanto dovrebbero essere.

Uno di questi casi è quando si aggiungono impostazioni tramite l'editor Impostazioni in Visual Studio, in cui sembrano essere disponibili solo tipi non generici. Se ci vai, noterai che tutte le System.Collectionsclassi sono disponibili, ma nessuna raccolta da System.Collections.Genericappare come applicabile.

Un altro caso è rappresentato dall'istanza di tipi tramite XAML in WPF. Non è supportato il passaggio di argomenti di tipo nella sintassi XML quando si utilizza uno schema precedente al 2009. Ciò è aggravato dal fatto che lo schema del 2006 sembra essere il default per i nuovi progetti WPF.


1
Nelle interfacce del servizio Web WCF è possibile utilizzare questa tecnica per fornire nomi di tipi di raccolta più belli (ad es. PersonCollection anziché ArrayOfPerson o altro)
Rico Suter,

7

Penso che tu abbia bisogno di esaminare un po 'di storia .

  • C # è stato usato per molto più tempo di quanto non abbia avuto tipi generici.
  • Mi aspetto che il software in questione abbia un proprio StringList a un certo punto della storia.
  • Questo potrebbe essere stato implementato avvolgendo un ArrayList.
  • Quindi qualcuno a refactoring per spostare in avanti la base di codice.
  • Ma non volevo toccare 1001 file diversi, quindi finisci il lavoro.

Altrimenti Theodoros Chatzigiannakis ha avuto le risposte migliori, ad esempio ci sono ancora molti strumenti che non comprendono i tipi generici. O forse anche un mix di risposta e storia.


3
"C # è stato usato per molto più tempo di quanto non abbia avuto tipi generici per." Non proprio, C # senza generici esiste da circa 4 anni (gennaio 2002 - novembre 2005), mentre C # con generici ha ormai 9 anni.
svick,


4

In alcuni casi è impossibile utilizzare tipi generici, ad esempio non è possibile fare riferimento a un tipo generico in XAML. In questi casi è possibile creare un tipo non generico che eredita dal tipo generico che si voleva usare originariamente.

Se possibile, lo eviterei.

A differenza di un typedef, crei un nuovo tipo. Se si utilizza StringList come parametro di input per un metodo, i chiamanti non possono passare a List<string>.

Inoltre, dovresti copiare esplicitamente tutti i costruttori che desideri utilizzare, nell'esempio StringList che non potresti dire new StringList(new[]{"a", "b", "c"}), anche se List<T>definisce un tale costruttore.

Modificare:

La risposta accettata si concentra sui pro quando si utilizzano i tipi derivati ​​all'interno del modello di dominio. Concordo sul fatto che possano essere potenzialmente utili in questo caso, tuttavia, personalmente li eviterei anche lì.

Ma non dovresti (secondo me) non usarli mai in un'API pubblica perché rendi la vita del tuo chiamante più dura o sprechi le prestazioni (anche a me non piacciono molto le conversioni implicite in generale).


3
Dici " non puoi fare riferimento a un tipo generico in XAML ", ma non sono sicuro di cosa ti riferisca. Vedi Generics in XAML
pswg

1
Era inteso come esempio. Sì, è possibile con lo Schema XAML 2009, ma AFAIK non con lo Schema 2006, che è ancora il VS predefinito.
Lukas Rieger,

1
Il tuo terzo paragrafo è solo la metà, in realtà puoi permetterlo con i convertitori impliciti;)
Knerd

1
@Knerd, vero, ma poi "implicitamente" crei un nuovo oggetto. A meno che tu non faccia molto più lavoro, questo creerà anche una copia dei dati. Probabilmente non importa con piccoli elenchi, ma divertiti, se qualcuno passa un elenco con alcune migliaia di elementi =)
Lukas Rieger,

@LukasRieger hai ragione: PI volevo solo precisarlo: P
Knerd,

2

Per quello che vale, ecco un esempio di come l'ereditarietà da una classe generica viene utilizzata nel compilatore Roslyn di Microsoft e senza nemmeno cambiare il nome della classe. (Ero così confuso da questo che sono finito qui nella mia ricerca per vedere se questo era davvero possibile.)

Nel progetto CodeAnalysis puoi trovare questa definizione:

/// <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>
{
  ...
}

Quindi nel progetto CSharpCodeanalysis c'è questa definizione:

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

Questa classe PEModuleBuilder non generica è ampiamente utilizzata nel progetto CSharpCodeanalysis e diverse classi in quel progetto ereditano da esso, direttamente o indirettamente.

E poi nel progetto BasicCodeanalysis c'è questa definizione:

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

Dal momento che possiamo (si spera) presumere che Roslyn sia stato scritto da persone con una vasta conoscenza di C # e come dovrebbe essere usato, sto pensando che questa è una raccomandazione della tecnica.


Sì. Inoltre, possono essere utilizzati per perfezionare la clausola where direttamente quando dichiarato.
Sentinella

1

Ci sono tre possibili ragioni per cui qualcuno dovrebbe provare a farlo dalla cima della mia testa:

1) Per semplificare le dichiarazioni:

Sicuro StringListe ListStringhanno circa la stessa lunghezza, ma immagina invece che stai lavorando con un esempio in UserCollectionrealtà Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>>o qualche altro grande esempio generico.

2) Per aiutare AOT:

A volte è necessario utilizzare la compilazione Ahead Of Time e non la compilazione Just In Time, ad esempio lo sviluppo per iOS. A volte il compilatore AOT ha difficoltà a capire tutti i tipi generici in anticipo, e questo potrebbe essere un tentativo di fornire un suggerimento.

3) Per aggiungere funzionalità o rimuovere la dipendenza:

Forse hanno funzionalità specifiche che vogliono implementare per StringList(potrebbe essere nascosto in un metodo di estensione, non ancora aggiunto, o storica) che o non possono ampliare List<string>senza ereditare da essa, o che non vogliono inquinare List<string>con .

In alternativa, potrebbero voler cambiare l'implementazione di StringListdown the track, quindi per il momento hanno appena usato la classe come marker (di solito per questo faresti / useresti interfacce IList).


Vorrei anche notare che hai trovato questo codice in Irony, che è un parser di lingua - un tipo di applicazione abbastanza insolito. Potrebbe esserci qualche altro motivo specifico per cui è stato utilizzato.

Questi esempi dimostrano che ci possono essere motivi legittimi per scrivere una simile dichiarazione, anche se non è troppo comune. Come per qualsiasi cosa, considera diverse opzioni e scegli quella che è meglio per te al momento.


Se dai un'occhiata al codice sorgente , sembra che l'opzione 3 sia la ragione: usano questa tecnica per aggiungere funzionalità / specializzare le raccolte:

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
A volte la ricerca di semplificare le dichiarazioni (o semplificare qualunque cosa) può comportare una maggiore complessità, piuttosto che una riduzione della complessità. Chiunque conosca moderatamente bene la lingua sa cos'è un List<T>, ma deve ispezionarlo StringListnel contesto per capire veramente di cosa si tratta. In realtà, è solo List<string>, con un nome diverso. Non sembra davvero esserci alcun vantaggio semantico nel farlo in un caso così semplice. Stai davvero sostituendo un tipo noto con lo stesso tipo con un nome diverso.
Craig,

@Craig Sono d'accordo, come notato dalla mia spiegazione del caso d'uso (dichiarazioni più lunghe) e di altri possibili motivi.
NPSF3000,

-1

Direi che non è una buona pratica.

List<string>comunica "un elenco generico di stringhe". Si List<string> applicano chiaramente metodi di estensione. È necessaria meno complessità per capire; è necessaria solo la lista.

StringListcomunica "la necessità di una classe separata". Forse si List<string> applicano i metodi di estensione (controllare l'implementazione del tipo effettivo per questo). È necessaria una maggiore complessità per comprendere il codice; Sono richiesti l'elenco e la conoscenza dell'implementazione della classe derivata.


4
I metodi di estensione si applicano comunque.
Theodoros Chatzigiannakis,

2
Fuori rotta. Ma non sai che fino a quando non controlli la StringList e vedi che ne deriva List<string>.
Ruudjah,

@TheodorosChatzigiannakis Mi chiedo se Ruudjah potrebbe elaborare cosa intende con "i metodi di estensione non si applicano". I metodi di estensione sono possibili con qualsiasi classe. Certo, la libreria di framework .NET viene fornita con molti metodi di estensione chiamati LINQ gratuitamente che si applicano alle classi di raccolta generiche, ma ciò non significa che i metodi di estensione non si applichino ad altre classi? Forse Ruudjah sta facendo un punto che non è ovvio a prima vista? ;-)
Craig

"i metodi di estensione non si applicano." Dove leggi questo? Sto dicendo " potrebbero essere applicati metodi di estensione .
Ruudjah,

1
L'implementazione del tipo non ti dirà se applicare o meno i metodi di estensione, giusto? Solo il fatto che sia una classe significa che si applicano i metodi di estensione . Qualsiasi consumatore del tuo tipo può creare metodi di estensione completamente indipendenti dal codice. Immagino sia quello a cui mi sto rivolgendo o chiedendo. Stai solo parlando di LINQ? LINQ è implementato usando metodi di estensione. Ma l'assenza di metodi di estensione specifici di LINQ per un tipo particolare non significa che i metodi di estensione per quel tipo non esistano o non esistano. Potrebbero essere creati in qualsiasi momento da chiunque.
Craig,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.