Czy dobrą praktyką jest dziedziczenie po typach ogólnych?

35

Czy lepiej jest używać List<string>adnotacji typu lub StringListgdzie?StringList

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

Natknąłem się na kilka z nich w Irony .

Ruudjah
źródło
Jeśli nie dziedziczysz po typach ogólnych, to dlaczego one istnieją?
Mawg
7
@Mawg Mogą być dostępne tylko dla instancji.
Theodoros Chatzigiannakis,
3
To jedna z moich najmniej ulubionych rzeczy do zobaczenia w kodzie
Kik
4
Fuj Niepoważnie. Jaka jest różnica semantyczna między StringListi List<string>? Nie ma żadnego, ale udało ci się (ogólne „ty”) dodać jeszcze jeden szczegół do podstawy kodu, który jest unikalny tylko dla twojego projektu i nic nie znaczy dla nikogo innego, dopóki nie przestudiują kodu. Po co maskować nazwę listy ciągów, aby nadal nazywać ją listą ciągów? Z drugiej strony, jeśli chcesz to zrobić BagOBagels : List<string>, myślę, że ma to większy sens. Możesz iterować jako IEnumerable, zewnętrzni konsumenci nie dbają o wdrożenie - dobroć na okrągło.
Craig,
1
XAML ma problem polegający na tym, że nie można używać generycznych i trzeba było stworzyć taki typ, aby wypełnić listę
Daniel Little,

Odpowiedzi:

27

Jest jeszcze jeden powód, dla którego możesz chcieć dziedziczyć po typie ogólnym.

Microsoft zaleca unikanie zagnieżdżania typów ogólnych w podpisach metod .

Dlatego dobrym pomysłem jest utworzenie nazwanych nazw domen biznesowych dla niektórych ogólnych. Zamiast mieć IEnumerable<IDictionary<string, MyClass>>, utwórz typ, MyDictionary : IDictionary<string, MyClass>a wtedy członek stanie się IEnumerable<MyDictionary>, co jest o wiele bardziej czytelne. Pozwala także korzystać z MyDictionary w wielu miejscach, zwiększając jawność kodu.

Może to nie brzmieć jak ogromna korzyść, ale w prawdziwym kodzie biznesowym widziałem leki generyczne, które sprawią, że twoje włosy staną się koniec. Rzeczy lubią List<Tuple<string, Dictionary<int, List<Dictionary<string, List<double>>>>>. Znaczenie tego ogólnego nie jest w żaden sposób oczywiste. Jednak gdyby został skonstruowany z serii wyraźnie utworzonych typów, intencja oryginalnego programisty byłaby prawdopodobnie o wiele bardziej wyraźna. Co więcej, jeśli ten rodzaj ogólny musiałby się z jakiegoś powodu zmienić, znalezienie i zastąpienie wszystkich wystąpień tego typu ogólnego może być prawdziwym problemem, w porównaniu do zmiany go w klasie pochodnej.

Wreszcie istnieje jeszcze jeden powód, dla którego ktoś może chcieć stworzyć własne typy na podstawie generycznych. Rozważ następujące klasy:

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

Skąd wiemy, który ICollection<MyItems> przekazać do konstruktora MyConsumerClass? Jeśli tworzymy klas pochodnych class MyItems : ICollection<MyItems>i class MyItemCache : ICollection<MyItems>, MyConsumerClass może być wtedy bardzo wyraźnie powiedzieć, który z dwóch zbiorów faktycznie chce. To działa bardzo dobrze z kontenerem IoC, który może bardzo łatwo rozwiązać dwie kolekcje. Pozwala także programistom na dokładność - jeśli ma sens MyConsumerClasspraca z dowolnym ICollection, mogą wziąć ogólny konstruktor. Jeśli jednak użycie jednego z dwóch typów pochodnych nigdy nie ma sensu, deweloper może ograniczyć parametr konstruktora do określonego typu.

