Почему бы не наследовать от List <T>?


1400

При планировании своих программ я часто начинаю с такой цепочки мыслей:

Футбольная команда - это просто список футболистов. Поэтому я должен представить это с:

var football_team = new List<FootballPlayer>();

Порядок в этом списке представляет порядок, в котором игроки перечислены в списке.

Но позже я понимаю, что у команд есть и другие свойства, помимо простого списка игроков, которые должны быть записаны. Например, промежуточные итоги в этом сезоне, текущий бюджет, унифицированные цвета, stringобозначение названия команды и т. Д.

Итак, я думаю:

Хорошо, футбольная команда похожа на список игроков, но, кроме того, у нее есть имя (а string) и промежуточный результат (а int). .NET не предоставляет класс для хранения футбольных команд, поэтому я создам свой собственный класс. Наиболее похожей и актуальной существующей структурой является List<FootballPlayer>, поэтому я унаследую от нее:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

Но оказывается, что руководство говорит, что вы не должны наследовать отList<T> . Я полностью смущен этим руководством в двух отношениях.

Почему бы нет?

Видимо Listкак-то оптимизировано для производительности . Как же так? Какие проблемы с производительностью я буду вызывать, если буду расширяться List? Что именно сломается?

Другая причина, которую я видел, заключается в том, что Listона предоставлена ​​Microsoft, и я не могу ее контролировать, поэтому я не могу изменить ее позже, выставив «публичный API» . Но я изо всех сил пытаюсь понять это. Что такое публичный API и почему меня это должно волновать? Если мой текущий проект не имеет и вряд ли когда-либо будет иметь этот публичный API, могу ли я смело игнорировать это руководство? Если я унаследую от вас List и выясню, что мне нужен публичный API, какие у меня будут трудности?

Почему это вообще имеет значение? Список есть список. Что может измениться? Что я мог бы хотеть изменить?

И, наконец, если Microsoft не хотела, чтобы я унаследовал List, почему они не сделали этот класс sealed?

Что еще я должен использовать?

Очевидно, для пользовательских коллекций Microsoft предоставила Collectionкласс, который должен быть расширен вместо List. Но этот класс очень прост и не имеет много полезных вещей, таких какAddRange , например. Ответ jvitor83 дает обоснование производительности для этого конкретного метода, но как медленный AddRangeне лучше, чем нет AddRange?

Наследование от Collection- это гораздо больше работы, чем наследование List, и я не вижу никакой выгоды. Конечно, Microsoft не сказала бы мне, чтобы я выполняла дополнительную работу без причины, поэтому я не могу избавиться от ощущения, что я как-то неправильно понимаю, а наследование Collectionна самом деле не является правильным решением для моей проблемы.

Я видел такие предложения, как реализация IList. Просто нет. Это десятки строк стандартного кода, которые мне ничего не дают.

Наконец, некоторые предлагают обернуть что- Listто в:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Есть две проблемы с этим:

  1. Это делает мой код излишне многословным. Теперь я должен позвонить, my_team.Players.Countа не просто my_team.Count. К счастью, с помощью C # я могу определить индексаторы, чтобы сделать индексирование прозрачным, и переслать все методы внутреннего List... Но это много кода! Что я получу за всю эту работу?

  2. Это просто не имеет никакого смысла. У футбольной команды нет «списка» игроков. Это является список игроков. Вы не говорите: «Джон Макфутбаллер присоединился к игрокам SomeTeam». Вы говорите: «Джон присоединился к SomeTeam». Вы не добавляете букву к «символам строки», вы добавляете букву к строке. Вы не добавляете книгу в книги библиотеки, вы добавляете книгу в библиотеку.

Я понимаю, что то, что происходит "под капотом", можно сказать, как "добавление X во внутренний список Y", но это кажется очень нелогичным способом мышления о мире.

Мой вопрос (резюмирован)

Каков правильный способ представления структуры данных на C #, который, «логически» (то есть «человеческому разуму»), представляет собой listвсего thingsлишь несколько наворотов?

Является ли наследование от List<T>всегда неприемлемым? Когда это приемлемо? Почему, почему нет? Что должен учитывать программист, когда решает, наследовать List<T>или нет?


90
Итак, действительно ли вы задаетесь вопросом, когда использовать наследование (квадрат - это прямоугольник, потому что всегда разумно указывать квадрат, когда запрашивается универсальный прямоугольник) и когда использовать композицию (кроме того, у FootballTeam есть список игроков-футболистов) к другим свойствам, которые столь же "фундаментальны" для него, как имя)? Будут ли другие программисты смущены, если в другом месте кода вы передадите им FootballTeam, когда они ожидают простой список?
Рейс Одиссея,

77
Что, если я скажу вам, что футбольная команда - это бизнес-компания, которая СОБИРАЕТ список контрактов с игроками вместо чистого списка игроков с некоторыми дополнительными свойствами?
STT LCU

75
Это действительно довольно просто. Футбольная команда - это список игроков? Очевидно, нет, потому что, как вы говорите, есть и другие важные свойства. Есть ли у футбольной команды список игроков? Да, поэтому он должен содержать список. Помимо этого, состав просто предпочтительнее наследования в целом, потому что его легче изменить позже. Если вы находите наследование и композицию логичными одновременно, переходите к композиции.
Тобберот

45
@FlightOdyssey: Предположим, у вас есть метод SquashRectangle, который принимает прямоугольник, делит его пополам и удваивает ширину. Теперь пройти в прямоугольнике , который случается быть 4x4 , но имеет тип прямоугольника и квадрата , который 4x4. Каковы размеры объекта после вызова SquashRectangle? Для прямоугольника ясно, что это 2x8. Если квадрат 2х8, то он больше не квадрат; если это не 2x8, то одна и та же операция с той же формой дает другой результат. Вывод состоит в том, что изменчивый квадрат не является своего рода прямоугольником.
Эрик Липперт

29
@Gangnus: Ваше утверждение просто ложно; реальный подкласс должен иметь функциональность, которая является надмножеством суперкласса. stringОбязан делать все ап objectможет сделать и больше .
Эрик Липперт

Ответы:


1567

Здесь есть несколько хороших ответов. Я бы добавил к ним следующие моменты.

Как правильно C # представлять структуру данных, которая, «логически» (то есть «человеческому разуму»), представляет собой просто список вещей с несколькими наворотами?

Попросите десять человек, не являющихся программистами, которые знакомы с существованием футбола, заполнить пробел:

Футбольная команда - это особый вид _____

Кто- нибудь сказал «список футболистов с несколькими наворотами», или все они сказали «спортивная команда», «клуб» или «организация»? Ваше представление о том, что футбольная команда - это особый список игроков, находится в вашем человеческом разуме и только в вашем человеческом разуме.

List<T>это механизм . Футбольная команда - это бизнес-объект, то есть объект, представляющий некоторую концепцию, которая находится в бизнес-сфере программы. Не смешивайте их! Футбольная команда - это своего рода команда; у него есть список, список - это список игроков . Список не является определенным списком игроков . Список - это список игроков. Так что сделайте свойство под названием Rosterэто List<Player>. И сделайте это, ReadOnlyList<Player>пока вы в этом, если вы не верите, что все, кто знает о футбольной команде, могут удалить игроков из реестра.

Является ли наследование от List<T>всегда неприемлемым?

Неприемлемо для кого? Меня? Нет.

Когда это приемлемо?

Когда вы создаете механизм, который расширяет List<T>механизм .

Что должен учитывать программист, когда решает, наследовать List<T>или нет?

Я строю механизм или бизнес-объект ?

