Ist es empfehlenswert, von generischen Typen zu erben?

35

Ist es besser, List<string>in Typanmerkungen oder StringListwo zu verwendenStringList

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

Ich lief in mehr von ihnen in Irony .

Ruudjah
quelle
Wenn Sie nicht von generischen Typen erben, warum gibt es sie dann?
Mawg
7
@Mawg Sie können nur zur Instanziierung zur Verfügung stehen.
Theodoros Chatzigiannakis
3
Dies ist eines meiner am wenigsten bevorzugten Dinge im Code zu sehen
Kik
4
Yuck. Nein im Ernst. Was ist der semantische Unterschied zwischen StringListund List<string>? Es gibt keine, aber Sie (das generische "Sie") haben es geschafft, der Codebasis noch ein weiteres Detail hinzuzufügen, das nur für Ihr Projekt gilt und erst nach dem Studium des Codes für andere von Bedeutung ist. Warum den Namen einer Liste von Zeichenfolgen maskieren, um sie weiterhin als Liste von Zeichenfolgen zu bezeichnen? Auf der anderen Seite halte BagOBagels : List<string>ich das für sinnvoller , wenn Sie das tun wollten . Sie können es als IEnumerable iterieren, externe Verbraucher interessieren sich nicht für die Implementierung - Güte alles in allem.
Craig
1
XAML hat ein Problem, bei dem Sie keine Generika verwenden können und einen Typ wie diesen erstellen müssen, um eine Liste
Daniel Little

Antworten:

27

Es gibt einen weiteren Grund, warum Sie möglicherweise von einem generischen Typ erben möchten.

Microsoft empfiehlt , das Verschachteln generischer Typen in Methodensignaturen zu vermeiden .

Es ist daher eine gute Idee, für einige Ihrer Generika Typen mit Geschäftsdomänennamen zu erstellen. Anstatt einen zu haben IEnumerable<IDictionary<string, MyClass>>, erstelle einen Typ MyDictionary : IDictionary<string, MyClass>und dann wird dein Mitglied ein IEnumerable<MyDictionary>, was viel besser lesbar ist. Außerdem können Sie MyDictionary an mehreren Stellen verwenden, um die Aussagekraft Ihres Codes zu erhöhen.

Das hört sich vielleicht nicht nach einem großen Vorteil an, aber im realen Geschäftscode habe ich Generika gesehen, die Ihnen die Haare zu Berge stehen lassen. Dinge wie List<Tuple<string, Dictionary<int, List<Dictionary<string, List<double>>>>>. Die Bedeutung dieses Generikums ist in keiner Weise offensichtlich. Wenn es jedoch mit einer Reihe von explizit erstellten Typen erstellt würde, wäre die Absicht des ursprünglichen Entwicklers wahrscheinlich viel klarer. Wenn sich dieser generische Typ aus irgendeinem Grund ändern muss, kann es im Vergleich zu einer Änderung in der abgeleiteten Klasse ein echtes Problem sein, alle Instanzen dieses generischen Typs zu finden und zu ersetzen.

Schließlich gibt es noch einen weiteren Grund, warum jemand seine eigenen Typen von Generika ableiten möchte. Betrachten Sie die folgenden Klassen:

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

ICollection<MyItems>Woher wissen wir nun, was an den Konstruktor für MyConsumerClass übergeben werden soll? Wenn wir abgeleitete Klassen class MyItems : ICollection<MyItems>und class MyItemCache : ICollection<MyItems>erstellen, kann MyConsumerClass sehr genau festlegen, welche der beiden Sammlungen tatsächlich benötigt wird. Dies funktioniert dann sehr gut mit einem IoC-Container, der die beiden Sammlungen sehr leicht auflösen kann. Außerdem kann der Programmierer präziser arbeiten. Wenn es sinnvoll ist MyConsumerClass, mit einer ICollection zu arbeiten, kann er den generischen Konstruktor verwenden. Wenn es jedoch geschäftlich nie sinnvoll ist, einen der beiden abgeleiteten Typen zu verwenden, kann der Entwickler den Konstruktorparameter auf den bestimmten Typ beschränken.