Podsumowując, często warto wyprowadzać typy z typów ogólnych - pozwala to na bardziej wyraźny kod tam, gdzie jest to stosowne i pomaga w czytelności i konserwacji.

Stephen
źródło
12
To absolutnie absurdalna rekomendacja Microsoft.
Thomas Eding,
7
Nie sądzę, że jest to śmieszne w przypadku publicznych interfejsów API. Zagnieżdżone rodzaje ogólne są trudne do zrozumienia na pierwszy rzut oka, a bardziej znacząca nazwa typu często może pomóc w zwiększeniu czytelności.
Stephen
4
Nie wydaje mi się, żeby to było śmieszne i na pewno jest w porządku dla prywatnych API ... ale dla publicznych API nie uważam, że to dobry pomysł. Nawet Microsoft zaleca przyjmowanie i zwracanie najróżniejszych typów podczas pisania interfejsów API przeznaczonych do publicznego użytku.
Paul d'Aoust,
35

Zasadniczo nie ma różnicy między dziedziczeniem po typach ogólnych a dziedziczeniem po czymkolwiek innym.

Dlaczego, u licha, miałbyś czerpać z jakiejkolwiek klasy i nie dodawać nic więcej?

Julia Hayward
źródło
17
Zgadzam się. Nie wnosząc nic, przeczytałbym „StringList” i pomyślałem sobie: „Co to naprawdę robi?”
Neil,
1
Zgadzam się również, że w majowym języku do dziedziczenia używasz słowa kluczowego „przedłuża”, jest to dobre przypomnienie, że jeśli zamierzasz odziedziczyć klasę, powinieneś rozszerzyć ją o dodatkowe funkcje.
Elliot Blackburn,
14
-1, ponieważ nie rozumiem, że jest to tylko jeden sposób na utworzenie abstrakcji poprzez wybranie innej nazwy - tworzenie abstrakcji może być bardzo dobrym powodem, aby nie dodawać niczego do tej klasy.
Dok. Brown
1
Rozważ moją odpowiedź. Podaję niektóre z powodów, dla których ktoś może zdecydować się to zrobić.
NPSF3000,
1
„dlaczego, do diabła, miałbyś czerpać z jakiejkolwiek klasy i nie dodawać nic więcej?” Zrobiłem to dla kontroli użytkowników. Możesz utworzyć ogólną kontrolę użytkownika, ale nie możesz jej używać w projektancie formularzy systemu Windows, więc musisz dziedziczyć po nim, określając typ i używać tego.
George T,
13

Nie znam dobrze C #, ale uważam, że jest to jedyny sposób na implementację typedef, z którego programiści C ++ zwykle korzystają z kilku powodów:

  • Dzięki temu kod nie wygląda jak morze nawiasów kątowych.
  • Umożliwia wymianę różnych pojemników, jeśli potrzeby zmienią się później, i daje do zrozumienia, że ​​zastrzegasz sobie do tego prawo.
  • Pozwala nadać typowi nazwę, która jest bardziej odpowiednia w kontekście.

Zasadniczo porzucili ostatnią przewagę, wybierając nudne, ogólne nazwy, ale pozostałe dwa powody pozostają.

