Чи є доброю практикою успадкування від родових типів?


35

Чи краще використовувати List<string>в анотаціях типу чи StringListдеStringList

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

Я зіткнувся з декількома з них в Іронії .


Якщо ти не успадковуєш із родових типів, то навіщо вони там?
Mawg

7
@Mawg Вони можуть бути доступні лише для отримання інформації.
Теодорос Чатзіґянакікіс

3
Це одна з моїх найменш улюблених речей, яку варто побачити в коді
Кік

4
Гидота. Ні, серйозно. У чому смислова різниця між StringListі List<string>? Немає жодної, але вам (загальному "ви") вдалося додати ще одну деталь до кодової бази, яка є унікальною лише для вашого проекту і нічого не означає для інших, поки вони не вивчать код. Навіщо маскувати назву списку рядків, щоб просто називати його списком рядків? З іншого боку, якщо ви хотіли це зробити, BagOBagels : List<string>я думаю, що це має більше сенсу. Ви можете повторити це як ІНезмірно, зовнішні споживачі не піклуються про впровадження - доброта у всьому.
Крейг

1
У XAML є проблема, коли ви не можете використовувати дженерики, і вам потрібно було створити такий тип, щоб заповнити список
Daniel Little

Відповіді:


27

Є ще одна причина, чому ви можете успадкувати від родового типу.

Microsoft рекомендує уникати введення загальних типів у підписах методів .

Тоді добре створити типи з назвою бізнес-домену для деяких своїх дженериків. Замість того, щоб мати IEnumerable<IDictionary<string, MyClass>>, створіть тип, MyDictionary : IDictionary<string, MyClass>і тоді ваш член стає членом IEnumerable<MyDictionary>, що набагато читає. Це також дозволяє використовувати MyDictionary в ряді місць, збільшуючи явність коду.

Це може не здаватися величезною вигодою, але в діловому кодексі бізнесу я бачив генеричні засоби, які змусять ваше волосся стояти в кінці. Такі речі List<Tuple<string, Dictionary<int, List<Dictionary<string, List<double>>>>>. Сенс цього родового жодним чином не очевидний. Однак, якби вона була побудована з низкою явно створених типів, намір оригінального розробника, ймовірно, буде набагато зрозумілішим. Крім того, якщо цей родовий тип потрібно змінити з якоїсь причини, пошук і заміна всіх примірників цього родового типу може бути справжнім болем порівняно зі зміною його у похідному класі.

Нарешті, є ще одна причина, чому хтось може бажати створити свої власні типи, похідні від дженериків. Розглянемо наступні класи:

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>перейти в конструктор для MyConsumerClass? Якщо ми створюємо похідні класи class MyItems : ICollection<MyItems>і class MyItemCache : ICollection<MyItems>, MyConsumerClass потім може бути вельми специфічний , про яку з двох колекцій вона на насправді хоче. Потім це дуже добре працює з контейнером IoC, який може вирішити дві колекції дуже легко. Це також дозволяє програмісту бути більш точним - якщо є сенс MyConsumerClassпрацювати з будь-яким ICollection, вони можуть взяти загальний конструктор. Якщо, однак, ніколи не має сенсу використовувати бізнес один із двох похідних типів, розробник може обмежити параметр конструктора конкретним типом.

Підводячи підсумок, часто варто виводити типи із загальних типів - це дозволяє отримати більш чіткий код, де це доречно, та допомагає читати та ремонтувати.


12
Це абсолютно смішна рекомендація Microsoft.
Томас Едінг

7
Я не думаю, що це так смішно для публічних API. Вкладені дженерики важко зрозуміти з першого погляду, а більш значуща назва типу часто може сприяти читанню.
Стівен

4
Я не думаю, що це смішно, і це, звичайно, нормально для приватних API ... але для публічних API я не думаю, що це гарна ідея. Навіть Microsoft рекомендує приймати та повертати основні типи, коли ви пишете API, призначені для громадського споживання.
Пол д'Ауст

35

У принципі немає різниці між успадкуванням від родових типів і успадкуванням від будь-чого іншого.

