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

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

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 не лучше, чем нет { {X4}}?

Наследование от 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> или нет?

1641
Superbest 11 Фев 2014 в 07:01
114
Итак, действительно ли ваш вопрос, когда использовать наследование (квадрат - это прямоугольник, потому что всегда разумно предоставить квадрат, когда был запрошен общий прямоугольник) и когда использовать композицию (в FootballTeam есть список футболистов, кроме того к другим свойствам, которые являются для него столь же «фундаментальными», как имя)? Могли бы другие программисты запутаться, если бы в другом месте кода вы передали им команду FootballTeam, когда они ожидали простой List?
 – 
Flight Odyssey
11 Фев 2014 в 08:30
94
Что, если я скажу вам, что футбольная команда - это бизнес-компания, которая ВЛАДЕТСЯ списком игроков, которые заключают контракты, а не простым списком игроков с некоторыми дополнительными свойствами?
 – 
STT LCU
11 Фев 2014 в 13:17
98
Это действительно очень просто. Является ли футбольная команда составом игроков? Очевидно, нет, потому что, как вы говорите, есть и другие соответствующие свойства. ЕСТЬ ли в футбольной команде состав игроков? Да, значит, он должен содержать список. В остальном композиция предпочтительнее наследования в целом, потому что позже ее легче изменить. Если вы считаете наследование и композицию логичными одновременно, переходите к композиции.
 – 
Tobberoth
11 Фев 2014 в 19:19
52
@FlightOdyssey: Предположим, у вас есть метод SquashRectangle, который принимает прямоугольник, уменьшает его высоту вдвое и удваивает его ширину. Теперь передайте прямоугольник, который бывает имеет размер 4x4, но имеет тип rectangle, и квадрат размером 4x4. Каковы размеры объекта после вызова SquashRectangle? Для прямоугольника это явно 2x8. Если квадрат 2x8, то это уже не квадрат; если это не 2x8, то одна и та же операция с той же формой дает другой результат. Вывод таков, что изменяемый квадрат не разновидности прямоугольника.
 – 
Eric Lippert
12 Фев 2014 в 02:34
41
@Gangnus: Ваше утверждение просто ложно; реальный подкласс должен иметь функциональность, которая является надмножеством суперкласса. string требуется для того, чтобы делать все, что может object и многое другое .
 – 
Eric Lippert
13 Фев 2014 в 00:57

26 ответов

Лучший ответ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ОБНОВИТЬ

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

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

1766
Eric Lippert 20 Мар 2022 в 18:34
103
Хотя другие ответы были очень полезны, я думаю, что этот решает мои проблемы напрямую. Что касается преувеличения количества кода, вы правы, что в конечном итоге это не так уж и много работы, но я очень легко запутываюсь, когда включаю какой-то код в свою программу, не понимая, почему я включаю его.
 – 
Superbest
11 Фев 2014 в 10:19
141
@Superbest: Рад, что смог помочь! Вы правы, когда прислушиваетесь к этим сомнениям; если вы не понимаете, зачем пишете какой-то код, либо разберитесь в этом, либо напишите другой код, который вы понимаете.
 – 
Eric Lippert
11 Фев 2014 в 10:25
54
Но вы, кажется, забываете золотые правила оптимизации: 1) не делайте этого, 2) пока не делайте этого и 3) не делайте этого, не выполнив предварительно профили производительности, чтобы показать, что нужно оптимизировать.
 – 