Karl Bielefeldt
źródło
7
Ech ... Nie są takie same. W C ++ masz dosłowny alias. StringList jestList<string> - mogą one używane zamiennie. Jest to ważne podczas pracy z innymi interfejsami API (lub podczas udostępniania interfejsu API), ponieważ wszyscy inni na świecie go używają List<string>.
Telastyn
2
Zdaję sobie sprawę, że nie są dokładnie tym samym. Tak blisko, jak to możliwe. Słabsza wersja ma oczywiście wady.
Karl Bielefeldt,
24
Nie, using StringList = List<string>;jest najbliższym odpowiednikiem C # dla typedef. Takie podklasowanie zapobiegnie użyciu jakichkolwiek konstruktorów z argumentami.
Pete Kirkham,
4
@PeteKirkham: zdefiniowanie StringList przez usingograniczenie go do pliku kodu źródłowego, w którym usingjest on umieszczony. Z tego punktu widzenia dziedziczenie jest bliższe typedef. A utworzenie podklasy nie uniemożliwia dodania wszystkich potrzebnych konstruktorów (choć może to być uciążliwe).
Doc Brown
2
@ Random832: pozwól, że wyrażę to inaczej: w C ++ można użyć typedef do przypisania nazwy w jednym miejscu , aby uczynić ją „pojedynczym źródłem prawdy”. W języku C # usingnie można użyć do tego celu, ale dziedziczenie może. To pokazuje, dlaczego nie zgadzam się z pozytywnym komentarzem Pete'a Kirkhama - nie uważam go usingza prawdziwą alternatywę dla C ++ typedef.
Doc Brown,
9

Mogę wymyślić co najmniej jeden praktyczny powód.

Deklarowanie typów nieogólnych, które dziedziczą z typów ogólnych (nawet bez dodawania czegokolwiek), pozwala na odwoływanie się do tych typów z miejsc, w których inaczej byłyby niedostępne lub dostępne w bardziej skomplikowany sposób niż powinny.

Jednym z takich przypadków jest dodanie ustawień za pomocą edytora ustawień w programie Visual Studio, gdzie wydaje się, że dostępne są tylko typy inne niż ogólne. Jeśli tam pojedziesz, zauważysz, że wszystkie System.Collectionsklasy są dostępne, ale żadne zbiory z nie System.Collections.Genericwydają się odpowiednie.

Innym przypadkiem jest tworzenie instancji przez XAML w WPF. Nie ma obsługi przekazywania argumentów typu w składni XML podczas korzystania ze schematu sprzed 2009 roku. Sytuację pogarsza fakt, że schemat z 2006 r. Wydaje się być domyślny dla nowych projektów WPF.

Theodoros Chatzigiannakis
źródło
1
W interfejsach usługi sieci web WCF można użyć tej techniki, aby zapewnić lepiej wyglądające nazwy typów kolekcji (np. PersonCollection zamiast ArrayOfPerson lub cokolwiek innego)
Rico Suter
7

Myślę, że musisz zajrzeć do historii .

  • C # był używany znacznie dłużej niż miał ogólne typy.
  • Oczekuję, że dane oprogramowanie miało kiedyś StringList.
  • Mogło to zostać zaimplementowane przez zawinięcie ArrayList.
  • Potem ktoś do refaktoryzacji, aby przesunąć bazę kodu do przodu.
  • Ale nie chciałem dotykać 1001 różnych plików, więc dokończ zadanie.

W przeciwnym razie Theodoros Chatzigiannakis miał najlepsze odpowiedzi, np. Wciąż istnieje wiele narzędzi, które nie rozumieją rodzajów ogólnych. A może nawet połączenie jego odpowiedzi i historii.

Ian
źródło
3
„C # był używany znacznie dłużej niż miał typowe typy”. Nie bardzo, C # bez generics istniał przez około 4 lata (styczeń 2002 - listopad 2005), podczas gdy C # z generics ma teraz 9 lat.
svick,
4

W niektórych przypadkach nie można używać typów ogólnych, na przykład nie można odwoływać się do typu ogólnego w XAML. W takich przypadkach możesz utworzyć rodzaj ogólny, który dziedziczy z typu ogólnego, którego pierwotnie chciałeś użyć.

Jeśli to możliwe, unikałbym tego.

W przeciwieństwie do typedef, tworzysz nowy typ. Jeśli użyjesz StringList jako parametru wejściowego do metody, twoi rozmówcy nie będą mogli przekazać List<string>.

Ponadto musisz jawnie skopiować wszystkie konstruktory, których chcesz użyć, w przykładzie StringList nie możesz powiedzieć new StringList(new[]{"a", "b", "c"}), nawet jeśli List<T>definiuje takiego konstruktora.