Zusammenfassend lohnt es sich oft, Typen von generischen Typen abzuleiten - dies ermöglicht gegebenenfalls expliziteren Code und verbessert die Lesbarkeit und Wartbarkeit.

Stephen
quelle
12
Das ist eine absolut lächerliche Empfehlung von Microsoft.
Thomas Eding
7
Ich finde es nicht so lächerlich für öffentliche APIs. Verschachtelte Generika sind auf einen Blick schwer zu verstehen, und ein aussagekräftigerer Typname kann häufig die Lesbarkeit verbessern.
Stephen
4
Ich finde es nicht lächerlich, und es ist sicherlich in Ordnung für private APIs ... aber für öffentliche APIs halte ich es nicht für eine gute Idee. Selbst Microsoft empfiehlt, beim Schreiben von APIs, die für den öffentlichen Gebrauch bestimmt sind, die einfachsten Typen zu akzeptieren und zurückzugeben.
Paul d'Aoust,
35

Grundsätzlich gibt es keinen Unterschied zwischen dem Erben von generischen Typen und dem Erben von etwas anderem.

Aber warum um alles in der Welt würden Sie aus einer Klasse stammen und nichts weiter hinzufügen?

Julia Hayward
quelle
17
Genau. Ohne irgendetwas beizutragen, würde ich "StringList" lesen und mir denken: "Was macht es wirklich ?"
Neil
1
Ich bin auch damit einverstanden, dass Sie in der Mai-Sprache zum Erben das Schlüsselwort "extend" verwenden. Dies ist eine gute Erinnerung daran, dass Sie eine Klasse mit weiteren Funktionen erweitern sollten, wenn Sie sie erben möchten.
Elliot Blackburn
14
-1, um nicht zu verstehen, dass dies nur eine Möglichkeit ist, eine Abstraktion durch Auswahl eines anderen Namens zu erstellen. Das Erstellen von Abstraktionen kann ein sehr guter Grund sein, dieser Klasse nichts hinzuzufügen.
Doc Brown
1
Betrachten Sie meine Antwort, ich gehe auf einige der Gründe ein, warum sich jemand dazu entschließen könnte.
NPSF3000
1
"Warum um alles in der Welt würden Sie aus einer Klasse stammen und nichts weiter hinzufügen?" Ich habe es für Benutzersteuerelemente getan. Sie können ein generisches Benutzersteuerelement erstellen, es jedoch nicht in einem Windows Formulardesigner verwenden. Sie müssen es also erben, den Typ angeben und dieses verwenden.
George T
13

Ich kenne C # nicht sehr gut, aber ich glaube, dies ist die einzige Möglichkeit, ein zu implementieren typedef, das C ++ - Programmierer aus verschiedenen Gründen verwenden:

  • Es verhindert, dass Ihr Code wie ein Meer von spitzen Klammern aussieht.
  • Sie können damit verschiedene zugrunde liegende Container austauschen, wenn sich Ihre Anforderungen später ändern, und Sie behalten sich das Recht vor, dies zu tun.
  • Sie können einem Typ einen Namen geben, der für den Kontext relevanter ist.

Sie haben im Grunde genommen den letzten Vorteil durch die Wahl langweiliger Gattungsnamen zunichte gemacht, aber die beiden anderen Gründe bleiben bestehen.

Karl Bielefeldt
quelle
7
Eh ... Sie sind nicht ganz gleich. In C ++ haben Sie einen wörtlichen Alias. StringList ist ein List<string>- sie können austauschbar verwendet werden. Dies ist wichtig, wenn Sie mit anderen APIs arbeiten (oder wenn Sie Ihre API verfügbar machen), da alle anderen Benutzer auf der Welt diese verwenden List<string>.
Telastyn
2
Mir ist klar, dass sie nicht genau dasselbe sind. Genau so nah wie möglich. Die schwächere Version hat sicherlich Nachteile.
Karl Bielefeldt
24
Nein, using StringList = List<string>;ist das nächste C # -Äquivalent einer Typendefinition. Eine solche Unterklasse verhindert die Verwendung von Konstruktoren mit Argumenten.
Pete Kirkham
4
@PeteKirkham: Durch Definieren einer StringList wird usingdiese auf die Quellcodedatei beschränkt, in der sich die usingbefindet. Aus dieser Sicht ist die Vererbung näher typedef. Und das Erstellen einer Unterklasse hindert eine Unterklasse nicht daran, alle benötigten Konstruktoren hinzuzufügen (obwohl dies möglicherweise mühsam ist).
Doc Brown
2
@ Random832: Lassen Sie mich das anders ausdrücken: In C ++ kann ein typedef verwendet werden, um den Namen an einer Stelle zuzuweisen und ihn zu einer "einzigen Quelle der Wahrheit" zu machen. usingKann in C # nicht für diesen Zweck verwendet werden, aber die Vererbung kann. Dies zeigt, warum ich Pete Kirkhams positivem Kommentar nicht zustimme - ich halte das nicht für usingeine echte Alternative zu C ++ typedef.
Doc Brown
9