Но это много кода! Что я получу за всю эту работу?

Вы потратили больше времени, набирая свой вопрос, что потребовалось бы вам, чтобы написать методы переадресации для соответствующих участников в List<T>пятьдесят раз. Вы явно не боитесь многословия, и мы говорим об очень небольшом количестве кода здесь; это несколько минут работы.

ОБНОВИТЬ

Я еще немного подумал, и есть еще одна причина, чтобы не называть футбольную команду списком игроков. На самом деле было бы плохой идеей моделировать футбольную команду как имеющую список игроков. Проблема с командой как / , имеющей список игроков , это то , что у вас есть это снимок команды в момент времени . Я не знаю, каково ваше экономическое обоснование для этого класса, но если бы у меня был класс, который представлял футбольную команду, я бы хотел задать ему такие вопросы, как «сколько игроков Seahawks пропустили игры из-за травм в период с 2003 по 2013 год?» или "У какого игрока из Денвера, который ранее играл за другую команду, был самый большой прирост в годовом исчислении?" или " Пиггеры прошли весь путь в этом году? "

То есть футбольная команда кажется мне хорошо смоделированной как совокупность исторических фактов, таких как, когда игрок был завербован, ранен, ушел в отставку и т. Д. Очевидно, что текущий список игроков является важным фактом, который, вероятно, должен быть передовым. центр, но могут быть и другие интересные вещи, которые вы хотите сделать с этим объектом, которые требуют более исторической перспективы.


84
Хотя другие ответы были очень полезны, я думаю, что этот ответ решает мои проблемы наиболее непосредственно. Что касается преувеличения количества кода, вы правы в том, что в конце концов это не так уж много работы, но я очень запутываюсь, когда включаю какой-то код в свою программу, не понимая, почему я его включаю.
Superbest

128
@Superbest: Рад, что смог помочь! Вы правы выслушать эти сомнения; если вы не понимаете, почему пишете какой-то код, либо разберитесь в этом, либо напишите другой код, который вы понимаете.
Эрик Липперт

48
@Mehrdad Но вы, кажется, забыли золотые правила оптимизации: 1) не делайте этого, 2) пока не делайте этого, и 3) не делайте этого, не выполнив сначала профили производительности, чтобы показать, что нужно оптимизировать.
AJMansfield