Однак чому б ти не походив з будь-якого класу і нічого не додавав би далі?


17
Я згоден. Не вкладаючи нічого, я прочитав "StringList" і подумав собі: "Що це насправді робить?"
Ніл

1
Я також погоджуюсь, що в мовій мові для успадкування ви використовуєте ключове слово "extends", це гарне нагадування про те, що якщо ви збираєтеся успадкувати клас, то вам слід розширити його з подальшою функціональністю.
Елліот Блекберн

14
-1, оскільки не розуміти, що це лише один із способів створення абстракції шляхом вибору іншого імені - створення абстракцій може бути дуже вагомою причиною того, що нічого не додавати до цього класу.
Док Браун

1
Розгляньте свою відповідь, я переживаю деякі причини, які хтось може зважитися на це.
NPSF3000

1
"чому б на землі ви походили з будь-якого класу і нічого не додавали б далі?" Я зробив це для контролю користувачів. Ви можете створити загальний користувальницький елемент керування, але ви не можете використовувати його в дизайнері форм Windows, тому вам доведеться успадкувати його, вказавши тип, і використовувати цей.
Джордж Т

13

Я не дуже добре знаю C #, але я вважаю, що це єдиний спосіб реалізувати програму typedef, яку зазвичай використовують програмісти на C ++ з кількох причин:

  • Це не дозволяє вашому коду виглядати як море кутових дужок.
  • Це дозволяє вам замінювати різні базові контейнери, якщо ваші потреби пізніше змінюються, і дає зрозуміти, що ви залишаєте за собою право.
  • Це дозволяє надати типу ім’я, яке є більш релевантним у контексті.

Вони в основному відкинули останню перевагу, вибравши нудні, загальні назви, але дві інші причини все ще стоять.


7
Е-е ... Вони не зовсім однакові. У C ++ у вас є буквальний псевдонім. StringList цеList<string> - вони можуть використовуватися як взаємозамінні. Це важливо під час роботи з іншими API (або під час викриття вашого API), оскільки всі інші в світі використовують List<string>.
Теластин

2
Я усвідомлюю, що вони не зовсім те саме. Так само близько, як є. Звісно, ​​є слабкіші версії.
Карл Білефельдт

24
Ні, using StringList = List<string>;це найближчий C # еквівалент typedef. Підкласифікація на зразок цього запобіжить використанню будь-яких конструкторів з аргументами.
Піт Кіркхем

4
@PeteKirkham: визначення StringList, usingобмеживши його файлом вихідного коду, де usingрозміщено. З цієї точки зору успадкування ближче typedef. І створення підкласу не заважає додавати всі необхідні конструктори (хоча це може бути і нудно).
Док Браун

2
@ Random832: дозвольте мені висловити це по-іншому: у C ++ typedef можна використовувати для призначення імені в одному місці , щоб зробити його "єдиним джерелом істини". У C # usingне можна використовувати для цієї мети, але спадкування може. Це показує, чому я не погоджуюсь із схваленим коментарем Піта Кіркема - я не вважаю usingсправжньою альтернативою C ++ typedef.
Doc Brown

9

Я можу придумати хоча б одну практичну причину.

Оголошення ненародових типів, які успадковують від загальних типів (навіть не додаючи нічого), дозволяє посилатися на ці типи з місць, де вони інакше були б недоступні або були доступні більш суперечливо, ніж повинні бути.

Один із таких випадків - це додавання налаштувань через редактор налаштувань у Visual Studio, де, здається, доступні лише неполітичні типи. Якщо ви заїдете туди, то помітите, що всі System.Collectionsкласи доступні, але жодні колекції не System.Collections.Genericвідображаються як відповідні.

Інший випадок, коли інстанціювання типів через XAML у WPF. Немає підтримки для передачі аргументів типу в синтаксисі XML при використанні схеми до 2009 року. Це погіршується тим, що схема 2006 року, як видається, є типовою для нових проектів WPF.


1
У інтерфейсах веб-сервісу WCF ви можете використовувати цю техніку, щоб забезпечити кращі вигляди імен типів колекції (наприклад, PersonCollection замість ArrayOfPerson чи будь-чого іншого)
Rico Suter