AJMansfield
11 Фев 2014 в 16:58
124
@Mehrdad: Честно говоря, если ваше приложение требует, чтобы вы заботились о производительности, скажем, виртуальных методов, то любой современный язык (C #, Java и т. Д.) Не для вас. У меня есть ноутбук, на котором можно моделировать каждую аркадную игру, в которую я играл в детстве, одновременно . Это позволяет мне не беспокоиться об уровне косвенности здесь и там.
 – 
Eric Lippert
11 Фев 2014 в 18:37
47
@Superbest: Теперь мы определенно находимся в конце спектра "композиция, а не наследование". Ракета состоит из частей ракеты . Ракета - это не «особый список запчастей»! Я бы посоветовал вам урезать его дальше; почему список, а не IEnumerable<T> - последовательность ?
 – 
Eric Lippert
11 Фев 2014 в 22:15
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

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

People playing football

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

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

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

Image showing members (and other attributes) of the Manchester United team

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

168
Faither 25 Мар 2020 в 20:15
14
Я ожидал, что ваш второй пример - это футбольный клуб, а не футбольная команда. В футбольном клубе есть менеджеры, администраторы и т. Д. Из этого источника: "Футбольная команда - это коллектив имя, присвоенное группе игроков ... Такие команды могут быть выбраны для игры в матче против противоположной команды, чтобы представлять футбольный клуб ... ". Поэтому я считаю, что команда - это список игроков (или, точнее, набор игроков).
 – 
Ben
13 Апр 2015 в 20:13
4
Более оптимизированный private readonly List<T> _roster = new List<T>(44);
 – 
SiD
20 Июл 2016 в 15:24
2
Необходимо импортировать пространство имен System.Linq.
 – 
Davide Cannizzo
12 Дек 2017 в 18:18
1
На мой взгляд, ответ Nean Der Thal правильный. Футбольная команда состоит из руководителей (тренера, помощника и т. Д.), Игроков (и важное примечание: «игроки, выбранные для матча», потому что не все игроки в команде будут выбраны, например, для матча лиги чемпионов), администраторов и т. Д. . Футбольный клуб - это что-то вроде людей в офисе, на стадионе, болельщиков, правления клуба и, наконец, что не менее важно: несколько команд (например, первая команда, женская команда, молодежные команды и т. Д.). верьте всему, что говорит Википедия;)
 – 
E. Verdi
21 Окт 2020 в 09:25
@ E.Verdi Я думаю, что на втором снимке на заднем плане изображен стадион, и, как вы предположили, он выглядит так, будто представляет клуб, а не команду. В конце концов, определения этих терминов являются просто семантикой (то, что я мог бы назвать командой, другие могли бы назвать составом и т. Д.). Я думаю, дело в том, что то, как вы моделируете свои данные, зависит от того, для чего они будут использоваться. Это хороший момент, и я думаю, что фотографии помогают это показать :)
 – 
Ben
12 Ноя 2020 в 19:04

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

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

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

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

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

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

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

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

70
Satyan Raina 11 Фев 2014 в 21:05
8
Хорошая уловка в Списке против Сета. Это кажется слишком частой ошибкой. Когда в коллекции может быть только один экземпляр элемента, предпочтительным кандидатом для реализации должен быть какой-то набор или словарь. Это нормально, когда в команде два игрока с одинаковым именем. Неправильно включать одного игрока дважды одновременно.
 – 
Fred Mitchell
12 Фев 2014 в 01:26
1
+1 для некоторых хороших моментов, хотя более важно спросить «может ли структура данных разумно поддерживать все полезное (в идеале краткосрочное и долгосрочное)», а не «делает ли структура данных больше, чем полезно».
 – 
Tony Delroy
17 Фев 2014 в 14:26
1
Что ж, я поднимаю вопрос не о том, что нужно проверять, «делает ли структура данных больше, чем то, что полезно». Это необходимо для проверки «Если структура данных Parent делает что-то, что не имеет отношения к делу, бессмысленно или противоречит интуиции по отношению к поведению, которое может подразумевать класс Child».
 – 
Satyan Raina
17 Фев 2014 в 15:07
2
На самом деле существует проблема с получением нерелевантных характеристик, полученных от Родителя, поскольку во многих случаях он не прошел бы отрицательные тесты. Программист может расширить Human {eat (); запустить(); write ();} от гориллы {eat (); запустить(); swing ();} думая, что нет ничего плохого в том, что человек обладает дополнительной способностью качаться. А затем в игровом мире ваш человек внезапно начинает обходить все предполагаемые препятствия на суше, просто перелетая через деревья. Если явно не указано иное, практичный Человек не должен уметь качаться. Такой дизайн оставляет API очень открытым для злоупотреблений и запутывания.
 – 
Satyan Raina
17 Фев 2014 в 18:51
1
Я также не предлагаю, чтобы класс Player был производным от HashSet. Я предлагаю, чтобы класс Player «в большинстве случаев» реализовывался с использованием HashSet через Composition, и это полностью детали на уровне реализации, а не на уровне дизайна (вот почему я упомянул это в качестве примечания к моему ответу). Это вполне может быть реализовано с использованием списка, если есть веское обоснование для такой реализации. Итак, чтобы ответить на ваш вопрос, нужен ли поиск O (1) по ключу? Нет. Поэтому НЕ следует расширять Player из HashSet.
 – 