107
@Mehrdad: Честно говоря, если ваше приложение требует, чтобы вы заботились о бремени производительности, скажем, виртуальных методов, то любой современный язык (C #, Java и т. Д.) Не является языком для вас. Прямо здесь у меня есть ноутбук, на котором можно было симулировать каждую аркадную игру, в которую я играл в детстве одновременно . Это позволяет мне не беспокоиться об уровне косвенности здесь и там.
Эрик Липперт

44
@Superbest: Теперь мы определенно находимся в конце спектра «состав, а не наследование». Ракета состоит из частей ракеты . Ракета не является "особым списком деталей"! Я бы посоветовал вам урезать его дальше; почему список, в отличие от IEnumerable<T>- это последовательность ?
Эрик Липперт

161

Ух ты, в твоем посте целый ряд вопросов и моментов. Большая часть рассуждений, которые вы получаете от Microsoft, точно соответствует действительности. Давайте начнем со всего оList<T>

  • List<T> является оптимизированным. Его основное использование должно использоваться как частный член объекта.
  • Microsoft не запечатать его , потому что иногда вы можете создать класс , который имеет имя дружественное: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Теперь это так же просто, как и делать var list = new MyList<int, string>();.
  • CA1002: Не открывайте общие списки . По сути, даже если вы планируете использовать это приложение в качестве единственного разработчика, стоит развивать с хорошими практиками кодирования, чтобы они внушали вам и вашей второй натуре. Вам по-прежнему разрешается выставлять список в качестве, IList<T>если вам нужен какой-либо потребитель, чтобы иметь индексированный список. Это позволит вам позже изменить реализацию внутри класса.
  • Microsoft сделала Collection<T>очень общее, потому что это общее понятие ... название говорит само за себя; это просто коллекция. Есть более точные версии , такие как SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>и т.д. , каждый из которых реализуют , IList<T>но не List<T> .
  • Collection<T>позволяет переопределять элементы (т. е. добавлять, удалять и т. д.), поскольку они являются виртуальными. List<T>не.
  • Последняя часть вашего вопроса на месте. Футбольная команда - это больше, чем просто список игроков, поэтому это должен быть класс, содержащий этот список игроков. Думай, композиция против наследования . У футбольной команды есть список игроков (список), это не список игроков.

Если бы я писал этот код, класс мог бы выглядеть примерно так:

public class FootballTeam
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    // Yes. I used LINQ here. This is so I don't have to worry about
    // _roster.Length vs _roster.Count vs anything else.
    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

6
«В твоем посте полно вопросов » - ну, я же сказал, что был полностью сбит с толку. Спасибо за ссылку на вопрос @ Readonly, теперь я вижу, что большая часть моего вопроса является частным случаем более общей проблемы «Композиция против наследования».
Superbest

3
@ Брайан: За исключением того, что вы не можете создать универсальный, используя псевдоним, все типы должны быть конкретными. В моем примере List<T>это расширенный внутренний класс, который использует обобщения для упрощения использования и объявления List<T>. Если расширение было просто class FootballRoster : List<FootballPlayer> { }, тогда да, я мог бы использовать псевдоним вместо этого. Полагаю, стоит отметить, что вы можете добавить дополнительные элементы / функции, но это ограничено, поскольку вы не можете переопределить важные элементы List<T>.
мое

6
Если бы это был Programmers.SE, я бы согласился с текущим диапазоном голосов за ответы, но, поскольку это сообщение SO, этот ответ, по крайней мере, пытается ответить на конкретные вопросы C # (.NET), даже если есть определенные общие вопросы дизайна.
Марк Херд

7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overriddenТеперь , что это хорошая причина , чтобы не наследовать из списка. У других ответов слишком много философии.
Навин

10
@my: Я подозреваю, что вы имеете в виду "убить" вопросов, так как сыщик - детектив.
Бэтти

152
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

Предыдущий код означает: кучка парней с улицы играет в футбол, и у них есть имя. Что-то вроде:

Люди играют в футбол

Во всяком случае, этот код (из моего ответа)

public class FootballTeam
{
    // A team's name
    public string TeamName; 

    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

Значит: это футбольная команда, в которой есть менеджмент, игроки, администраторы и т. Д. Что-то вроде:

Изображение, показывающее членов (и другие атрибуты) команды Манчестер Юнайтед

Вот как ваша логика представлена ​​в картинках ...


9
Я ожидаю, что ваш второй пример - футбольный клуб, а не футбольная команда. У футбольного клуба есть менеджеры, администраторы и т. Д. Из этого источника : «Футбольная команда - это собирательное название, данное группе игроков ... Такие команды могут быть выбраны для игры в матче против команды соперника, для представления футбольного клуба». ... ". Так что я считаю, что команда - это список игроков (или, точнее, совокупность игроков).
Бен

3
Более оптимизированныйprivate readonly List<T> _roster = new List<T>(44);
SiD

2
Необходимо импортировать System.Linqпространство имен.
Давиде

1
Для визуальных изображений: +1
V.7

115

Это классический пример композиции против наследования .

В этом конкретном случае:

Является ли команда списком игроков с добавленным поведением?

или

Является ли команда собственным объектом, который содержит список игроков.

Расширяя список, вы ограничиваете себя несколькими способами:

  1. Вы не можете ограничить доступ (например, запретить людям менять реестр). Вы получаете все методы List независимо от того, нужны они вам или нет.

  2. Что произойдет, если вы хотите иметь списки других вещей, а также. Например, в командах есть тренеры, менеджеры, болельщики, экипировка и т. Д. Некоторые из них вполне могут быть списками сами по себе.

  3. Вы ограничиваете свои варианты для наследования. Например, вы можете создать универсальный объект Team, а затем использовать BaseballTeam, FootballTeam и т. Д., Которые наследуются от этого. Чтобы унаследовать от List, вам нужно сделать наследование от Team, но это означает, что все различные типы команд должны иметь одинаковую реализацию этого списка.

Композиция - включая объект, задающий поведение, которое вы хотите внутри вашего объекта.

Наследование - ваш объект становится экземпляром объекта с желаемым поведением.

Оба имеют свое применение, но это очевидный случай, когда состав предпочтительнее.


1
Расширяя # 2, для List нет смысла иметь-a List.
Cruncher

Во-первых, вопрос не имеет ничего общего с композицией против наследования. Ответ: OP не хочет реализовывать более конкретный вид списка, поэтому он не должен расширять List <>. Я запутался, потому что у вас очень высокий балл по стеку и вы должны это четко знать, и люди доверяют тому, что вы говорите, так что к настоящему моменту минимум 55 человек, которые проголосовали и чья идея смущены, верят, что так или иначе можно строить система, но ясно, что это не так! # no-rage
Мауро Сампьетро

6
@sam Он хочет, чтобы поведение списка. У него есть два варианта: он может расширить список (наследование) или включить список в свой объект (композицию). Возможно, вы неправильно поняли часть вопроса или ответа, а не 55 человек были неправы, и вы правы? :)
Тим Б

4
Да. Это вырожденный случай только с одним super и sub, но я даю более общее объяснение его конкретного вопроса. У него может быть только одна команда, и у него всегда может быть один список, но выбор остается наследовать список или включать его в качестве внутреннего объекта. Как только вы начинаете включать несколько типов команд (футбол. Крикет. И т. Д.), И они начинают представлять собой нечто большее, чем просто список игроков, вы видите, что у вас есть полный состав против вопроса о наследовании. В таких случаях важно смотреть на картину в целом, чтобы избежать неправильного выбора на раннем этапе, что впоследствии потребует большого количества рефакторинга.
Тим Б

3
OP не запрашивал Composition, но пример использования - это четкий пример проблемы X: Y, в которой один из 4 принципов объектно-ориентированного программирования нарушен, неправильно использован или не полностью понят. Более ясный ответ - либо написать специализированную коллекцию, такую ​​как Stack или Queue (которая не соответствует сценарию использования), либо понять состав. Футбольная команда НЕ является списком футболистов. Независимо от того, попросил ли OP об этом явно или нет, это не имеет значения, без понимания абстракции, инкапсуляции, наследования и полиморфизма они не поймут ответ, следовательно, X: Y amok
Джулия Макгиган

67

Как уже отмечалось, команда игроков - это не список игроков. Эту ошибку делают многие люди повсюду, возможно, на разных уровнях знаний. Часто проблема тонкая, а иногда и очень грубая, как в этом случае. Такие конструкции плохие, потому что они нарушают принцип подстановки Лискова . В Интернете есть много хороших статей, объясняющих эту концепцию, например, http://en.wikipedia.org/wiki/Liskov_substitution_principle

Таким образом, есть два правила, которые должны быть сохранены в отношениях Родитель / Ребенок среди классов:

  • Ребенок должен требовать не меньшей характеристики, чем то, что полностью определяет Родителя.
  • Родитель не должен требовать никаких характеристик в дополнение к тому, что полностью определяет Ребенка.

Другими словами, Родитель является необходимым определением ребенка, а ребенок - достаточным определением Родителя.

Вот способ обдумать свое решение и применить вышеупомянутый принцип, который должен помочь избежать такой ошибки. Следует проверить свою гипотезу, проверив, действительны ли все операции родительского класса для производного класса как структурно, так и семантически.

  • Является ли футбольная команда списком футболистов? (Все ли свойства списка применимы к команде в одном и том же значении)
    • Является ли команда набором однородных объектов? Да, команда это коллекция игроков
    • Описывает ли порядок включения игроков состояние команды и обеспечивает ли команда сохранение последовательности, если она не была явно изменена? Нет и нет
    • Ожидается ли, что игроки будут включены / выброшены в зависимости от их очередного положения в команде? нет

Как видите, только первая характеристика списка применима к команде. Следовательно, команда - это не список. Список будет подробным описанием того, как вы управляете своей командой, поэтому его следует использовать только для хранения объектов игрока и манипулирования методами класса Team.

На этом этапе я хотел бы отметить, что класс Team, по моему мнению, даже не должен быть реализован с использованием List; он должен быть реализован с использованием структуры данных Set (например, HashSet) в большинстве случаев.


8
Хороший улов в Списке против Сета. Это кажется ошибкой, слишком часто совершаемой. Когда в коллекции может быть только один экземпляр элемента, предпочтительным кандидатом для реализации должен быть какой-то набор или словарь. Можно иметь команду с двумя игроками с одинаковыми именами. Недопустимо, чтобы один игрок включался дважды одновременно.
Фред Митчелл

1
+1 для некоторых хороших моментов, хотя более важно спросить «может ли структура данных разумно поддерживать все полезное (в идеале краткосрочные и долгосрочные)», а не «делает ли структура данных больше, чем полезна».
Тони Делрой

1
@TonyD Ну, я хочу подчеркнуть, что не нужно проверять, «делает ли структура данных больше, чем то, что полезно». Он должен проверить «Если родительская структура данных делает что-то не относящееся к делу, бессмысленное или противоречащее поведению, которое может подразумевать класс Child».
Сатьян Райна

1
@TonyD На самом деле существует проблема, связанная с тем, что нерелевантные характеристики получены из Parent, поскольку во многих случаях он может не пройти отрицательные тесты. Программист может расширить Human {eat (); запустить(); write ();} из гориллы {eat (); запустить(); swing ();} думая, что в человеке нет ничего плохого в том, чтобы иметь возможность качаться. И затем в игровом мире ваш человек внезапно начинает обходить все предполагаемые препятствия на земле, просто качаясь над деревьями. Если явно не указано, практический Человек не должен быть в состоянии качаться. Такой дизайн оставляет API очень открытым для злоупотреблений и путаницы
Сатьян Райна

1
@TonyD Я не предлагаю, чтобы класс Player тоже был производным от HashSet. Я предлагаю, чтобы класс Player «в большинстве случаев» был реализован с использованием HashSet через Composition, и это полностью детализация уровня реализации, а не детали уровня проектирования (именно поэтому я упомянул его в качестве идентификатора моего ответа). Это вполне может быть реализовано с использованием списка, если есть обоснованное обоснование для такой реализации. Итак, чтобы ответить на ваш вопрос, нужно ли искать O (1) по ключу? Нет. Поэтому НЕ следует также расширять Player из HashSet.
Сатьян Райна

50

Что если у FootballTeamнего есть резервная команда вместе с основной командой?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
}

Как бы вы смоделировали это?

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

Отношения явно есть, а не есть .

или RetiredPlayers?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
    List<FootballPlayer> RetiredPlayers { get; set; }
}

Как правило, если вы хотите наследовать от коллекции, назовите класс SomethingCollection.

Ваш SomethingCollectionсемантически имеет смысл? Делайте это только если ваш тип является коллекцией Something.

В случае, если FootballTeamэто не звучит правильно. А Teamбольше, чем Collection. А Teamможет иметь тренеров, тренеров и т. Д., Как указали другие ответы.