7

Я думаю, вам потрібно заглянути в якусь історію .

  • C # використовується набагато довше, ніж це було для загальних типів.
  • Я очікую, що даний програмний продукт мав власний StringList в якийсь момент історії.
  • Це, можливо, було реалізовано, обгортаючи ArrayList.
  • Тоді хтось до рефакторингу переміщує базу коду вперед.
  • Але не хотів торкатися до 1001 різних файлів, тому закінчуй роботу.

В іншому випадку найкращі відповіді отримав Теодорос Чатзіґананакіс, наприклад, є ще багато інструментів, які не розуміють загальних типів. А може, навіть суміш його відповіді та історії.


3
"C # використовується набагато довше, ніж це було для загальних типів." Насправді, C # без дженериків існував близько 4 років (січень 2002 - листопад 2005), тоді як C # з дженериками зараз 9 років.
svick


4

У деяких випадках неможливо використовувати загальні типи, наприклад, ви не можете посилатися на загальний тип у XAML. У цих випадках ви можете створити неродовий тип, який успадковується від загального типу, який ви спочатку хотіли використовувати.

Якщо можливо, я б цього уникав.

На відміну від typedef, ви створюєте новий тип. Якщо ви використовуєте StringList як вхідний параметр методу, ваші абоненти не можуть передавати a List<string>.

Крім того, вам потрібно буде явно скопіювати всі конструктори, якими ви хочете скористатися, у прикладі StringList ви не могли сказати new StringList(new[]{"a", "b", "c"}), хоча List<T>визначає такий конструктор.

Редагувати:

Прийнята відповідь фокусується на профі, коли використовуються похідні типи у вашій моделі домену. Я погоджуюсь, що вони потенційно можуть бути корисними в цьому випадку, все ж я особисто уникав би їх навіть там.

Але вам слід (на мою думку) ніколи не використовувати їх у публічному API, тому що ви або ускладнюєте життя свого абонента, або втрачаєте продуктивність (я також не дуже люблю неявні перетворення взагалі).


3
Ви говорите: " Ви не можете посилатися на загальний тип у XAML ", але я не впевнений, на що ви маєте на увазі. Дивіться Generics у XAML
pswg

1
Це малося на увазі як приклад. Так, це можливо зі схемою XAML 2009 року, але AFAIK не зі схемою 2006 року, яка все ще є за замовчуванням VS.
Лукас Рігер

1
Ваш третій абзац - це лише наполовину вірно, ви можете насправді дозволити це за допомогою неявних перетворювачів;)
Knerd

1
@Knerd, правда, але тоді ви "неявно" створюєте новий об'єкт. Якщо ви не зробите набагато більше роботи, це також створить копію даних. Мабуть, це не має значення з невеликими списками, але веселіться, якщо хтось передає список з кількома тисячами елементів =)
Лукас Рігер

@LukasRieger ви маєте рацію: PI просто хотів зазначити: P
Knerd

2

Про те, що це варто, ось приклад того, як успадковування від загального класу використовується в компіляторі Roslyn від Microsoft, і навіть не змінюючи назву класу. (Мене це настільки спалахнуло, що я опинився тут, шукаючи, чи справді це можливо.)

У проекті CodeAnalysis ви можете знайти таке визначення:

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

Потім у проекті CSharpCodeanalysis є таке визначення:

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

Цей негенеричний клас PEModuleBuilder широко використовується в проекті CSharpCodeanalysis, і кілька класів цього проекту успадковують від нього прямо чи опосередковано.

А потім у проекті BasicCodeanalysis є таке визначення:

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

Оскільки ми можемо (сподіваємось) припустити, що Рослін були написані людьми з великими знаннями C # і як це слід використовувати, я думаю, що це рекомендація методики.


Так. А також вони можуть бути використані для уточнення пункту де безпосередньо при оголошенні.
Sentinel

1

Є три можливі причини, чому хтось спробував би це зробити вгорі голови:

1) Спростити декларації:

Звичайно , StringListі ListStringприблизно такі ж довжинами , але уявіть собі , замість того, щоб ви працюєте з , UserCollectionщо насправді , Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>>або яким - то іншим великим загальним прикладом.

2) Щоб допомогти AOT:

Іноді потрібно використовувати компіляцію Ahead Of Time, а не Just In Time Compilation - наприклад, розробку для iOS. Іноді компілятор AOT має труднощі розібратися у всіх загальних типах заздалегідь, і це може бути спробою надати йому підказку.

3) Щоб додати функціональність або усунути залежність:

Може бути , у них є певні функції , які вони хочуть реалізувати для StringList(може бути прихований в метод розширення, але не додав, або історичний) , що вони або не можуть відправляти повідомлення List<string>НЕ наслідуючи від нього, або що вони не хочуть забруднювати List<string>з .

Або, можливо, вони захочуть змінити реалізацію StringListнизхідної доріжки, тому щойно щойно використовували клас як маркер (зазвичай для цього ви робите / використовуєте інтерфейси, наприклад IList).


Я також зазначу, що ви знайшли цей код в Irony, який є мовним аналізатором - досить незвичним видом програми. Можливо, є якась інша конкретна причина того, що це було використано.

Ці приклади демонструють, що для написання такої декларації можуть бути законні причини - навіть якщо це не надто часто. Як і в чому завгодно, розгляньте кілька варіантів і виберіть найкращий для вас час.


Якщо ви подивитесь на вихідний код, можливо, причина 3 є причиною - вони використовують цю методику для додавання функціональності / спеціалізації колекцій:

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
Іноді прагнення спростити декларації (або спростити будь-яке) може призвести до збільшення складності, а не до зниження складності. Усі, хто добре знає мову, знають, що List<T>таке, але вони повинні перевірити StringListв контексті, щоб зрозуміти, що це таке. Насправді це просто List<string>, переходячи під іншою назвою. Насправді, схоже, немає жодної смислової переваги робити це в такому простому випадку. Ви насправді просто замінюєте відомий тип на той самий тип, який має інше ім’я.
Крейг

@Craig Я погоджуюся, як це відмічено моїм поясненням використання (довші декларації) та іншими можливими причинами.
NPSF3000

-1

Я б сказав, що це не дуже добра практика.

List<string>передає "загальний список рядків". Очевидно List<string> застосовуються методи розширення. Для розуміння потрібно менше складності; потрібен лише Список.

StringListповідомляє "потреба в окремому класі". Можливо, List<string> застосовуються методи розширення (перевірте для цього фактичну реалізацію типу). Більше складності потрібно для розуміння коду; Перелік та отримані знання щодо впровадження класу необхідні.


4
Методи розширення застосовуються в будь-якому випадку.
Теодорос Чатзіґянакікіс

2
Звісно. Але ви не знаєте, що доки не оглянете StringList і побачите, що це походить List<string>.
Рууджа

@TheodorosChatzigiannakis Мені цікаво, чи міг Рууджія детальніше пояснити, що він має на увазі, "методи розширення не застосовуються". Методи розширення - це можливість з будь-яким класом. Звичайно, бібліотека .NET Framework постачається з безліччю методів розширення, що називається LINQ, які застосовуються до загальних класів колекції, але це не означає, що методи розширення не застосовуються до будь-яких інших класів? Може бути, Рууджі зазначає, що на перший погляд не очевидно? ;-)
Крейг

"методи розширення не застосовуються." Де ти це читаєш? Я кажу: " Можуть застосовуватися методи розширення .
Рууджа,

1
Реалізація типу не підкаже, застосовуються чи ні методи розширення, правда? Справа в тому, що це клас означає, що застосовуються методи розширення . Будь-який споживач вашого типу може створити методи розширення повністю незалежно від вашого коду. Я здогадуюсь, що це я отримую або прошу. Ви просто говорите про LINQ? LINQ реалізується з використанням методів розширення. Але відсутність специфічних методів розширення LINQ для певного типу не означає, що методи розширення для цього типу не існують або не будуть. Їх міг створити будь-хто будь-коли.
Крейг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.