Satyan Raina
18 Фев 2014 в 14:07

Что, если у 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. X Y? или Имеет X Y?

  2. Означают ли имена моих классов то, что они есть?

54
Sam Leach 3 Апр 2014 в 14:59
Если тип каждого игрока классифицирует его как принадлежащий к DefensivePlayers, OffensivePlayers или OtherPlayers, было бы законно полезно иметь тип, который мог бы использоваться кодом, который ожидает List<Player>, но также включает элементы DefensivePlayers, OffsensivePlayers или SpecialPlayers типа IList<DefensivePlayer>, IList<OffensivePlayer> и IList<Player>. Можно использовать отдельный объект для кэширования отдельных списков, но инкапсуляция их в один и тот же объект, что и основной список, будет казаться чище [используйте аннулирование перечислителя списков ...
 – 
supercat
12 Сен 2014 в 00:48
... как признак того факта, что основной список изменился, и при следующем доступе к подспискам необходимо будет создать заново].
 – 
supercat
12 Сен 2014 в 00:48
2
Хотя я согласен с высказанной мыслью, мне действительно больно видеть, как кто-то дает совет по дизайну и предлагает раскрыть конкретный List с помощью общедоступного метода получения и установки в бизнес-объекте :(
 – 
sara
1 Фев 2016 в 12:56

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

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

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

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

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

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

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

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

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

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

Имеет ли для вас смысл footballTeam.Reverse()? footballTeam.ConvertAll<TOutput>() похоже на то, что вам нужно?

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

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

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

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

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

45
Toby Speight 27 Май 2021 в 10:39

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

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

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

37
Dmitry S. 11 Фев 2014 в 07:25

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

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

string team = new string();

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

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

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

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

34
Peter Mortensen 17 Фев 2014 в 05:09
2
Это хороший момент - можно думать о команде как о просто названии. Однако, если мое приложение нацелено на работу с действиями игроков, то это мышление немного менее очевидно. Во всяком случае, похоже, что в конечном итоге проблема сводится к композиции и наследованию.
 – 
Superbest
17 Фев 2014 в 00:34
6
Это достойный взгляд на это. В качестве примечания обратите внимание на следующее: богатый человек владеет несколькими футбольными командами, он дает одну другу в подарок, друг меняет название команды, увольняет тренера, и заменяет всех игроков. Команда друзей встречает мужскую команду на зеленом поле, и, поскольку мужская команда проигрывает, он говорит: «Не могу поверить, что вы побеждаете меня той командой, которую я вам дал!» человек прав? как бы вы это проверили?
 – 
user1852503
17 Фев 2014 в 09:59

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

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

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

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

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

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

Классы более сложные

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

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

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

IList<Player> footballTeam = ...

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

type FootballTeam = IList<Player>

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

31
Chad 26 Сен 2019 в 12:40

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

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

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

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

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}
29
Mauro Sampietro 11 Фев 2014 в 20:25
22
Это не объясняет почему . ОП знает, что так думают большинство других программистов, но он просто не понимает, почему это важно. Это только действительно предоставление информации, уже включенной в вопрос.
 – 
Servy
11 Фев 2014 в 20:48
2
Это первое, о чем я подумал, что его данные были просто неправильно смоделированы. Итак, причина в том, что ваша модель данных неверна.
 – 
rball
11 Фев 2014 в 21:11
3
Почему бы не наследовать из списка? Потому что ему не нужен более конкретный список.
 – 
Mauro Sampietro
13 Фев 2014 в 17:36
2
Не думаю, что второй пример верен. Вы открываете состояние и тем самым нарушаете инкапсуляцию. Состояние должно быть скрытым / внутренним по отношению к объекту, и способ изменить это состояние должен быть через общедоступные методы. Какие именно методы следует определять исходя из бизнес-требований, но это должно быть основано на принципе «это нужно прямо сейчас», а не на основе «может пригодиться когда-нибудь, я не знаю». Держите свой api плотно, и вы сэкономите время и силы.
 – 