Edytować:

Akceptowana odpowiedź skupia się na profesjonalistach, gdy używasz typów pochodnych w modelu domeny. Zgadzam się, że mogą być potencjalnie przydatne w tym przypadku, ale osobiście unikałbym ich nawet tam.

Ale nie należy (moim zdaniem) nigdy używać ich w publicznym interfejsie API, ponieważ albo utrudniasz życie dzwoniącemu, albo marnujesz wydajność (ja też tak naprawdę nie lubię niejawnych konwersji).

Lukas Rieger
źródło
3
Mówisz: „ nie możesz odwoływać się do typu ogólnego w XAML ”, ale nie jestem pewien, o czym mówisz. Zobacz Generics w XAML
pswg,
1
To miał być przykład. Tak, jest to możliwe ze schematem XAML 2009, ale AFAIK nie ze schematem 2006, który nadal jest domyślnym VS.
Lukas Rieger,
1
Twój trzeci akapit jest tylko w połowie prawdą, możesz to naprawdę
zaakceptować za
1
@Knerd, prawda, ale potem „domyślnie” tworzysz nowy obiekt. Jeśli nie wykonasz dużo więcej pracy, spowoduje to również utworzenie kopii danych. Prawdopodobnie nie ma to znaczenia przy małych listach, ale baw się dobrze, jeśli ktoś przejdzie listę z kilkoma tysiącami elementów =)
Lukas Rieger
@LukasRieger masz rację: PI chciał tylko zauważyć, że: P
Knerd
2

Dla tego, co jest warte, oto przykład, w jaki sposób dziedziczenie po klasie ogólnej jest używane w kompilatorze Roslyn Microsoftu, nawet bez zmiany nazwy klasy. (Byłem tak oszołomiony, że znalazłem się tutaj, szukając, czy to naprawdę możliwe).

W projekcie CodeAnalysis można znaleźć tę definicję:

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

Następnie w projekcie CSharpCodeanalysis istnieje następująca definicja:

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

Ta nietypowa klasa PEModuleBuilder jest szeroko stosowana w projekcie CSharpCodeanalysis, a kilka klas w tym projekcie dziedziczy po niej, bezpośrednio lub pośrednio.

A następnie w projekcie BasicCodeanalysis jest następująca definicja:

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

Ponieważ możemy (mam nadzieję) założyć, że Roslyn został napisany przez ludzi z rozległą wiedzą na temat języka C # i tego, jak należy go używać, myślę, że jest to zalecenie tej techniki.

RenniePet
źródło
Tak. Można je również wykorzystać do udoskonalenia klauzuli where bezpośrednio po zadeklarowaniu.
Sentinel,
1

Istnieją trzy możliwe powody, dla których ktoś próbowałby to zrobić z mojej głowy:

1) Aby uprościć deklaracje:

Jasne StringListi ListStringmają taką samą długość, ale wyobraź sobie, że pracujesz z UserCollectiontak naprawdę Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>>lub z innym dużym, ogólnym przykładem.

2) Aby pomóc AOT:

Czasami musisz używać kompilacji Ahead Of Time, a nie tylko kompilacji In Time - np. Programując dla iOS. Czasami kompilator AOT ma trudności z ustaleniem z wyprzedzeniem wszystkich rodzajów ogólnych, a może to być próba dostarczenia podpowiedzi.

3) Aby dodać funkcjonalność lub usunąć zależność:

Może mają określoną funkcjonalność, dla której chcą zaimplementować StringList(mogą być ukryte w metodzie rozszerzenia, jeszcze nie dodanej lub historycznej), do której albo nie mogą dodać List<string>bez dziedziczenia po niej, albo której nie chcą zanieczyszczaćList<string> z .

Alternatywnie mogą chcieć zmienić implementację w StringListdół ścieżki, więc na razie użyłem klasy jako znacznika (zwykle tworzysz / używasz do tego np IList. Interfejsów ).


