Это хорошая практика для наследования от универсальных типов?

35

Лучше использовать List<string>в аннотации типа или StringListгдеStringList

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

Я столкнулся с несколькими из них в Иронии .

Ruudjah
источник
Если вы не наследуете от универсальных типов, то почему они там?
Mawg
7
@ Mawg Они могут быть доступны только для создания экземпляров.
Теодорос Чатциганнакис
3
Это одна из моих наименее любимых вещей, которую можно увидеть в коде
Kik
4
Тьфу. Нет, серьезно. В чем семантическая разница между StringListи List<string>? Ничего такого нет, но вам (универсальному «вы») удалось добавить еще одну деталь в кодовую базу, которая уникальна только для вашего проекта и ничего не значит для кого-либо еще, пока они не изучат код. Зачем маскировать имя списка строк, чтобы продолжать называть его списком строк? С другой стороны, если вы хотите, BagOBagels : List<string>я думаю, это имеет больше смысла. Вы можете повторить это как IEnumerable, внешние потребители не заботятся о реализации - добро повсюду.
Крейг
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 #, но считаю, что это единственный способ реализовать a 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.
Док Браун
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 », но я не уверен, что вы имеете в виду. См. Обобщения в XAML
pswg
1
Это было задумано как пример. Да, это возможно для схемы XAML 2009 года, но AFAIK не для схемы 2006 года, которая по-прежнему используется по умолчанию для VS.
Лукас Ригер
1
Ваш третий абзац наполовину верен, вы можете разрешить это с неявными конвертерами;)
Knerd
1
@Knerd, правда, но тогда вы «неявно» создаете новый объект. Если вы не сделаете намного больше работы, это также создаст копию данных. Вероятно, не имеет значения с небольшими списками, но получайте удовольствие, если кто-то пропустит список с несколькими тысячами элементов =)
Лукас Ригер
@LukasRieger вы правы: PI просто хотел указать на это: P
Knerd
2

Для чего стоит, вот пример того, как наследование от универсального класса используется в компиляторе Microsoft Roslyn, даже без изменения имени класса. (Я был настолько сбит с толку этим, что попал сюда в поисках, чтобы увидеть, действительно ли это возможно.)

В проекте 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)

Поскольку мы можем (надеюсь) предположить, что Roslyn был написан людьми с обширными знаниями C # и как его следует использовать, я думаю, что это рекомендация метода.

RenniePet
источник
Да. А также они могут быть использованы для уточнения предложения where непосредственно при объявлении.
Сентинел
1

Есть три возможные причины, по которым кто-то может попытаться сделать это с моей головы:

1) Чтобы упростить декларации:

Конечно, StringListи ListStringони примерно одинаковой длины, но представьте, что вместо этого вы работаете с UserCollectionэтим на самом деле Dictionary<Tuple<string,Type>, IUserData<Dictionary,MySerializer>>или каким-то другим крупным общим примером.

2) Чтобы помочь AOT:

Иногда вам нужно использовать компиляцию Ahead Of Time, а не компиляцию Just In Time - например, разработку для 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; 
    }
NPSF3000
источник
1
Иногда стремление упростить декларации (или упростить что угодно) может привести к увеличению сложности, а не к снижению сложности. Каждый, кто знает язык в меру хорошо, знает, что это List<T>такое, но они должны проверять StringListконтекст, чтобы действительно понять, что это такое. На самом деле это просто List<string>другое имя. В таком простом случае, похоже, нет смысла в семантическом преимуществе. Вы на самом деле просто заменяете известный тип тем же типом с другим именем.
Крейг
@Craig Я согласен, как отмечено в моем объяснении варианта использования (более длинные объявления) и других возможных причин.
NPSF3000
-1

Я бы сказал, что это не очень хорошая практика.

List<string>сообщает "общий список строк". Очевидно, что List<string> применяются методы расширения. Требуется меньше сложности, чтобы понять; нужен только список.

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

Ruudjah
источник
4
Методы расширения применяются в любом случае.
Теодорос Чатзигианнакис
2
Конечно. Но вы не знаете, пока вы не осмотрите StringList и не увидите, откуда он происходит List<string>.
Рууджа
@TheodorosChatzigiannakis Интересно, сможет ли Рууджа уточнить, что он подразумевает под «методами расширения не применяются». Методы расширения возможны для любого класса. Конечно, библиотека .NET Framework поставляется с множеством расширенных методов, называемых LINQ, которые применяются к универсальным классам коллекций, но это не значит, что методы расширения не применяются ни к каким другим классам? Может быть, Рууджа делает вывод, который на первый взгляд неочевиден? ;-)
Крэйг
"методы расширения не применяются." Где ты это читаешь? Я говорю: « Могут применяться методы расширения .
Рууджа
1
Реализация типа не скажет вам, применяются ли методы расширения, правда? Просто тот факт , что это расширение средств класса метода действительно применяется. Любой потребитель вашего типа может создавать методы расширения полностью независимо от вашего кода. Я думаю, это то, к чему я клоню или спрашиваю. Вы просто говорите о LINQ? LINQ реализован с использованием методов расширения. Но отсутствие специфичных для LINQ методов расширения для определенного типа не означает, что методы расширения для этого типа не существуют или не будут. Они могут быть созданы в любое время кем угодно.
Крейг