sara
15 Сен 2015 в 18:28
2
Вопрос не в инкапсуляции. Я сосредотачиваюсь на том, чтобы не расширять тип, если вы не хотите, чтобы этот тип вел себя более определенным образом. Эти поля должны быть автосвойствами в C # и методами get / set на других языках, но здесь, в этом контексте, это совершенно излишне.
 – 
Mauro Sampietro
16 Сен 2015 в 10:46

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

OP в основном просит пояснить правила наследования от List<T>:

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

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

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

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

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

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

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

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

Зависит от контекста, но поскольку мы используем FootballTeam в качестве примера - представьте, что вы не можете добавить 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 делайте то, что хотите.

23
Mark Brackett 7 Апр 2015 в 00:35

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

myTeam.subList(3, 5);

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

20
Cruncher 12 Фев 2014 в 01:42
3
Может, если бы вы назвали это myTeam.subTeam(3, 5);
 – 
Sam Leach
15 Фев 2014 в 01:01
4
Если предположить, что это правда, тогда вам все равно понадобится композиция, а не наследование. Поскольку subList больше не будет даже возвращать Team.
 – 
Cruncher
18 Фев 2014 в 19:52

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

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

Когда вы наследуете от 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(...) изнутри.


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

20
Disillusioned 16 Фев 2014 в 00:05

Это зависит от поведения вашего "командного" объекта. Если он ведет себя так же, как коллекция, можно сначала представить его простым списком. Тогда вы можете начать замечать, что вы продолжаете дублировать код, повторяющийся в списке; на этом этапе у вас есть возможность создать объект FootballTeam, который обертывает список игроков. Класс FootballTeam становится домом для всего кода, который выполняет итерацию по списку игроков.

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

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

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

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

17
xpmatteo 11 Фев 2014 в 20:51

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

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

12
Paul J Abernathy 11 Фев 2014 в 18:56

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

11
QuentinUK 18 Фев 2014 в 06:53
1
Оглядываясь назад, после объяснения @EricLippert и других, вы действительно дали отличный ответ на часть моего вопроса, касающуюся API - в дополнение к тому, что вы сказали, если я сделаю class FootballTeam : List<FootballPlayers>, пользователи моего класса будут могу сказать, что я унаследовал от List<T>, используя отражение, видя методы List<T>, которые не имеют смысла для футбольной команды, и имея возможность использовать FootballTeam в List<T>, поэтому я бы раскрыл детали реализации клиенту (без надобности).
 – 
Superbest
18 Фев 2014 в 21:19

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

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

9
Shital Shah 16 Фев 2014 в 11:50

Ух ты, в твоем посте масса вопросов и замечаний. Большинство рассуждений, которые вы получаете от 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<T>//generic class
{
    // 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.
}
194
Nigel 16 Ноя 2021 в 19:09
5
@Brian: За исключением того, что вы не можете создать общий псевдоним, все типы должны быть конкретными. В моем примере List<T> расширен как частный внутренний класс, который использует дженерики для упрощения использования и объявления List<T>. Если бы расширение было просто class FootballRoster : List<FootballPlayer> { }, тогда да, я мог бы использовать вместо этого псевдоним. Думаю, стоит отметить, что вы можете добавить дополнительные элементы / функции, но они ограничены, поскольку вы не можете переопределить важные элементы List<T>.
 – 
myermian
11 Фев 2014 в 20:33
7
Если бы это были Programmers.SE, я бы согласился с текущим диапазоном голосов за ответы, но, поскольку это сообщение SO, этот ответ, по крайней мере, пытается ответить на конкретные проблемы C # (.NET), хотя есть определенные общие вопросы дизайна.
 – 
Mark Hurd
12 Фев 2014 в 06:10
15
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden Теперь это - хорошая причина не наследовать от List. В других ответах слишком много философии.
 – 
Navin
12 Фев 2014 в 07:54
2
Если вы вызовете List<T>.GetEnumerator(), вы получите структуру с именем Enumerator. Если вы вызовете IList<T>.GetEnumerator(), вы получите переменную типа IEnumerable<T>, которая содержит упакованную версию указанной структуры. В первом случае foreach будет вызывать методы напрямую. В последнем случае все вызовы должны виртуально отправляться через интерфейс, что делает каждый вызов медленнее. (На моей машине примерно в два раза медленнее.)
 – 