Chciałbym również zauważyć, że ten kod został znaleziony w Irony, który jest parserem językowym - dość nietypowym rodzajem aplikacji. Może być jakiś inny konkretny powód, dla którego został użyty.

Te przykłady pokazują, że mogą istnieć uzasadnione powody, aby napisać takie oświadczenie - nawet jeśli nie jest to zbyt powszechne. Podobnie jak w przypadku innych elementów, zastanów się nad kilkoma opcjami i wybierz najlepszą dla siebie.


Jeśli spojrzysz na kod źródłowy , wydaje się, że powodem jest opcja 3 - używają tej techniki, aby dodać funkcjonalność / specjalizować kolekcje:

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
źródło
1
Czasami dążenie do uproszczenia deklaracji (lub uproszczenia czegokolwiek) może skutkować zwiększoną złożonością, a nie zmniejszoną złożonością. Każdy, kto zna język umiarkowanie dobrze, wie, co to List<T>jest, ale musi sprawdzić StringListw kontekście, aby faktycznie zrozumieć, co to jest. W rzeczywistości chodzi tylko List<string>o inne imię. Naprawdę nie wydaje się, aby robienie tego w tak prostym przypadku było jakikolwiek semantyczny. Naprawdę zastępujesz dobrze znany typ tym samym typem o innej nazwie.
Craig,
@Craig Zgadzam się, jak zauważono w moim wyjaśnieniu przypadku użycia (dłuższe deklaracje) i innych możliwych przyczyn.
NPSF3000,
-1

Powiedziałbym, że to nie jest dobra praktyka.

List<string>przekazuje „ogólną listę ciągów”. Oczywiście List<string> obowiązują metody rozszerzenia. Do zrozumienia potrzebna jest mniejsza złożoność; potrzebna jest tylko lista.

StringListkomunikuje „potrzebę odrębnej klasy”. Być może List<string> zastosowanie mają metody rozszerzenia (sprawdź faktyczną implementację typu). Potrzebna jest większa złożoność, aby zrozumieć kod; Wymagana jest znajomość implementacji listy i pochodnych klas.

Ruudjah
źródło
4
Mimo to obowiązują metody rozszerzenia.
Theodoros Chatzigiannakis,
2
Oczywiście. Ale nie wiesz tego, dopóki nie przejrzysz StringList i zobaczysz, że pochodzi List<string>.
Ruudjah,
@TheodorosChatzigiannakis Zastanawiam się, czy Ruudjah mógłby wyjaśnić, co rozumie przez „metody rozszerzenia nie mają zastosowania”. Metody rozszerzenia są możliwe dla dowolnej klasy. Jasne, biblioteka frameworku .NET jest dostarczana z wieloma metodami rozszerzenia zwanymi LINQ, które mają zastosowanie do ogólnych klas kolekcji, ale to nie znaczy, że metody rozszerzenia nie mają zastosowania do innych klas? Może Ruudjah mówi na pierwszy rzut oka coś, co nie jest oczywiste? ;-)
Craig,
„metody rozszerzenia nie mają zastosowania”. Gdzie to czytasz? Mówię: „ mogą obowiązywać metody rozszerzenia .
Ruudjah,
1
Jednak implementacja typu nie powie ci, czy mają zastosowanie metody rozszerzenia, prawda? Fakt, że jest to klasa, oznacza, że obowiązują metody rozszerzeń . Każdy konsument tego typu może tworzyć metody rozszerzenia całkowicie niezależnie od kodu. Chyba o to pytam. Mówisz tylko o LINQ? LINQ jest implementowany przy użyciu metod rozszerzenia. Ale brak metod rozszerzenia specyficznych dla LINQ dla określonego typu nie oznacza, że ​​metody rozszerzenia dla tego typu nie istnieją lub nie będą istnieć. Mogą być tworzone w dowolnym momencie przez każdego.
Craig