Планируя свои программы, я часто начинаю с такой цепочки мыслей:
Футбольная команда - это просто список футболистов. Поэтому я должен представить это следующим образом:
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
a>, например. ответ jvitor83 дает обоснование производительности для этого конкретного метода, но насколько медленный AddRange
не лучше, чем нет { {X4}}?
Наследование от Collection
требует больше усилий, чем наследование от List
, и я не вижу пользы. Конечно, Microsoft не посоветовала бы мне выполнять дополнительную работу без причины, поэтому я не могу избавиться от ощущения, что я что-то неправильно понимаю, и наследование Collection
на самом деле не является правильным решением моей проблемы.
Я видел такие предложения, как реализация IList
. Просто нет. Это десятки строк шаблонного кода, который мне ничего не дает.
Наконец, некоторые предлагают обернуть List
во что-нибудь:
class FootballTeam
{
public List<FootballPlayer> Players;
}
С этим есть две проблемы:
Это делает мой код излишне многословным. Теперь я должен позвонить
my_team.Players.Count
, а не простоmy_team.Count
. К счастью, с C # я могу определять индексаторы, чтобы сделать индексацию прозрачной, и пересылать все методы внутреннегоList
... Но это много кода! Что я получу за всю эту работу?Это просто не имеет никакого смысла. У футбольной команды нет списка игроков. Это список игроков. Вы не говорите: «Джон Макфутболлер присоединился к игрокам SomeTeam». Вы говорите: «Джон присоединился к SomeTeam». Вы не добавляете букву к «символам строки», вы добавляете букву к строке. Вы не добавляете книгу в книги библиотеки, вы добавляете книгу в библиотеку.
Я понимаю, что то, что происходит «под капотом», можно назвать «добавлением X во внутренний список Y», но это кажется очень противоречащим интуиции способом мышления о мире.
Мой вопрос (кратко)
Каков правильный способ C # представления структуры данных, которая «логически» (то есть «для человеческого разума») представляет собой всего лишь list
из things
с несколькими прибамбасами ?
Всегда ли наследование от List<T>
неприемлемо? Когда это приемлемо? Почему, почему нет? Что должен учитывать программист, решая, унаследовать от List<T>
или нет?
26 ответов
Здесь есть несколько хороших ответов. Я бы добавил к ним следующие моменты.
Каков правильный способ C # представления структуры данных, которая «логически» (то есть «для человеческого разума») представляет собой просто список вещей с несколькими прибамбасами?
Попросите десять человек, не являющихся программистами и знакомых с существованием футбола, заполнить пропуск:
Футбольная команда - это особый вид _____
кто-нибудь сказал "список футболистов с несколькими прибамбасами" или все сказали "спортивная команда", "клуб" или "организация"? Ваше представление о том, что футбольная команда — это особый список игроков, принадлежит вашему человеческому разуму, и только вашему человеческому разуму.
List<T>
- это механизм . Футбольная команда - это бизнес-объект , то есть объект, который представляет некоторую концепцию, относящуюся к бизнес-области программы. Не смешивайте их! Футбольная команда - это разновидность команды; он имеет состав, состав - это список игроков . Состав - это не особый вид списка игроков . Список - это список игроков. Итак, создайте свойство с именем Roster
, которое является List<Player>
. И сделайте это ReadOnlyList<Player>
, пока вы это делаете, если только вы не верите, что каждый, кто знает о футбольной команде, может удалить игроков из списка.
Всегда ли наследование от
List<T>
неприемлемо?
Неприемлемо для кого? Мне? Нет.
Когда это приемлемо?
Когда вы создаете механизм, который расширяет механизм List<T>
.
Что должен учитывать программист, решая, унаследовать от
List<T>
или нет?
Я создаю механизм или бизнес-объект?
Но это много кода! Что я получу за всю эту работу?
Вы потратили больше времени на ввод своего вопроса, которое вам потребовалось бы, чтобы написать методы пересылки для соответствующих участников List<T>
пятьдесят раз. Вы явно не боитесь многословия, и здесь мы говорим об очень небольшом объеме кода; это несколько минут работы.
ОБНОВИТЬ
Я еще немного подумал, и есть еще одна причина не моделировать футбольную команду как список игроков. На самом деле было бы плохой идеей моделировать футбольную команду как имеющую список игроков. Проблема с командой как со списком игроков заключается в том, что у вас есть моментальный снимок команды в момент времени. Я не знаю, каково ваше экономическое обоснование для этого класса, но если бы у меня был класс, представляющий футбольную команду, я бы хотел задать ему такие вопросы, как «сколько игроков «Сихокс» пропустили игры из-за травм в период с 2003 по 2013 год?» или «У какого игрока «Денвера», который ранее играл за другую команду, был самый большой прирост ярдов по сравнению с прошлым годом?» или "Прошли ли в этом году свиньи до конца?"
То есть, мне кажется, что футбольная команда хорошо смоделирована как сборник исторических фактов , например, когда игрок был нанят, получил травму, вышел на пенсию и т. Д. Очевидно, текущий состав игроков является важным фактом, который вероятно, должен быть в центре внимания, но могут быть другие интересные вещи, которые вы захотите сделать с этим объектом, которые требуют более исторической перспективы.
IEnumerable<T>
- последовательность ?
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.
}
Значит: это футбольная команда, в которой есть менеджмент, игроки, админы и т. Д. Что-то вроде:
Вот как ваша логика представлена на картинках ...
private readonly List<T> _roster = new List<T>(44);
System.Linq
.
Как все отметили, команда игроков - это не список игроков. Эту ошибку совершают многие люди повсюду, возможно, с разным уровнем знаний. Часто проблема тонкая, а иногда и очень серьезная, как в этом случае. Такие конструкции плохи, потому что они нарушают принцип замещения Лискова . В Интернете есть много хороших статей, объясняющих эту концепцию, например, http://en.wikipedia.org/wiki/Liskov_substitution_principle < / а>
Таким образом, существует два правила, которые должны быть сохранены в отношениях родитель / потомок между классами:
- Ребенок не должен требовать характеристики меньше, чем то, что полностью определяет Родителя.
- Родитель не должен требовать никаких дополнительных характеристик, кроме того, что полностью определяет Дитя.
Другими словами, Родитель - это необходимое определение дочернего элемента, а дочерний элемент - достаточное определение Родителя.
Вот способ продумать свое решение и применить вышеупомянутый принцип, который должен помочь избежать такой ошибки. Следует проверить свою гипотезу, проверив, все ли операции родительского класса допустимы для производного класса как структурно, так и семантически.
- Футбольная команда - это список футболистов? (Все ли свойства списка применяются к команде в одном и том же смысле)
- Является ли команда набором однородных объектов? Да, команда – это набор игроков.
- Описывает ли порядок включения игроков состояние команды, и гарантирует ли команда сохранение последовательности, если она явно не изменена? Нет и нет
- Предполагается ли, что игроки будут включаться/исключаться в зависимости от их последовательного положения в команде? Нет
Как видите, к команде применима только первая характеристика списка. Следовательно, команда - это не список. Список будет деталью реализации того, как вы управляете своей командой, поэтому его следует использовать только для хранения объектов игроков и манипулировать им с помощью методов класса Team.
Здесь я хотел бы отметить, что, на мой взгляд, класс Team не следует даже реализовывать с помощью List; в большинстве случаев он должен быть реализован с использованием структуры данных Set (например, HashSet).
Что, если у 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>
, если вы возвращаете его из метода.
В итоге
Спроси себя
X
Y
? или ИмеетX
Y
?Означают ли имена моих классов то, что они есть?
DefensivePlayers
, OffensivePlayers
или OtherPlayers
, было бы законно полезно иметь тип, который мог бы использоваться кодом, который ожидает List<Player>
, но также включает элементы DefensivePlayers
, OffsensivePlayers
или SpecialPlayers
типа IList<DefensivePlayer>
, IList<OffensivePlayer>
и IList<Player>
. Можно использовать отдельный объект для кэширования отдельных списков, но инкапсуляция их в один и тот же объект, что и основной список, будет казаться чище [используйте аннулирование перечислителя списков ...
Дизайн> Реализация
Какие методы и свойства вы выставляете - это дизайнерское решение. От того, от какого базового класса вы наследуете, зависит реализация. Я считаю, что стоит сделать шаг назад к первому.
Объект - это набор данных и поведения.
Итак, ваши первые вопросы должны быть такими:
- Какие данные содержит этот объект в создаваемой мной модели?
- Какое поведение этот объект демонстрирует в этой модели?
- Как это может измениться в будущем?
Имейте в виду, что наследование подразумевает отношение «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», если только вы не глубоко вникаете в абстрактные, особенно когда проходит время и точные данные и поведение объектов в коде изменения. Это не должно заставлять вас всегда исключать наследование от классов коллекции; но это может показаться наводящим на размышления.
Прежде всего, это связано с удобством использования. Если вы используете наследование, класс Team
будет предоставлять поведение (методы), которые разработаны исключительно для манипулирования объектами. Например, методы AsReadOnly()
или CopyTo(obj)
не имеют смысла для объекта группы. Вместо метода AddRange(items)
вам, вероятно, понадобится более описательный метод AddPlayers(players)
.
Если вы хотите использовать LINQ, реализация универсального интерфейса, такого как ICollection<T>
или IEnumerable<T>
, будет иметь больше смысла.
Как уже упоминалось, композиция - это правильный путь. Просто реализуйте список игроков как частную переменную.
Позвольте мне переписать ваш вопрос. так что вы можете увидеть объект с другой точки зрения.
Когда мне нужно представлять футбольную команду, я понимаю, что это в основном имя. Нравится: "Орлы"
string team = new string();
Позже я понял, что в командах тоже есть игроки.
Почему я не могу просто расширить строковый тип, чтобы он также содержал список игроков?
Ваша точка входа в проблему произвольна. Постарайтесь подумать, что есть (свойства) у команды, а не то, что она есть .
После того, как вы это сделаете, вы сможете увидеть, разделяет ли он свойства с другими классами. И подумайте о наследовании.
Это зависит от контекста
Когда вы рассматриваете свою команду как список игроков, вы сводите «идею» футбольной команды к одному аспекту: вы сокращаете «команду» до людей, которых видите на поле. Этот прогноз верен только в определенном контексте. В другом контексте это могло бы быть совершенно неправильно. Представьте, что вы хотите стать спонсором команды. Так что вам нужно поговорить с менеджерами команды. В этом контексте команда проецируется в список ее менеджеров. И эти два списка обычно не очень сильно пересекаются. Другие контексты - это текущие игроки против бывших игроков и т. Д.
Неясная семантика
Таким образом, проблема с рассмотрением команды как списка ее игроков заключается в том, что ее семантика зависит от контекста и не может быть расширена при изменении контекста. Кроме того, трудно выразить, какой контекст вы используете.
Классы расширяемы
Когда вы используете класс только с одним членом (например, IList activePlayers
), вы можете использовать имя члена (и дополнительно его комментарий), чтобы прояснить контекст. Когда есть дополнительные контексты, вы просто добавляете дополнительного члена.
Классы более сложные
В некоторых случаях создание дополнительного класса может оказаться излишним. Каждое определение класса должно быть загружено через загрузчик классов и будет кэшировано виртуальной машиной. Это приводит к потере производительности и памяти во время выполнения. Когда у вас очень конкретный контекст, можно рассматривать футбольную команду как список игроков. Но в этом случае вам действительно нужно просто использовать IList
, а не производный от него класс.
Заключение / соображения
Когда у вас очень конкретный контекст, нормально рассматривать команду как список игроков. Например, внутри метода вполне нормально написать:
IList<Player> footballTeam = ...
При использовании F # можно даже создать аббревиатуру типа:
type FootballTeam = IList<Player>
Но когда контекст более широкий или даже неясный, вам не следует этого делать. Это особенно актуально, когда вы создаете новый класс, чей контекст, в котором он может использоваться в будущем, не ясен. Предупреждающий знак - когда вы начинаете добавлять в свой класс дополнительные атрибуты (название команды, тренера и т. Д.). Это явный признак того, что контекст, в котором будет использоваться класс, не фиксирован и в будущем изменится. В этом случае вы не можете рассматривать команду как список игроков, но вы должны смоделировать список игроков (активных, не травмированных и т. Д.) Как атрибут команды.
Футбольная команда не список футболистов. Футбольная команда состоит из списка футболистов!
Это логически неверно:
class FootballTeam : List<FootballPlayer>
{
public string TeamName;
public int RunningTotal
}
И это правильно:
class FootballTeam
{
public List<FootballPlayer> players
public string TeamName;
public int RunningTotal
}
Просто потому, что я думаю, что другие ответы в значительной степени зависят от того, является ли футбольная команда "является" 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 делайте то, что хотите.
Позволяет ли людям говорить
myTeam.subList(3, 5);
Вообще имеет смысл? Если нет, то это не должен быть список.
myTeam.subTeam(3, 5);
subList
больше не будет даже возвращать Team
.
Здесь есть много отличных ответов, но я хочу затронуть кое-что, о чем я не упоминал: объектно-ориентированный дизайн - это расширение возможностей объектов .
Вы хотите заключить все свои правила, дополнительную работу и внутренние детали в соответствующий объект. Таким образом, другим объектам, взаимодействующим с этим, не нужно обо всем этом беспокоиться. Фактически, вы хотите пойти дальше и активно предотвратить обход этих внутренних объектов другими объектами.
Когда вы наследуете от 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(...)
изнутри.
Надеюсь, вы понимаете, насколько важна инкапсуляция для расширения возможностей ваших объектов. Вы хотите, чтобы каждый класс хорошо выполнял свою работу, не опасаясь вмешательства со стороны других объектов.
Это зависит от поведения вашего "командного" объекта. Если он ведет себя так же, как коллекция, можно сначала представить его простым списком. Тогда вы можете начать замечать, что вы продолжаете дублировать код, повторяющийся в списке; на этом этапе у вас есть возможность создать объект FootballTeam, который обертывает список игроков. Класс FootballTeam становится домом для всего кода, который выполняет итерацию по списку игроков.
Это делает мой код излишне многословным. Теперь я должен вызвать my_team.Players.Count вместо my_team.Count. К счастью, с C # я могу определять индексаторы, чтобы сделать индексацию прозрачной, и пересылать все методы внутреннего списка ... Но это много кода! Что я получу за всю эту работу?
Инкапсуляция. Вашим клиентам не обязательно знать, что происходит внутри FootballTeam. Все ваши клиенты знают, что это можно реализовать, просмотрев список игроков в базе данных. Им не нужно знать, и это улучшит ваш дизайн.
Это просто не имеет никакого смысла. У футбольной команды нет списка игроков. Это список игроков. Вы не говорите: «Джон Макфутболлер присоединился к игрокам SomeTeam». Вы говорите: «Джон присоединился к SomeTeam». Вы не добавляете букву к «символам строки», вы добавляете букву к строке. Вы не добавляете книгу в книги библиотеки, вы добавляете книгу в библиотеку.
Точно :) вы скажете footballTeam.Add (john), а не footballTeam.List.Add (john). Внутренний список не будет виден.
Это напоминает мне компромисс между «есть» и «есть». Иногда проще и разумнее наследовать непосредственно от суперкласса. В других случаях имеет смысл создать автономный класс и включить класс, от которого вы бы унаследовали, в качестве переменной-члена. Вы по-прежнему можете получить доступ к функциям класса, но не привязаны к интерфейсу или каким-либо другим ограничениям, которые могут возникнуть в результате наследования от класса.
Что ты делаешь? Как и многое другое ... это зависит от контекста. Я бы руководствовался тем, что для наследования от другого класса действительно должны существовать отношения "есть". Итак, если вы пишете класс под названием BMW, он может унаследовать от Car, потому что BMW на самом деле является автомобилем. Класс Horse может быть унаследован от класса Mammal, потому что лошадь на самом деле является млекопитающим в реальной жизни, и любые функции Mammal должны иметь отношение к Horse. Но можно ли сказать, что команда - это список? Насколько я могу судить, это не похоже на то, чтобы команда действительно "была" списком. Итак, в этом случае у меня был бы список в качестве переменной-члена.
В руководящих принципах говорится, что общедоступный API не должен раскрывать внутреннее дизайнерское решение о том, используете ли вы список, набор, словарь, дерево или что-то еще. «Команда» - это не обязательно список. Вы можете реализовать его в виде списка, но пользователи вашего общедоступного API должны использовать ваш класс по мере необходимости. Это позволяет вам изменить свое решение и использовать другую структуру данных, не затрагивая общедоступный интерфейс.
class FootballTeam : List<FootballPlayers>
, пользователи моего класса будут могу сказать, что я унаследовал от List<T>
, используя отражение, видя методы List<T>
, которые не имеют смысла для футбольной команды, и имея возможность использовать FootballTeam
в List<T>
, поэтому я бы раскрыл детали реализации клиенту (без надобности).
Когда они говорят, что List<T>
"оптимизирован", я думаю, они хотят иметь в виду, что у него нет таких функций, как виртуальные методы, которые немного дороже. Итак, проблема в том, что как только вы выставляете List<T>
в своем общедоступном API , вы теряете возможность применять бизнес-правила или настраивать его функциональность позже. Но если вы используете этот унаследованный класс как внутренний в своем проекте (в отличие от потенциально доступного тысячам ваших клиентов / партнеров / других команд в качестве API), тогда все будет в порядке, если это сэкономит ваше время и это функциональность, которую вы хотите дубликат. Преимущество наследования от List<T>
заключается в том, что вы избавляетесь от большого количества глупого кода оболочки, который никогда не будет настраиваться в обозримом будущем. Также, если вы хотите, чтобы ваш класс явно имел ту же семантику, что и List<T>
в течение всего срока службы ваших API, тогда это тоже может быть нормально.
Я часто вижу, как многие люди делают кучу дополнительной работы только потому, что так гласит правило FxCop, или из-за того, что в чьем-то блоге говорится, что это «плохая» практика. Во многих случаях это превращает код в странность палоузы паттернов проектирования. Как и в случае с другими рекомендациями, рассматривайте их как рекомендации, которые могут иметь исключения.
Ух ты, в твоем посте масса вопросов и замечаний. Большинство рассуждений, которые вы получаете от 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.
}
List<T>
расширен как частный внутренний класс, который использует дженерики для упрощения использования и объявления List<T>
. Если бы расширение было просто class FootballRoster : List<FootballPlayer> { }
, тогда да, я мог бы использовать вместо этого псевдоним. Думаю, стоит отметить, что вы можете добавить дополнительные элементы / функции, но они ограничены, поскольку вы не можете переопределить важные элементы List<T>
.
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden
Теперь это - хорошая причина не наследовать от List. В других ответах слишком много философии.
List<T>.GetEnumerator()
, вы получите структуру с именем Enumerator
. Если вы вызовете IList<T>.GetEnumerator()
, вы получите переменную типа IEnumerable<T>
, которая содержит упакованную версию указанной структуры. В первом случае foreach будет вызывать методы напрямую. В последнем случае все вызовы должны виртуально отправляться через интерфейс, что делает каждый вызов медленнее. (На моей машине примерно в два раза медленнее.)
foreach
не заботится о том, реализует ли Enumerator
интерфейс IEnumerable<T>
. Если он сможет найти необходимые методы на Enumerator
, он не будет использовать IEnumerable<T>
. Если вы действительно декомпилируете код, вы увидите, как foreach избегает использования вызовов виртуальной диспетчеризации при итерации по List<T>
, но будет с IList<T>
.
Хотя у меня нет сложного сравнения, как в большинстве этих ответов, я хотел бы поделиться своим методом решения этой ситуации. Расширяя 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
{
...
}
Я просто хотел добавить, что Бертран Мейер, изобретатель Эйфеля и разработавший по контракту, мог бы Team
унаследовать от List<Player>
, даже не моргнув глазом.
В своей книге Построение объектно-ориентированного программного обеспечения он обсуждает реализацию системы с графическим пользовательским интерфейсом, в которой прямоугольные окна могут иметь дочерние окна. Он просто наследует Window
как от Rectangle
, так и от Tree<Window>
, чтобы повторно использовать реализацию.
Однако C # - это не Eiffel. Последний поддерживает множественное наследование и переименование функций . В C #, создавая подкласс, вы наследуете и интерфейс, и реализацию. Вы можете переопределить реализацию, но соглашения о вызовах копируются непосредственно из суперкласса. Однако в Eiffel вы можете изменить имена общедоступных методов, так что вы можете переименовать Add
и Remove
в Hire
и Fire
в своем Team
. Если экземпляр Team
возвращается к List<Player>
, вызывающий будет использовать Add
и Remove
для его изменения, но ваши виртуальные методы Hire
и {{ X10}} будет вызываться.
Если пользователям вашего класса нужны все методы и свойства, которые есть в ** List, вы должны унаследовать от него свой класс. Если они им не нужны, заключите список и создайте оболочки для методов, которые действительно нужны пользователям вашего класса.
Это строгое правило, если вы пишете общедоступный API или любой другой код, который будет использоваться многими людьми. Вы можете игнорировать это правило, если у вас крошечное приложение и не более двух разработчиков. Это сэкономит вам время.
Для крошечных приложений вы также можете выбрать другой, менее строгий язык. Ruby, JavaScript - все, что позволяет писать меньше кода.
Думаю, я не согласен с вашим обобщением. Команда - это не просто набор игроков. У команды гораздо больше информации о ней - название, эмблема, набор управленческого / административного персонала, сбор тренерской бригады, затем набор игроков. Итак, правильно, ваш класс 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();
}
Предпочитайте интерфейсы классам
Классы не должны быть производными от классов и вместо этого реализовывать минимальные необходимые интерфейсы.
Наследование прерывает инкапсуляцию
Получение из классов нарушает инкапсуляцию:
- предоставляет внутренние подробности о том, как реализована ваша коллекция
- объявляет интерфейс (набор общедоступных функций и свойств), который может не подходить
Помимо прочего, это затрудняет рефакторинг вашего кода.
Классы - это деталь реализации
Классы - это деталь реализации, которую следует скрыть от других частей вашего кода. Короче говоря, 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 библиотека.
Когда это приемлемо?
Процитирую Эрика Липперта:
Когда вы создаете механизм, который расширяет механизм
List<T>
.
Например, вы устали от отсутствия метода AddRange
в IList<T>
:
public interface IMoreConvenientListInterface<T> : IList<T>
{
void AddRange(IEnumerable<T> collection);
}
public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
Проблемы с сериализацией
Один аспект отсутствует. Классы, унаследованные от 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
Это классический пример композиции и наследование.
В этом конкретном случае:
Является ли команда списком игроков с дополнительным поведением
Или
Является ли команда отдельным объектом, который содержит список игроков.
Расширяя список, вы ограничиваете себя несколькими способами:
Вы не можете ограничить доступ (например, запретить людям менять список). Вы получаете все методы List независимо от того, нужны они вам / нужны или нет.
Что произойдет, если вы захотите иметь списки и других вещей. Например, у команд есть тренеры, менеджеры, болельщики, оборудование и т. Д. Некоторые из них вполне могут быть списками сами по себе.
Вы ограничиваете свои возможности наследования. Например, вы можете создать общий объект Team, а затем унаследовать от него BaseballTeam, FootballTeam и т. Д. Чтобы наследовать от List, вам нужно наследовать от Team, но тогда это означает, что все различные типы команд вынуждены иметь одинаковую реализацию этого списка.
Композиция - включая объект, дающий желаемое поведение внутри вашего объекта.
Наследование - ваш объект становится экземпляром объекта, который имеет желаемое поведение.
Оба имеют свое применение, но это явный случай, когда композиция предпочтительнее.
Мой грязный секрет: мне все равно, что говорят люди, и я это делаю. .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
Collection<T>
не то же самое, что List<T>
, поэтому может потребоваться довольно много работы для проверки в большом проекте.
team.ByName("Nicolas")
означает "Nicolas"
- название команды.
Похожие вопросы
Связанные вопросы
Новые вопросы
c#
C# (произносится как «see Sharp») — это высокоуровневый мультипарадигменный язык программирования со статической типизацией, разработанный Microsoft. Код C# обычно нацелен на семейство инструментов и сред выполнения Microsoft .NET, которое включает в себя .NET, .NET Framework, .NET MAUI и Xamarin среди прочих. Используйте этот тег для ответов на вопросы о коде, написанном на C#, или о формальной спецификации C#.
string
требуется для того, чтобы делать все, что можетobject
и многое другое .