Jonathan Allen
20 Апр 2016 в 22:58
3
Вы полностью упускаете суть. foreach не заботится о том, реализует ли Enumerator интерфейс IEnumerable<T>. Если он сможет найти необходимые методы на Enumerator, он не будет использовать IEnumerable<T>. Если вы действительно декомпилируете код, вы увидите, как foreach избегает использования вызовов виртуальной диспетчеризации при итерации по List<T>, но будет с IList<T>.
 – 
Jonathan Allen
21 Апр 2016 в 02:39

Хотя у меня нет сложного сравнения, как в большинстве этих ответов, я хотел бы поделиться своим методом решения этой ситуации. Расширяя 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
{
    ...
}
5
Null511 20 Янв 2018 в 05:43

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

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

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

4
Alexey 27 Апр 2015 в 14:35
1
Здесь проблема не в множественном наследовании, миксинах (и т. Д.) Или в том, как бороться с их отсутствием. На любом языке, даже на человеческом, команда - это не список людей, а составленный из списка людей. Это абстрактное понятие. Если Бертран Мейер или кто-либо другой управляет командой, создавая подклассы, List поступает неправильно. Вы должны создать подкласс List, если вам нужен более конкретный вид списка. Надеюсь, ты согласен.
 – 
Mauro Sampietro
7 Июл 2015 в 16:57
1
Это зависит от того, какое у вас наследство. Сама по себе это абстрактная операция, это не означает, что два класса находятся в отношениях «особого типа», даже если это основная интерпретация. Однако наследование может рассматриваться как механизм повторного использования реализации, если дизайн языка поддерживает его, даже несмотря на то, что в настоящее время предпочтительной альтернативой является композиция.
 – 
Alexey
7 Июл 2015 в 22:12

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

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

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

3
Ivan Nikitin 16 Фев 2014 в 11:59

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

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

Возможно, вам подходит концепция игроков, делающих ставку на PlayerCollection?

public class PlayerCollection : Collection<Player> 
{ 
}

И тогда Футбольная команда может выглядеть так:

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();
}
3
Eniola 23 Ноя 2016 в 18:55

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

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

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

Получение из классов нарушает инкапсуляцию:

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

Помимо прочего, это затрудняет рефакторинг вашего кода.

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

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

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

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

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

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

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

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

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

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

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

Примечания об избежании Boilerplate

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

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

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

3
cdiggins 7 Окт 2018 в 08:01
1
Создание интерфейса в первую очередь предотвратит наследование «неправильных» классов, потому что тогда вы точно увидите, какое поведение вы ищете. Это была моя первая мысль.
 – 
cmxl
9 Авг 2020 в 11:15

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

Процитирую Эрика Липперта:

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

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

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

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
0
phoog 21 Май 2020 в 22:48

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

Один аспект отсутствует. Классы, унаследованные от List, нельзя правильно сериализовать с помощью XmlSerializer. В этом случае вместо этого следует использовать DataContractSerializer или необходима собственная реализация сериализации.
public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized
    // There is no error, the data is simply not there.
    string AnyPropertyInDerivedFromList { get; set; }     
}

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

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

Вместо этого используйте IList

Лично я бы не стал наследовать от List, но реализовал IList. Visual Studio сделает всю работу за вас и создаст полноценную рабочую версию. Посмотрите здесь: Как получить полную рабочую реализацию IList

10
marsh-wiggle 26 Апр 2021 в 13:15

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

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

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

Или

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

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

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

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

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

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

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

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

132
Peter Mortensen 8 Мар 2014 в 15:02
1
Если перейти к пункту 2, для List не имеет смысла иметь иметь-a List.
 – 
Cruncher
12 Фев 2014 в 17:47
Во-первых, вопрос не имеет ничего общего с композицией и наследованием. Ответ: OP не хочет реализовывать более конкретный вид списка, поэтому он не должен расширять List <>. Я запутался, потому что у вас очень высокие баллы по stackoverflow, и вы должны это четко знать, и люди доверяют тому, что вы говорите, поэтому к настоящему времени минимум 55 человек, которые проголосовали за и чьи идеи запутались, верят, что либо способ, либо другой приемлем для создания система, но это явно не так! # без ярости
 – 