FootballCollectionзвучит как коллекция футбольных мячей или, может быть, коллекция футбольных принадлежностей. TeamCollectionКоллекция команд.

FootballPlayerCollectionзвучит как набор игроков, который будет правильным именем для класса, который наследуется, List<FootballPlayer>если вы действительно хотите это сделать.

На самом деле List<FootballPlayer>это очень хороший тип для общения. Может быть, IList<FootballPlayer>если вы возвращаете его из метода.

В итоге

Спроси себя

  1. Есть ? или имеет ?XY XY

  2. Мои названия классов означают, что они?


Если тип каждого игрока будет классифицировать его как принадлежащие либо DefensivePlayers, OffensivePlayersили OtherPlayers, возможно , было бы законно полезно иметь тип , который может быть использован код , который ожидает , List<Player>но также включены члены DefensivePlayers, OffsensivePlayersили SpecialPlayersтипа IList<DefensivePlayer>, IList<OffensivePlayer>и IList<Player>. Можно использовать отдельный объект для кэширования отдельных списков, но инкапсуляция их в том же объекте, что и основной список, может показаться более чистой [использовать недействительность перечислителя списка ...
суперкат

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

2
Хотя я согласен с высказанным замечанием, я действительно разрываюсь на части, когда вижу, что кто-то дает советы по дизайну и предлагает раскрыть конкретный List <T> с помощью публичного метода получения и установки в бизнес-объекте :(
sara

38

Дизайн> Реализация

Какие методы и свойства вы выставляете - это дизайнерское решение. Базовый класс, от которого вы наследуете, является деталью реализации. Я чувствую, что стоит вернуться к прежнему.

Объект - это набор данных и поведения.

Итак, ваши первые вопросы должны быть:

  • Какие данные содержит этот объект в модели, которую я создаю?
  • Какое поведение этот объект проявляет в этой модели?
  • Как это может измениться в будущем?

Имейте в виду, что наследование подразумевает отношения «isa» (is a), тогда как композиция подразумевает отношение «hasa» (hasa). Выберите правильный вариант для вашей ситуации, принимая во внимание, куда все может пойти по мере развития вашего приложения.

Подумайте о мышлении в интерфейсах, прежде чем думать о конкретных типах, так как некоторым людям легче перевести свой мозг в «режим проектирования» таким образом.

Это не то, что каждый делает сознательно на этом уровне в повседневном программировании. Но если вы размышляете над темой такого рода, вы идете по водам. Осознание этого может быть освобождением.

Учитывайте особенности дизайна

Посмотрите на List <T> и IList <T> в MSDN или Visual Studio. Посмотрите, какие методы и свойства они выставляют. Все ли эти методы выглядят как то, что кто-то хотел бы сделать с FootballTeam по вашему мнению?

Имеет ли для вас смысл FootballTeam.Reverse ()? Выглядит ли footballTeam.ConvertAll <TOutput> () что-то, что вы хотите?

Это не вопрос с подвохом; ответ может быть действительно «да». Если вы реализуете / наследуете List <Player> или IList <Player>, вы застряли с ними; если это идеально подходит для вашей модели, сделайте это.

Если вы решите «да», это имеет смысл, и вы хотите, чтобы ваш объект обрабатывался как набор / список игроков (поведение), и поэтому вы хотите реализовать ICollection или IList, во что бы то ни стало. Номинально:

class FootballTeam : ... ICollection<Player>
{
    ...
}

Если вы хотите, чтобы ваш объект содержал коллекцию / список игроков (данных), и поэтому вы хотите, чтобы коллекция или список были свойством или членом, во что бы то ни стало. Номинально:

class FootballTeam ...
{
    public ICollection<Player> Players { get { ... } }
}

Вам может показаться, что вы хотите, чтобы люди могли только перечислять набор игроков, а не считать их, добавлять к ним или удалять их. IEnumerable <Player> - вполне допустимый вариант для рассмотрения.

Вы можете почувствовать, что ни один из этих интерфейсов вообще не полезен в вашей модели. Это менее вероятно (IEnumerable <T> полезен во многих ситуациях), но все же возможно.

Любой, кто пытается сказать вам, что одно из них категорически и категорически неверно в каждом случае, ошибается . Любой, кто пытается сказать вам, что он категорически и окончательно прав в любом случае, ошибается.

Перейти к реализации

Как только вы определились с данными и поведением, вы можете принять решение о реализации. Это включает в себя, какие конкретные классы вы зависите от наследования или композиции.

Это не может быть большим шагом, и люди часто объединяют дизайн и реализацию, так как вполне возможно пробежаться по голове за секунду или две и начать печатать.

Мысленный эксперимент

Искусственный пример: как уже упоминали другие, команда не всегда «просто» коллекция игроков. Поддерживаете ли вы коллекцию матчей для команды? Взаимозаменяема ли команда с клубом, в вашей модели? Если это так, и если ваша команда представляет собой набор игроков, возможно, это также набор сотрудников и / или набор очков. Тогда вы в конечном итоге:

class FootballTeam : ... ICollection<Player>, 
                         ICollection<StaffMember>,
                         ICollection<Score>
{
    ....
}

Несмотря на дизайн, на данный момент в C # вы все равно не сможете реализовать все это, унаследовав от List <T>, так как C # «only» поддерживает одиночное наследование. (Если вы пробовали эту малярию в C ++, вы можете считать это хорошей вещью.) Реализация одной коллекции с помощью наследования и одной с помощью композиции может показаться грязной. И такие свойства, как Count, приводят пользователей в замешательство, если вы не реализуете ILIst <Player> .Count и IList <StaffMember> .Count и т. Д. Явно, и тогда они просто болезненны, а не сбивают с толку. Вы можете видеть, куда это идет; интуитивное чувство, размышляя над этим проспектом, вполне может сказать вам, что неправильно идти в этом направлении (и правильно или неправильно, ваши коллеги могли бы также, если бы вы реализовали это таким образом!)

Короткий ответ (слишком поздно)

Указание о том, что не следует наследовать от классов коллекций, не относится к C #, вы найдете его во многих языках программирования. Получается мудрость, а не закон. Одна из причин заключается в том, что на практике считается, что композиция часто побеждает наследование с точки зрения понятности, реализуемости и ремонтопригодности. С объектами реального мира / домена чаще встречаются полезные и непротиворечивые отношения «hasa», чем полезные и непротиворечивые отношения «isa», если вы не углубляетесь в абстракцию, особенно когда проходит время и точные данные и поведение объектов в коде меняется. Это не должно заставлять вас всегда исключать наследование от классов коллекции; но это может быть наводящим на размышления.


34

Прежде всего, это связано с удобством использования. Если вы используете наследование, Teamкласс будет демонстрировать поведение (методы), которые предназначены исключительно для манипулирования объектами. Например, AsReadOnly()или CopyTo(obj)методы не имеют смысла для объекта команды. Вместо AddRange(items)метода вы, вероятно, захотите более описательный AddPlayers(players)метод.

Если вы хотите использовать LINQ, реализация общего интерфейса, такого как ICollection<T>или, IEnumerable<T>будет иметь больше смысла.

Как уже упоминалось, композиция - верный путь. Просто реализуйте список игроков как личную переменную.


30

Футбольная команда - это не список футболистов. Футбольная команда состоит из списка футболистов!

Это логически неправильно:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

и это правильно

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}

19
Это не объясняет почему . ОП знает, что большинство других программистов так думают, он просто не понимает, почему это важно. Это только действительно предоставляет информацию уже в вопросе.
Сервировка

2
Это первое, о чем я тоже подумала, как его данные были просто неправильно смоделированы. Так что причина в том, что ваша модель данных неверна.
rball

3
Почему бы не наследовать из списка? Потому что он не хочет более конкретный вид списка.
Мауро Сампьетро

2
Я не думаю, что второй пример верен. Вы выставляете состояние и тем самым нарушаете инкапсуляцию. Состояние должно быть скрыто / внутренне для объекта, и способ изменить это состояние должен быть открытыми методами. Какие именно методы следует определять исходя из требований бизнеса, но они должны основываться на принципах «нужно сейчас», а не «может быть, когда-нибудь, я не знаю». Держите свой API, и вы сэкономите время и силы.
Сара

1
Инкапсуляция не в этом вопрос. Я сосредоточен на том, чтобы не расширять тип, если вы не хотите, чтобы этот тип вел себя более определенным образом. Эти поля должны быть autoproperties в c # и методы get / set на других языках, но здесь, в этом контексте, это совершенно лишнее
Mauro Sampietro

28

Это зависит от контекста

Когда вы рассматриваете свою команду как список игроков, вы проецируете «идею» команды по мячу для ног на один аспект: вы сводите «команду» к людям, которых вы видите на поле. Эта проекция верна только в определенном контексте. В другом контексте это может быть совершенно неправильно. Представьте, что вы хотите стать спонсором команды. Так что вы должны поговорить с менеджерами команды. В этом контексте команда проецируется на список ее менеджеров. И эти два списка обычно не сильно совпадают. Другие контексты - это текущие против бывших игроков и т. Д.

Непонятная семантика

Таким образом, проблема с рассмотрением команды как списка ее игроков заключается в том, что ее семантика зависит от контекста и что она не может быть расширена при изменении контекста. Кроме того, трудно выразить, какой контекст вы используете.

Классы расширяемые

Когда вы используете класс только с одним членом (например IList activePlayers), вы можете использовать имя члена (и дополнительно его комментарий), чтобы сделать контекст понятным. Когда есть дополнительные контексты, вы просто добавляете дополнительного участника.

Занятия сложнее

В некоторых случаях создание дополнительного класса может оказаться излишним. Каждое определение класса должно быть загружено через загрузчик классов и будет кэшироваться виртуальной машиной. Это стоит вам времени выполнения и памяти. Если у вас очень специфический контекст, возможно, будет неплохо рассматривать футбольную команду в качестве списка игроков. Но в этом случае вы должны просто использовать IList класс, а не класс, производный от него.

Заключение / Соображения

Когда у вас есть очень специфический контекст, можно рассматривать команду как список игроков. Например, внутри метода вполне нормально написать:

IList<Player> footballTeam = ...

При использовании F # даже можно создать аббревиатуру типа:

type FootballTeam = IList<Player>

Но когда контекст шире или даже неясен, вы не должны этого делать. Это особенно актуально, когда вы создаете новый класс, контекст которого в будущем может быть использован, неясен. Предупреждающий знак - это когда вы начинаете добавлять дополнительные атрибуты в ваш класс (название команды, тренер и т. Д.). Это явный признак того, что контекст, в котором будет использоваться класс, не является фиксированным и изменится в будущем. В этом случае вы не можете рассматривать команду как список игроков, но вы должны смоделировать список игроков (активных, не травмированных и т. Д.) Как атрибут команды.


27

Позвольте мне переписать ваш вопрос. так что вы можете увидеть предмет с другой точки зрения.

Когда мне нужно представлять футбольную команду, я понимаю, что это в основном имя. Нравится: "Орлы"

string team = new string();

Потом я понял, что в командах тоже есть игроки.

Почему я не могу просто расширить строковый тип, чтобы он также содержал список игроков?

Ваша точка входа в проблему является произвольной. Попытайтесь думать , что это команда имеет (свойства), а не то , что она есть .

После того, как вы это сделаете, вы сможете увидеть, разделяет ли он свойства с другими классами. И подумай о наследовании.


1
Это хороший момент - о команде можно думать только как об имени. Однако, если мое приложение нацелено на работу с действиями игроков, то это мышление немного менее очевидно. Во всяком случае, кажется, что проблема сводится к композиции против наследования в конце концов.
Superbest

6
Это один из достойных способов взглянуть на это. В качестве примечания, пожалуйста, примите во внимание следующее: богатому человеку принадлежит несколько футбольных команд, он дает один другу в подарок, друг меняет название команды, увольняет тренера, и заменяет всех игроков. Команда друзей встречает мужскую команду на зеленом поле, и, когда мужская команда проигрывает, он говорит: «Я не могу поверить, что ты побеждаешь меня с командой, которую я тебе дал!» мужчина прав? как бы ты проверил это?
user1852503

20

Просто потому, что я думаю, что другие ответы в значительной степени отклоняются от того, является ли футбольная команда «есть» List<FootballPlayer>или «имеет» List<FootballPlayer>, которая действительно не отвечает на этот вопрос в письменном виде.

ФП, главным образом, просит уточнить руководящие принципы для наследования от List<T>:

Руководство говорит, что вы не должны наследовать от List<T>. Почему бы нет?

Потому List<T>что не имеет виртуальных методов. Это меньше проблем в вашем собственном коде, так как вы обычно можете переключить реализацию с относительно небольшой болью - но это может быть гораздо более серьезной сделкой в ​​публичном API.

Что такое публичный API и почему меня это должно волновать?

Публичный API - это интерфейс, который вы предоставляете сторонним программистам. Подумайте рамки кода. И помните, что упомянутыми руководящими принципами являются « Руководящие указания по разработке .NET Framework », а не « Руководящие указания по разработке приложений .NET ». Есть разница, и, вообще говоря, дизайн публичного API намного более строг.

Если мой текущий проект не имеет и вряд ли когда-либо будет иметь этот публичный API, могу ли я смело игнорировать это руководство? Если я унаследую от List и выясню, что мне нужен публичный API, какие трудности у меня будут?

Да, довольно. Возможно, вы захотите рассмотреть обоснование, стоящее за этим, чтобы посмотреть, применимо ли это к вашей ситуации, но если вы не создаете общедоступный API, вам не нужно особенно беспокоиться о проблемах API, таких как управление версиями (из которых это подмножество).

Если вы добавите общедоступный API в будущем, вам нужно будет либо абстрагировать свой API от своей реализации (не подвергая его List<T>непосредственному воздействию ), либо нарушить рекомендации с возможной будущей болью, которая влечет за собой.

Почему это вообще имеет значение? Список есть список. Что может измениться? Что я мог бы хотеть изменить?

Зависит от контекста, но поскольку мы используем FootballTeamв качестве примера - представьте, что вы не можете добавить a, FootballPlayerесли это приведет к тому, что команда пересечет верхний предел зарплаты. Возможный способ добавить что-то вроде:

 class FootballTeam : List<FootballPlayer> {
     override void Add(FootballPlayer player) {
        if (this.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
 }

Ах ... но вы не можете, override Addпотому что это не так virtual(по причинам производительности).

Если вы находитесь в приложении (что, по сути, означает, что вы и все ваши вызывающие программы компилируются вместе), то теперь вы можете перейти к использованию IList<T>и исправить любые ошибки компиляции:

 class FootballTeam : IList<FootballPlayer> {
     private List<FootballPlayer> Players { get; set; }

     override void Add(FootballPlayer player) {
        if (this.Players.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
     /* boiler plate for rest of IList */
 }

но если вы публично предоставили информацию третьим лицам, вы просто внесли критическое изменение, которое приведет к ошибкам компиляции и / или выполнения.

TL; DR - рекомендации для публичных API. Для частных API делай что хочешь.


18

Позволяет ли людям говорить

myTeam.subList(3, 5);

вообще имеет смысл? Если нет, то это не должно быть списком.


3
Это может произойти, если вы это называетеmyTeam.subTeam(3, 5);
Сэм Лич

3
@SamLeach Предполагая, что это правда, вам все равно понадобится композиция, а не наследование. Как subListдаже не вернется Team.
Cruncher

1
Это убедило меня больше, чем любой другой ответ. +1
FireCubez

16

Это зависит от поведения вашего «командного» объекта. Если он ведет себя так же, как коллекция, возможно, было бы неплохо сначала представить его простым списком. Затем вы можете начать замечать, что продолжаете дублировать код, который повторяется в списке; на данный момент у вас есть возможность создать объект FootballTeam, который оборачивает список игроков. Класс FootballTeam становится домом для всего кода, который повторяется в списке игроков.

Это делает мой код излишне многословным. Теперь я должен вызвать my_team.Players.Count, а не просто my_team.Count. К счастью, с помощью C # я могу определить индексаторы, чтобы сделать индексирование прозрачным, и переслать все методы внутреннего List ... Но это много кода! Что я получу за всю эту работу?

Инкапсуляция. Вашим клиентам не нужно знать, что происходит внутри FootballTeam. Для всех ваших клиентов это может быть реализовано путем просмотра списка игроков в базе данных. Им не нужно знать, и это улучшает ваш дизайн.

Это просто не имеет никакого смысла. У футбольной команды нет «списка» игроков. Это список игроков. Вы не говорите: «Джон Макфутбаллер присоединился к игрокам SomeTeam». Вы говорите: «Джон присоединился к SomeTeam». Вы не добавляете букву к «символам строки», вы добавляете букву к строке. Вы не добавляете книгу в книги библиотеки, вы добавляете книгу в библиотеку.

Точно :) Вы скажете footballTeam.Add (john), а не footballTeam.List.Add (john). Внутренний список не будет виден.


1
ОП хочет понять, как РЕАЛЬНОСТЬ МОДЕЛИ, но затем определяет футбольную команду как список футболистов (что концептуально неправильно). Это проблема. Любой другой аргумент вводит его в заблуждение по моему мнению.
Мауро Сампьетро

3
Я не согласен с тем, что список игроков неверен концептуально. Не обязательно. Как кто-то другой написал на этой странице: «Все модели неправильны, но некоторые полезны»
xpmatteo

Правильные модели трудно найти, после того, как они сделаны, они являются полезными. Если модель может быть использована неправильно, концептуально это неправильно. stackoverflow.com/a/21706411/711061
Мауро Сампьетро

15

Здесь есть много отличных ответов, но я хочу коснуться кое-чего, о чем я не упоминал: объектно-ориентированный дизайн - это расширение возможностей объектов .

Вы хотите инкапсулировать все свои правила, дополнительную работу и внутренние детали внутри соответствующего объекта. Таким образом, другие объекты, взаимодействующие с этим, не должны беспокоиться обо всем этом. Фактически, вы хотите пойти дальше и активно препятствовать тому, чтобы другие объекты обходили эти внутренние механизмы .

Когда вы наследуете List, все другие объекты могут видеть вас как список. Они имеют прямой доступ к методам добавления и удаления игроков. И вы потеряете свой контроль; например:

Предположим, вы хотите различить, когда игрок уходит, зная, вышел ли он в отставку, ушел в отставку или был уволен. Вы могли бы реализовать RemovePlayerметод, который принимает соответствующее входное перечисление. Однако, унаследовав от List, вы не смогли бы предотвратить прямой доступ к Remove, RemoveAllи даже Clear. В результате вы фактически лишили своего FootballTeamкласса силы .


Дополнительные мысли по поводу инкапсуляции ... Вы подняли следующую проблему:

Это делает мой код излишне многословным. Теперь я должен вызвать my_team.Players.Count, а не просто my_team.Count.

Вы правы, это было бы излишне многословным для всех клиентов, чтобы использовать вашу команду. Тем не менее, эта проблема очень мала по сравнению с тем фактом, что вы сталкивались List Playersсо всеми и каждому, поэтому они могут возиться с вашей командой без вашего согласия.

Вы продолжаете говорить:

Это просто не имеет никакого смысла. У футбольной команды нет «списка» игроков. Это список игроков. Вы не говорите: «Джон Макфутбаллер присоединился к игрокам SomeTeam». Вы говорите: «Джон присоединился к SomeTeam».

Вы ошибаетесь по поводу первого бита: отбросьте слово «список», и на самом деле очевидно, что в команде есть игроки.
Однако вы ударяете ноготь по голове вторым. Вы не хотите, чтобы клиенты звонили ateam.Players.Add(...). Вы хотите, чтобы они звонили ateam.AddPlayer(...). И ваша реализация будет (возможно, помимо прочего) вызываться Players.Add(...)внутри.


Надеюсь, вы сможете увидеть, насколько важна инкапсуляция для расширения возможностей ваших объектов. Вы хотите, чтобы каждый класс хорошо выполнял свою работу, не боясь вмешательства других объектов.


12

Какой правильный способ представления структуры данных в C # ...

Помните: «Все модели ошибочны, но некоторые полезны». - Джордж EP Box

Не существует «правильного пути», только полезного.

Выберите тот, который полезен для вас и / ваших пользователей. Вот и все. Развивайся экономно, не переусердствуй. Чем меньше кода вы пишете, тем меньше кода вам нужно будет отлаживать. (читайте следующие издания).

- Отредактировано

Мой лучший ответ будет ... это зависит. Наследование от List подвергло бы клиентов этого класса методам, которые могут быть недоступны, прежде всего потому, что FootballTeam выглядит как бизнес-объект.

- Издание 2

Я искренне не помню, на что я ссылался в комментарии «не переусердствуйте». Хотя я считаю, что мышление KISS является хорошим руководством, я хочу подчеркнуть, что наследование бизнес-класса от List создаст больше проблем, чем решит, из-за утечки абстракции .

С другой стороны, я считаю, что существует ограниченное количество случаев, когда полезно просто наследовать от List. Как я уже писал в предыдущем издании, это зависит. Ответ на каждый случай в значительной степени зависит от знаний, опыта и личных предпочтений.

Спасибо @kai за то, что помогли мне более точно обдумать ответ.


Мой лучший ответ будет ... это зависит. Наследование от List подвергло бы клиентов этого класса методам, которые могут быть недоступны, прежде всего потому, что FootballTeam выглядит как бизнес-объект. Я не знаю, должен ли я отредактировать этот ответ или опубликовать другой, есть идеи?
ArturoTena

2
Немного о том, что « наследование от a Listбудет подвергать клиентов этого класса методам, которые, возможно, не должны подвергаться », - именно то, что делает наследование от Listабсолютного нет-нет. После того, как они подвергаются воздействию, они постепенно подвергаются насилию и неправильному использованию с течением времени. Экономия времени благодаря «экономическому развитию» может легко привести к десятикратной экономии в будущем: отладке злоупотреблений и в конечном итоге рефакторингу для исправления наследства. Принцип YAGNI также может рассматриваться как смысл: вам не нужны все эти методы List, поэтому не подвергайте их воздействию.
Разочарован

3
«Не переусердствуйте. Чем меньше кода вы пишете, тем меньше кода вам нужно будет отлаживать». <- Я думаю, что это вводит в заблуждение. Инкапсуляция и компоновка по наследованию НЕ являются чрезмерной инженерией и не вызывают больше необходимости в отладке. Инкапсулируя, вы ОГРАНИЧИВАЕТЕ количество способов, которыми клиенты могут использовать (и злоупотреблять) ваш класс, и, таким образом, у вас меньше точек входа, которые требуют тестирования и проверки ввода. Наследование от того, Listчто это быстро и легко и, следовательно, приведет к уменьшению количества ошибок, является просто ошибкой, просто плохой дизайн и плохой дизайн приводят к гораздо большему количеству ошибок, чем «из-за инженерной мысли».
Сара

@ Кай, я согласен с вами во всех отношениях. Я искренне не помню, на что я ссылался в комментарии «не переусердствуйте». OTOH, я считаю, что есть ограниченное количество случаев, когда просто наследовать от List полезно. Как я писал в более позднем издании, это зависит. Ответ на каждый случай в значительной степени зависит от знаний, опыта и личных предпочтений. Как и все в жизни. ;-)
ArturoTena

11

Это напоминает мне о том, "Есть" против "имеет" компромисс. Иногда проще и имеет смысл наследовать напрямую от суперкласса. В других случаях имеет смысл создать автономный класс и включить класс, от которого вы бы унаследовали, в качестве переменной-члена. Вы все еще можете получить доступ к функциональности класса, но не привязаны к интерфейсу или другим ограничениям, которые могут возникнуть в результате наследования от класса.

Что ты делаешь? Как и во многих вещах ... это зависит от контекста. Руководство, которое я бы использовал, заключается в том, что для наследования от другого класса действительно должны быть отношения «есть». Так что, если вы пишете класс под названием BMW, он может унаследовать от Car, потому что BMW действительно является автомобилем. Класс Horse может наследоваться от класса Mammal, потому что лошадь в действительности является млекопитающим в реальной жизни, и любые функции Mammal должны иметь отношение к Horse. Но можете ли вы сказать, что команда - это список? Из того, что я могу сказать, не похоже, что команда действительно "является" списком. Так что в этом случае, я бы имел List в качестве переменной-члена.


9

Мой грязный секрет: мне все равно, что говорят люди, и я делаю это. .NET Framework распространяется с помощью «XxxxCollection» (пример UIElementCollection в верхней части моей головы).

Так что мне мешает сказать:

team.Players.ByName("Nicolas")

Когда я нахожу это лучше, чем

team.ByName("Nicolas")

Более того, мой PlayerCollection может использоваться другим классом, например, «Club» без какого-либо дублирования кода.

club.Players.ByName("Nicolas")

Лучшие практики вчерашнего дня, возможно, не завтрашние. За большинством лучших практик нет никаких оснований, большинство из них - это лишь широкое согласие сообщества. Вместо того, чтобы спрашивать сообщество, будет ли оно винить вас, когда вы это делаете, спросите себя, что является более читабельным и понятным?

team.Players.ByName("Nicolas") 

или

team.ByName("Nicolas")

В самом деле. У тебя есть сомнения? Теперь, возможно, вам нужно поиграть с другими техническими ограничениями, которые мешают вам использовать List <T> в вашем реальном случае использования. Но не добавляйте ограничение, которое не должно существовать. Если Microsoft не задокументировала причину, то это, безусловно, «лучшая практика», пришедшая из ниоткуда.


9
Хотя важно иметь смелость и инициативу, чтобы оспаривать принятую мудрость, когда это уместно, я думаю, что разумно сначала понять, почему принятая мудрость стала принятой в первую очередь, прежде чем приступить к ее оспариванию. -1 потому что «игнорировать это руководство!» не является хорошим ответом на вопрос «Почему существует это руководство?» Между прочим, сообщество не просто «обвиняло меня» в этом деле, но и дало удовлетворительное объяснение, которое убедило меня.
Superbest

3
На самом деле, когда никаких объяснений не дается, ваш единственный способ раздвинуть границу и проверить, не боясь нарушения. Сейчас же. Спросите себя, даже если Список <T> не был хорошей идеей. Сколько боли было бы изменить наследование от List <T> на Collection <T>? Предположение: меньше времени, чем размещение на форуме, независимо от длины вашего кода с помощью инструментов рефакторинга. Теперь это хороший вопрос, но не практический.
Николя Дориер

Чтобы уточнить: я, конечно, не жду, когда благословение SO унаследует то, что я хочу, как бы я этого не хотел, но смысл моего вопроса состоял в том, чтобы понять соображения, которые должны быть приняты в этом решении, и почему так много опытных программистов, кажется, имеют решил противоположность того, что я сделал. Collection<T>это не то же самое, List<T>что может потребоваться немало усилий для проверки в крупномасштабном проекте.
Superbest

2
team.ByName("Nicolas")означает "Nicolas"имя команды.
Сэм Лич

1
«не добавляйте ограничение, которое не должно существовать». Напротив, не показывайте ничего, у вас нет веских причин для разоблачения. Это не пуризм или слепое фанатизм, а также не последняя модная тенденция дизайна. Это базовый объектно-ориентированный дизайн 101, начальный уровень. Не секрет, почему существует это «правило», это результат нескольких десятилетий опыта.
Сара

8

В руководящих принципах говорится, что общедоступный API-интерфейс не должен раскрывать внутреннее дизайнерское решение о том, используете ли вы список, набор, словарь, дерево или что-то еще. «Команда» - это не обязательно список. Вы можете реализовать его в виде списка, но пользователи вашего публичного API должны использовать ваш класс по мере необходимости. Это позволяет вам изменить свое решение и использовать другую структуру данных, не затрагивая общедоступный интерфейс.


Оглядываясь назад, после объяснения @EricLippert и других, вы фактически дали отличный ответ на часть API моего вопроса - в дополнение к тому, что вы сказали, если я это сделаю class FootballTeam : List<FootballPlayers>, пользователи моего класса смогут сказать мне: Мы унаследовали от List<T>использования рефлексии, увидели List<T>методы, которые не имеют смысла для футбольной команды, и смогли использовать FootballTeamих List<T>, поэтому я бы раскрыл детали реализации клиенту (без необходимости).
Superbest

7

Когда они говорят, что они List<T>«оптимизированы», я думаю, они хотят иметь в виду, что у них нет таких функций, как виртуальные методы, которые немного дороже. Таким образом, проблема заключается в том, что, как только вы открываете доступ List<T>к общедоступному API , вы теряете возможность применять бизнес-правила или настраивать его функциональность позже. Но если вы используете этот унаследованный класс как внутренний в своем проекте (в отличие от потенциально уязвимого для тысяч ваших клиентов / партнеров / других команд в качестве API), то, возможно, все будет в порядке, если он экономит ваше время и вам нужна эта функциональность. дублировать. Преимущество наследования от этого List<T>состоит в том, что вы устраняете много глупого кода-обертки, который никогда не будет настроен в обозримом будущем. Также, если вы хотите, чтобы ваш класс явно имел точно такую ​​же семантику, что иList<T> для жизни ваших API, то это также может быть в порядке.

Я часто вижу множество людей, выполняющих тонны дополнительной работы только потому, что так говорит правило FxCop, или чей-то блог говорит, что это «плохая» практика. Много раз, это превращает код в дизайн паттерна странности. Как и в случае с большинством рекомендаций, относитесь к ним как к руководству, которое может иметь исключения.


4

Хотя у меня нет сложного сравнения, как большинство из этих ответов, я хотел бы поделиться своим методом для решения этой ситуации. Расширяя IEnumerable<T>, вы можете позволить вашему Teamклассу поддерживать расширения запросов Linq, не раскрывая публично все методы и свойства List<T>.

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}

3

Если пользователям вашего класса нужны все методы и свойства **, которые есть в List, вы должны извлечь из него свой класс. Если они им не нужны, приложите List и создайте оболочки для методов, которые действительно нужны вашим классным пользователям.

Это строгое правило, если вы пишете публичный API или любой другой код, который будет использоваться многими людьми. Вы можете игнорировать это правило, если у вас крошечное приложение и не более 2 разработчиков. Это сэкономит вам время.

Для крошечных приложений вы также можете выбрать другой, менее строгий язык. Ruby, JavaScript - все, что позволяет писать меньше кода.


3

Я просто хотел добавить, что Бертран Мейер, изобретатель Eiffel и дизайн по контракту, Teamунаследовал бы от List<Player>того, что стукнуло веко.

В своей книге « Построение объектно-ориентированного программного обеспечения» он обсуждает реализацию системы с графическим интерфейсом, в которой у прямоугольных окон могут быть дочерние окна. Он просто должен Windowнаследовать от обоих Rectangleи Tree<Window>повторно использовать реализацию.

Тем не менее, C # не является Eiffel. Последний поддерживает множественное наследование и переименование функций . В C # при создании подкласса вы наследуете как интерфейс, так и реализацию. Вы можете переопределить реализацию, но соглашения о вызовах копируются непосредственно из суперкласса. В Eiffel, однако, вы можете изменить имена общедоступных методов, так что вы можете переименовать Addи Removeв Hireи Fireв вашем Team. Если экземпляр Teamпередается обратно в List<Player>, вызывающая сторона будет использовать Addи Removeизменять его, но ваши виртуальные методы Hireи Fireбудут вызваны.


1
Здесь проблема не в множественном наследовании, миксинах (и т. Д.) Или в том, как бороться с их отсутствием. На любом языке, даже на человеческом языке, команда - это не список людей, а составленный из списка людей. Это абстрактное понятие. Если Бертран Мейер или кто-либо другой управляет командой, используя подклассы List, то он делает неправильно. Вы должны разделить список на подклассы, если вы хотите более конкретный вид списка. Надеюсь, вы согласны.
Мауро Сампьетро

1
Это зависит от того, что наследство для вас. Само по себе это абстрактная операция, это не значит, что два класса находятся в «особом виде» отношений, даже если это основная интерпретация. Однако наследование можно рассматривать как механизм повторного использования реализации, если языковой дизайн его поддерживает, хотя в настоящее время композиция является предпочтительной альтернативой.
Алексей

2

Я думаю, что я не согласен с вашим обобщением. Команда - это не просто набор игроков. У команды есть намного больше информации об этом - имя, эмблема, коллекция управленческого / административного персонала, коллекция тренерской команды, затем коллекция игроков. Таким образом, ваш класс FootballTeam должен иметь 3 коллекции, а не сам набор; если это правильно моделировать реальный мир.

Вы могли бы рассмотреть класс PlayerCollection, который, как и Specialized StringCollection, предлагает некоторые другие средства, такие как проверка и проверка перед добавлением или удалением объектов из внутреннего хранилища.

Возможно, понятие Better PlayerCollection подходит вашему предпочтительному подходу?

public class PlayerCollection : Collection<Player> 
{ 
}

И тогда FootballTeam может выглядеть так:

public class FootballTeam 
{ 
    public string Name { get; set; }
    public string Location { get; set; }

    public ManagementCollection Management { get; protected set; } = new ManagementCollection();

    public CoachingCollection CoachingCrew { get; protected set; } = new CoachingCollection();

    public PlayerCollection Players { get; protected set; } = new PlayerCollection();
}

2

Предпочитаю интерфейсы классам

Классы должны избегать наследования от классов и реализовывать минимально необходимые интерфейсы.

Наследование нарушает инкапсуляцию

Вывод из классов нарушает инкапсуляцию :

  • раскрывает внутренние детали о том, как ваша коллекция реализована
  • объявляет интерфейс (набор открытых функций и свойств), который может не подходить

Среди прочего это усложняет рефакторинг вашего кода.

Классы - это деталь реализации

Классы - это детали реализации, которые должны быть скрыты от других частей вашего кода. Короче говоря, это System.Listконкретная реализация абстрактного типа данных, которая может быть или не быть подходящей сейчас и в будущем.

Концептуально тот факт, что System.Listтип данных называется списком, является чем-то вроде красной селедки. A System.List<T>- это изменяемая упорядоченная коллекция, которая поддерживает амортизированные операции O (1) для добавления, вставки и удаления элементов, а также операции O (1) для получения количества элементов или получения и установки элемента по индексу.

Чем меньше интерфейс, тем более гибкий код

При проектировании структуры данных, чем проще интерфейс, тем более гибок код. Просто посмотрите, насколько мощный LINQ для демонстрации этого.

Как выбрать интерфейсы

Когда вы думаете «список», вы должны начать говорить себе: «Мне нужно представлять коллекцию бейсболистов». Допустим, вы решили смоделировать это с помощью класса. Сначала вы должны решить, какое минимальное количество интерфейсов необходимо предоставить этому классу.

Некоторые вопросы, которые могут помочь в этом процессе:

  • Нужно ли иметь счет? Если не рассмотреть вопрос о реализацииIEnumerable<T>
  • Будет ли эта коллекция меняться после ее инициализации? Если не считать IReadonlyList<T>.
  • Это важно, чтобы я мог получить доступ к элементам по индексу? РассматриватьICollection<T>
  • Важен ли порядок, в котором я добавляю предметы в коллекцию? Может быть, это ISet<T>?
  • Если вы действительно хотите это, тогда иди и реализуй IList<T>.

Таким образом, вы не будете связывать другие части кода с деталями реализации вашей коллекции игроков в бейсбол и будете свободны изменять ее реализацию, если вы уважаете интерфейс.

Используя этот подход, вы обнаружите, что код становится проще для чтения, рефакторинга и повторного использования.

Заметки о том, как избежать Boilerplate

Реализация интерфейсов в современной IDE должна быть простой. Щелкните правой кнопкой мыши и выберите «Интерфейс реализации». Затем перенаправьте все реализации в класс-член, если вам нужно.

Тем не менее, если вы обнаружите, что пишете много шаблонов, это возможно потому, что вы раскрываете больше функций, чем должны быть. Это та же самая причина, по которой вы не должны наследовать от класса.

Вы также можете разработать меньшие интерфейсы, которые имеют смысл для вашего приложения, и, возможно, просто пару вспомогательных функций расширения, чтобы сопоставить эти интерфейсы с любыми другими, которые вам нужны. Это подход, который я использовал в своем собственном IArrayинтерфейсе для библиотеки LinqArray .


2

Проблемы с сериализацией

Один аспект отсутствует. Классы, которые наследуются от List <>, не могут быть правильно сериализованы с помощью XmlSerializer . В этом случае вместо этого должен использоваться DataContractSerializer или необходима собственная реализация сериализации.

public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized:
    string AnyPropertyInDerivedFromList { get; set; }     
}

public class Demo
{
    // this properties will be seralized
    string AnyPropetyInDemo { get; set; }  
}

Дополнительная информация: Когда класс наследуется от List <>, XmlSerializer не сериализует другие атрибуты.


0

Когда это приемлемо?

Цитирую Эрика Липперта:

Когда вы создаете механизм, который расширяет List<T>механизм.

Например, вы устали от отсутствия AddRangeметода в IList<T>:

public interface IMoreConvenientListInterface<T> : IList<T>
{
    void AddRange(IEnumerable<T> collection);
}

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.