Mir fällt mindestens ein praktischer Grund ein.

Durch das Deklarieren von nicht generischen Typen, die von generischen Typen erben (auch ohne Hinzufügen von Elementen), können diese Typen an Stellen referenziert werden, an denen sie sonst nicht verfügbar wären oder auf eine komplexere Weise verfügbar wären, als sie sollten.

Ein solcher Fall ist das Hinzufügen von Einstellungen über den Einstellungseditor in Visual Studio, in dem anscheinend nur nicht generische Typen verfügbar sind. Wenn Sie dorthin gehen, werden Sie feststellen, dass alle System.CollectionsKlassen verfügbar sind, jedoch keine Sammlungen von vorhanden sind System.Collections.Generic.

Ein weiterer Fall ist das Instanziieren von Typen über XAML in WPF. Bei Verwendung eines Schemas vor 2009 wird die Übergabe von Typargumenten in der XML-Syntax nicht unterstützt. Dies wird durch die Tatsache verschlimmert, dass das 2006-Schema der Standard für neue WPF-Projekte zu sein scheint.

Theodoros Chatzigiannakis
quelle
1
In WCF-Webdienstschnittstellen können Sie diese Technik verwenden, um besser aussehende Auflistungstypnamen bereitzustellen (z. B. PersonCollection anstelle von ArrayOfPerson oder was auch immer)
Rico Suter
7

Ich denke, Sie müssen in die Geschichte schauen .

  • C # wurde viel länger verwendet als es generische Typen für gab.
  • Ich gehe davon aus, dass die angegebene Software irgendwann in der Geschichte eine eigene StringList hatte.
  • Möglicherweise wurde dies durch das Umschließen einer ArrayList implementiert.
  • Dann muss jemand ein Refactoring durchführen, um die Codebasis vorwärts zu bewegen.
  • Wollte aber nicht 1001 verschiedene Dateien anfassen, also beende den Job.

Ansonsten hatte Theodoros Chatzigiannakis die besten Antworten, zB gibt es immer noch viele Tools, die keine generischen Typen verstehen. Oder vielleicht sogar eine Mischung aus seiner Antwort und seiner Geschichte.

Ian
quelle
3
Msgstr "C # wurde viel länger verwendet als generische Typen." Nicht wirklich, C # ohne Generika gab es ungefähr 4 Jahre (Januar 2002 - November 2005), während C # mit Generika jetzt 9 Jahre alt ist.
Svick
4

In einigen Fällen ist es nicht möglich, generische Typen zu verwenden. Beispielsweise können Sie in XAML keinen generischen Typ referenzieren. In diesen Fällen können Sie einen nicht generischen Typ erstellen, der von dem generischen Typ erbt, den Sie ursprünglich verwenden wollten.

Wenn möglich, würde ich es vermeiden.

Im Gegensatz zu einem Typedef erstellen Sie einen neuen Typ. Wenn Sie die StringList als Eingabeparameter für eine Methode verwenden, können Ihre Aufrufer a nicht übergeben List<string>.

Außerdem müssten Sie alle Konstruktoren, die Sie verwenden möchten, explizit kopieren, was Sie im StringList-Beispiel nicht sagen können new StringList(new[]{"a", "b", "c"}), obwohl Sie List<T>einen solchen Konstruktor definieren.

Bearbeiten:

Die akzeptierte Antwort konzentriert sich auf die Profis, wenn die abgeleiteten Typen in Ihrem Domain-Modell verwendet werden. Ich bin damit einverstanden, dass sie in diesem Fall möglicherweise nützlich sein können, aber ich persönlich würde sie sogar dort meiden.

Sie sollten sie jedoch (meiner Meinung nach) niemals in einer öffentlichen API verwenden, da Sie entweder das Leben Ihres Anrufers verkürzen oder die Leistung beeinträchtigen (implizite Conversions gefallen mir im Allgemeinen auch nicht wirklich).

Lukas Rieger
quelle
3
Sie sagen: " Sie können in XAML nicht auf einen generischen Typ verweisen ", aber ich bin mir nicht sicher, worauf Sie sich beziehen. Siehe Generics in XAML
pswg
1
Es war als Beispiel gedacht. Ja, es ist mit dem XAML-Schema 2009 möglich, aber AFAIK nicht mit dem Schema 2006, das immer noch die VS-Standardeinstellung ist.
Lukas Rieger
1
Ihr dritter Absatz ist nur zur Hälfte wahr, Sie können dies tatsächlich mit impliziten Konvertern zulassen;)
Knerd
1
@Knerd, true, aber dann erstellen Sie "implizit" ein neues Objekt. Wenn Sie nicht viel mehr arbeiten, wird auch eine Kopie der Daten erstellt. Bei kleinen Listen ist das wahrscheinlich egal, aber viel Spaß, wenn jemand eine Liste mit ein paar tausend Elementen übergibt =)
Lukas Rieger
@ LukasRieger du hast recht: PI wollte nur darauf hinweisen: P
Knerd
2

Nachstehend finden Sie ein Beispiel dafür, wie das Erben von einer generischen Klasse im Microsoft Roslyn-Compiler verwendet wird, ohne den Namen der Klasse zu ändern. (Ich war so verblüfft, dass ich hier auf meiner Suche gelandet bin, um zu sehen, ob dies wirklich möglich ist.)

Im Projekt CodeAnalysis finden Sie diese Definition:

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

Dann gibt es in Projekt CSharpCodeanalysis diese Definition:

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

Diese nicht generische PEModuleBuilder-Klasse wird häufig im CSharpCodeanalysis-Projekt verwendet, und mehrere Klassen in diesem Projekt erben direkt oder indirekt davon.

Und dann gibt es im Projekt BasicCodeanalysis diese Definition:

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

Da wir (hoffentlich) davon ausgehen können, dass Roslyn von Leuten mit umfassenden Kenntnissen in C # geschrieben wurde und wie es verwendet werden sollte, denke ich, dass dies eine Empfehlung der Technik ist.

RenniePet
quelle
Ja. Sie können auch verwendet werden, um die where-Klausel direkt bei der Deklaration zu verfeinern.
Sentinel
1

Es gibt drei mögliche Gründe, warum jemand versucht, das aus meinem Kopf heraus zu tun:

1) Vereinfachung der Erklärungen:

Sicher StringListund ListStringungefähr gleich lang, aber stellen Sie sich stattdessen vor, Sie arbeiten mit einem UserCollectiontatsächlichen Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>>oder einem anderen großen generischen Beispiel.

2) Um AOT zu helfen:

Manchmal müssen Sie die Ahead Of Time-Kompilierung und nicht die Just In Time-Kompilierung verwenden, z. B. für iOS. Manchmal hat der AOT-Compiler Schwierigkeiten, alle generischen Typen im Voraus herauszufinden, und dies könnte ein Versuch sein, ihm einen Hinweis zu geben.

3) So fügen Sie Funktionen hinzu oder entfernen Abhängigkeiten:

Vielleicht haben sie spezifische Funktionalität , dass sie für implementieren wollen StringList(in einem Erweiterungsmethode versteckt werden könnten, noch nicht oder historische hinzugefügt) , dass sie entweder nicht hinzufügen können List<string>von ihm ohne Erben, oder dass sie zu verschmutzen wollen nicht List<string>mit .

Alternativ möchten sie möglicherweise die Implementierung von StringListDown-the-Track ändern , haben also die Klasse gerade erst als Marker verwendet (normalerweise würden Sie hierfür Schnittstellen erstellen / verwenden, z IList. B. ).