Mauro Sampietro
9 Авг 2016 в 16:16
7
Он хочет поведения списка. У него есть два варианта: он может расширить список (наследование) или включить список в свой объект (композиция). Возможно, вы неправильно поняли часть вопроса или ответа, а не 55 человек, которые ошибались, а вы правы? :)
 – 
Tim B
9 Авг 2016 в 17:24
5
Да. Это вырожденный случай, когда только один супер и дополнительный, но я даю более общее объяснение его конкретного вопроса. У него может быть только одна команда, и у нее всегда может быть один список, но выбор по-прежнему состоит в том, чтобы унаследовать список или включить его в качестве внутреннего объекта. Как только вы начнете включать несколько типов команд (футбол, крикет и т. Д.), И они начнут быть чем-то большим, чем просто список игроков, вы увидите, как у вас есть полный состав и вопрос о наследовании. В подобных случаях важно смотреть на картину в целом, чтобы избежать неправильного выбора на раннем этапе, что впоследствии потребует значительного рефакторинга.
 – 
Tim B
10 Авг 2016 в 11:55
3
OP не запрашивал композицию, но вариант использования является четким примером проблемы X: Y, в которой один из 4 принципов объектно-ориентированного программирования нарушен, неправильно использован или не полностью понят. Более четкий ответ - либо написать специализированную коллекцию, такую ​​как Stack или Queue (которая, похоже, не соответствует варианту использования), либо понять композицию. Футбольная команда НЕ является списком футболистов. Независимо от того, запрашивал ли OP явно или нет, не имеет значения, без понимания абстракции, инкапсуляции, наследования и полиморфизма они не поймут ответ, следовательно, X: Y amok
 – 
Julia McGuigan
1 Фев 2018 в 20:42

Мой грязный секрет: мне все равно, что говорят люди, и я это делаю. .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 в вашем реальном варианте использования. Но не добавляйте ограничение, которого не должно быть. Если Microsoft не задокументировала причину, то это, безусловно, «лучшая практика», появившаяся из ниоткуда.

9
Peter Mortensen 17 Фев 2014 в 05:11
11
Хотя важно иметь смелость и инициативу, чтобы бросить вызов общепринятой мудрости, когда это уместно, я думаю, что было бы разумно сначала понять, почему принятая мудрость стала общепринятой, прежде чем приступать к ее оспариванию. -1 потому что «Не обращайте внимания на эту директиву!» не лучший ответ на вопрос «Почему существует это руководство?» Между прочим, сообщество не просто «обвинило меня» в этом случае, но предоставило удовлетворительное объяснение, которое меня убедило.
 – 
Superbest
12 Фев 2014 в 17:59
3
На самом деле, когда не дается никаких объяснений, ваш единственный способ - раздвинуть границы и проверить, не боясь нарушения. Теперь. Спросите себя, даже если List был плохой идеей. Насколько сложно было бы изменить наследование с List на Collection ? Предположение: меньше времени, чем размещение сообщений на форуме, независимо от длины вашего кода с помощью инструментов рефакторинга. Это хороший вопрос, но не практический.
 – 
Nicolas Dorier
12 Фев 2014 в 18:02
Чтобы уточнить: я, конечно, не жду благословения SO наследовать от всего, что я хочу, но я хочу этого, но суть моего вопроса заключалась в том, чтобы понять соображения, которые должны быть включены в это решение, и почему так много опытных программистов, похоже, имеют решил противоположное тому, что сделал я. Collection<T> не то же самое, что List<T>, поэтому может потребоваться довольно много работы для проверки в большом проекте.
 – 
Superbest
12 Фев 2014 в 18:31
2
team.ByName("Nicolas") означает "Nicolas" - название команды.
 – 
Sam Leach
15 Фев 2014 в 00:54
1
«не добавляйте ограничение, которое не должно существовать» В противоположность этому, не раскрывайте ничего, для чего у вас нет веских причин. Это не пуризм или слепое фанатизм, и не последняя тенденция дизайнерской моды. Это базовый объектно-ориентированный дизайн 101, начальный уровень. Ни для кого не секрет, почему существует это «правило», это результат нескольких десятилетий опыта.
 – 
sara
15 Сен 2015 в 18:45