Ich würde auch bemerken, dass Sie diesen Code in Irony gefunden haben, einem Sprachparser - eine ziemlich ungewöhnliche Art von Anwendung. Es kann einen anderen bestimmten Grund dafür geben, dass dies verwendet wurde.

Diese Beispiele zeigen, dass es legitime Gründe geben kann, eine solche Erklärung zu verfassen - auch wenn sie nicht allzu häufig ist. Wie bei allem, sollten Sie mehrere Optionen in Betracht ziehen und die für Sie jeweils beste auswählen.


Wenn Sie sich den Quellcode ansehen , scheint Option 3 der Grund zu sein - sie verwenden diese Technik, um Funktionen hinzuzufügen / Sammlungen zu spezialisieren:

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
quelle
1
Manchmal kann die Suche nach einer Vereinfachung von Deklarationen (oder einer Vereinfachung dessen, was auch immer) eher zu einer erhöhten Komplexität als zu einer verringerten Komplexität führen. Jeder, der die Sprache mäßig gut kennt, weiß, was eine Sprache List<T>ist, aber er muss den StringListKontext überprüfen, um zu verstehen, was sie ist. In Wirklichkeit heißt es einfach List<string>anders. In einem so einfachen Fall scheint dies wirklich keinen semantischen Vorteil zu haben. Sie ersetzen einfach einen bekannten Typ durch den gleichen Typ mit einem anderen Namen.
Craig
@Craig Ich bin damit einverstanden, wie aus meiner Erklärung zum Verwendungszweck (längere Erklärungen) und anderen möglichen Gründen hervorgeht.
NPSF3000,
-1

Ich würde sagen, es ist keine gute Praxis.

List<string>kommuniziert "eine generische Liste von Zeichenfolgen". Es List<string> gelten eindeutig Erweiterungsmethoden. Zum Verstehen ist weniger Komplexität erforderlich. es wird nur eine Liste benötigt.

StringListkommuniziert "ein Bedürfnis nach einer eigenen Klasse". Eventuell List<string> gelten Erweiterungsmethoden (prüfen Sie hierzu die tatsächliche Typimplementierung). Um den Code zu verstehen, ist mehr Komplexität erforderlich. Liste und die abgeleiteten Klassenimplementierungskenntnisse sind erforderlich.

Ruudjah
quelle
4
Erweiterungsmethoden gelten trotzdem.
Theodoros Chatzigiannakis
2
Natürlich. Aber Sie wissen es nicht, bis Sie die StringList inspizieren und sehen, dass sie von stammt List<string>.
Ruudjah
@TheodorosChatzigiannakis Ich frage mich, ob Ruudjah erläutern könnte, was er unter "Erweiterungsmethoden gelten nicht" versteht. Erweiterungsmethoden sind für jede Klasse möglich. Sicher, die .NET Framework-Bibliothek wird mit einer Vielzahl von Erweiterungsmethoden ausgeliefert, die unentgeltlich als LINQ bezeichnet werden und für die generischen Auflistungsklassen gelten. Das bedeutet jedoch nicht, dass Erweiterungsmethoden für keine anderen Klassen gelten. Vielleicht macht Ruudjah einen Punkt, der auf den ersten Blick nicht offensichtlich ist? ;-)
Craig
"Erweiterungsmethoden gelten nicht." Wo liest du das? Ich sage "Erweiterungsmethoden können
angewendet werden
1
Die Typ-Implementierung sagt Ihnen jedoch nicht, ob Erweiterungsmethoden angewendet werden oder nicht, oder? Allein die Tatsache , dass es sich um eine Klasse mittels Erweiterungsmethoden sind keine Anwendung. Jeder Konsument Ihres Typs kann Erweiterungsmethoden völlig unabhängig von Ihrem Code erstellen. Ich denke, das ist es, worauf ich hinaus will oder was ich frage. Sprechen Sie nur über LINQ? LINQ wird mithilfe von Erweiterungsmethoden implementiert . Das Fehlen von LINQ-spezifischen Erweiterungsmethoden für einen bestimmten Typ bedeutet jedoch nicht, dass Erweiterungsmethoden für diesen Typ nicht vorhanden sind oder nicht existieren. Sie können jederzeit von jedermann erstellt werden.
Craig