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

avatar
Superbest
11 февраля 2014 в 03:01
215505
27
1572

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

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

var football_team = new List<FootballPlayer>();

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

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

Тогда я думаю:

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

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

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

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

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

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

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

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

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

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

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

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

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

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

С этим связаны две проблемы:

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

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

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

Мой вопрос (вкратце)

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

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

Источник
Flight Odyssey
11 февраля 2014 в 04:30
98

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

Superbest
11 февраля 2014 в 05:18
6

@FlightOdyssey Мне кажется, что на практике мой вопрос звучит так: «Почему композиция лучше наследования при расширении функциональности List?» Я считаю, что «список игроков» является наиболее фундаментальным свойством футбольной команды - прежде всего, таким как имя или бюджет. Может существовать команда, у которой нет названия (соседские дети, которые формируют две команды, чтобы играть днем), но команда без игроков для меня не имеет смысла. Что до удивления, не знаю. Мне сложно представить, чтобы кто-то удивился такой наследственности, но я чувствую, что могу что-то упустить.

Karl Knechtel
11 февраля 2014 в 08:29
1

См. Также coderhelper.com/questions/4353203/…

STT LCU
11 февраля 2014 в 09:17
86

что, если я скажу вам, что футбольная команда - это бизнес-компания, которая ВЛАДЕЕТ списком контрактов на игроков, а не простым списком игроков с некоторыми дополнительными свойствами?

Tobberoth
11 февраля 2014 в 15:19
89

Это действительно очень просто. Является ли футбольная команда составом игроков? Очевидно, нет, потому что, как вы говорите, есть и другие соответствующие свойства. ЕСТЬ ли в футбольной команде состав игроков? Да, значит, он должен содержать список. В остальном композиция предпочтительнее наследования в целом, потому что позже ее легче изменить. Если вы считаете наследование и композицию логичными одновременно, переходите к композиции.

Izkata
11 февраля 2014 в 20:43
7

my_team.CountFootballs, my_team.CountSpareUniforms ...

Eric Lippert
11 февраля 2014 в 22:34
15

@FlightOdyssey: будьте осторожны с аналогией «квадрат - это вид прямоугольника»; он работает, только если Square и Rectangle имеют неизменные значения . Если они изменчивы, у вас проблемы.

Eric Lippert
11 февраля 2014 в 22:34
49

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

dlev
12 февраля 2014 в 00:09
5

@EricLippert Очевидно, что Squash() должен быть просто виртуальным методом в классе Rectangle, и вы получите полезный NotSupportedException, когда попытаетесь вызвать метод на Square, который маскируется под Rectangle. Чтобы избежать этого исключения, я бы добавил к классу виртуальное свойство IsSquashable, тем самым преобразовав досадное исключение в тупоголовое исключение (поскольку они должны были только что проверить флаг!) Затем я ... о нет, я ' косоглазый. ОО-дизайн - это сложно.

Gangnus
12 февраля 2014 в 20:27
1

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

Eric Lippert
12 февраля 2014 в 20:57
36

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

Flight Odyssey
12 февраля 2014 в 21:55
2

@EricLippert Достаточно справедливо относительно Rectangle s и Square s. Моя цель в использовании этого конкретного примера не состояла в том, чтобы сказать, что в каждая возможная структура классов должна быть реализована как подкласс Rectangle, просто чтобы дать простой пример для иллюстрации общей концепции наследования, не слишком увязнув в расширенных объяснениях и / или неясной терминологии.

dan_l
16 февраля 2014 в 01:17
5

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

nawfal
29 мая 2014 в 06:29
1

Ответы здесь подтверждают обоснованность (особенно см. Эрика Липперта и Сэма Лича). Действительно, футбольная команда не является списком игроков . Это всего лишь один аспект. Класс, который мог бы быть унаследован от List<T>, имел бы смысл, если бы он назвал его FootballPlayerCollection (пуристы поправят меня, что Collection<T> - лучший выбор, хорошо, но вы поняли).

stakx - no longer contributing
15 апреля 2015 в 11:34
2

Немного не по теме (но все же стоит упомянуть ИМХО): List<T> не подходит, потому что он может содержать дубликаты. Может ли футбольная команда иметь одного и того же игрока более одного раза ?! Это было бы так, если бы FootballTeam унаследовал от List<T> или содержал его. Я рекомендую вам выбрать тип коллекции с более строгими гарантиями, например ISet<T>.

Zesty
23 июля 2015 в 12:08
2

Я не согласен с двумя перечисленными вами "проблемами". Во-первых, если у меня 1 команда и 11 игроков, я ожидаю, что my_team.Players.Count = 11, Players.Count (массив) = 11 и my_team.Count = 1 (действительно, my_team не должен иметь даже метода Count). Во-вторых, футбольная команда - это не просто игроки - у нее есть различные свойства, такие как установленный, состояние и т. Д. Таким образом, ее более целесообразно смоделировать как my_team, которая включает в себя Players как свойство. class FootballTeam {public List <FootballPlayer> Players; } - естественный способ смоделировать это, даже без того, чтобы MS давала вам рекомендации.

M. Pathan
19 августа 2015 в 04:28
0

Посмотрите ответы на этот пост. coderhelper.com/questions/400135/c-sharp-listt-or-ilistt

FreeText
29 июня 2018 в 19:47
6

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

Doga Oruc
16 мая 2021 в 09:54
0

«Вы не добавляете книгу в книги библиотеки, вы добавляете книгу в библиотеку», вы можете посмотреть, что такое «Метонимия».

Ответы (27)

avatar
Eric Lippert
11 февраля 2014 в 05:43
1714

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ОБНОВЛЕНИЕ

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

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

Superbest
11 февраля 2014 в 06:19
93

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

Eric Lippert
11 февраля 2014 в 06:25
135

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

Jules
11 февраля 2014 в 08:22
1

@EricLippert "Вы потратили больше времени на ввод своего вопроса, поскольку вам потребовалось бы написать методы пересылки для соответствующих членов List <T> пятьдесят раз". - Прошло некоторое время с тех пор, как я работал в Visual Studio, но при работе на Java с Eclipse среда может автоматически сгенерировать эти методы (Источник / Создать методы делегата / выбрать список / завершить: 4 щелчка мышью). У VS нет возможности сделать это?

peterh
11 февраля 2014 в 08:41
4

Разница между так называемым «механизмом» и «бизнес-объектами» не объясняет, почему List <T> не следует расширять до «бизнес-объекта». Каждый объект - это «механизмы», хотя и не всегда настолько понятны и просты, как подобный универсальный элемент.

user541686
11 февраля 2014 в 10:59
1

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

AJMansfield
11 февраля 2014 в 12:58
53

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

Eric Lippert
11 февраля 2014 в 14:37
117

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

Eric Lippert
11 февраля 2014 в 14:38
0

@Jules: VS имеет такой механизм реализации интерфейсов; Навскидку не знаю, есть ли у него метод пересылки. Было бы легко написать на Roslyn, если бы его не было.

Brian
11 февраля 2014 в 14:54
1

@Jules: Я недостаточно уверен в своих навыках VS, чтобы утверждать, что в VS IDE отсутствует эта функция, но отмечу, что некоторые расширения VS (например, R #) поддерживают это.

user541686
11 февраля 2014 в 16:45
4

@EricLippert: Если вам не нужно об этом беспокоиться, тогда вам не о чем беспокоиться. Если да, то да. Все, что я говорил, это то, что когда это узкое место, решением может быть наследование, а не композиция. Нет причин, по которым вам нужно сбрасывать весь язык только потому, что вы где-то натолкнулись на узкое место ...

WernerCD
11 февраля 2014 в 17:52
5

Obviously the current player roster is an important fact that should probably be front-and-center, but there may be other interesting things you want to do with this object that require a more historical perspective. - Считаете ЯГНИ? Уверен, что в какой-то момент в ближайшем или далеком будущем вам может понадобиться взглянуть на это ... но нужно ли вам это СЕЙЧАС? Создание возможности будущей потребности (которая может когда-либо существовать, а может и не существовать ...) - это напрасная трата усилий.

Eric Lippert
11 февраля 2014 в 18:00
5

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

Superbest
11 февраля 2014 в 18:11
2

Что касается вашего обновления: я на самом деле пытался сделать калькулятор Delta-V, который потребляет список частей ракеты (с такими свойствами, как масса и количество топлива). Действительно, у ракеты нет единого списка деталей, но у нее есть разные наборы деталей для каждой ступени. Я справился с этим, присвоив ракете List<Stage>, где у каждого Stage есть List<RocketPart> - хотя мне стало любопытно об общей проблеме, помимо этой конкретной программы.

Eric Lippert
11 февраля 2014 в 18:15
45

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

Matt Johnson-Pint
11 февраля 2014 в 22:29
4

@EricLippert - Вам следует взглянуть на статью Temporal Patterns Фаулера, если вы еще этого не сделали. :) Но нужно ли вам это делать или нет, во многом зависит от бизнес-сценария и требований к хранению / извлечению . Например, вы можете захотеть, чтобы ваш объект представлял только моментальный снимок, но чтобы ваша инфраструктура базы данных знала о временном аспекте. Я сделал именно это в этом проекте.

Eric Lippert
11 февраля 2014 в 22:39
12

@MattJohnson: Спасибо за ссылку. Один из парадоксов того, как преподают ООП, заключается в том, что «Банковский счет» часто является первым упражнением, которое дают студентам. И, конечно, то, как их учат моделировать, прямо противоположно тому, как реальные банковские счета моделируются в банковском программном обеспечении. В реальном программном обеспечении учетная запись представляет собой список дебетов и кредитов только для добавления, из которого может быть вычислен баланс, а не изменяемый баланс, который уничтожается и заменяется при обновлении.

Magus
11 февраля 2014 в 23:44
3

@Mehrdad: Чтобы повторить то, что сказал Эрик Липперт, учитывая все, что среда выполнения уже делает, что-то всегда будет худшим узким местом, чем выбор композиции. Все, что настолько причудливо, чтобы двигаться быстрее исключительно за счет наследования, вероятно, не следует программировать на C #, что определенно уже влияет на производительность.

Eric Lippert
12 февраля 2014 в 21:32
0

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

gilly3
13 февраля 2014 в 22:02
4

Я полностью согласен с вашим последним утверждением. Очевидно, все здесь согласны - футбольная команда - это , а не список игроков. Проблема в том, что он полностью упускает из виду суть вопроса. Это был неудачный пример для использования в вопросе, потому что интуитивный ответ на него полностью заслонил актуальный, действительный вопрос. Было бы лучше сказать, что он хотел расширить List<T> для некоторых небольших функций в своем свойстве Roster. Затем мы могли бы обсудить причины, по которым MS не рекомендует расширять List<T> и когда это было бы целесообразно в любом случае.

Disillusioned
16 февраля 2014 в 07:12
0

@ gilly3 На основании комментария OP здесь, кажется, выбранный пример, и вопрос вопроса действительно актуален.

Jonathan Leonard
17 февраля 2014 в 06:54
0

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

myermian
17 февраля 2014 в 15:36
0

@EricLippert: Я не согласен с той частью вашего ответа, которая написана после обновления. Не лучше ли создать бизнес-объект так, чтобы он представлял экземпляр футбольной команды, и использовать методы хранения данных для получения бизнес-аналитики / истории?

Eric Lippert
18 февраля 2014 в 01:50
2

@ m-y: Лучше по какой метрике?

Sujit Singh
27 мая 2021 в 08:03
0

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

Sujit Singh
27 мая 2021 в 08:04
0

Итак, команда будет составом свойств, методов и т.д., где одним из свойств может быть IList <FootballPlayer>

Jonathan Wood
26 октября 2021 в 17:30
0

Я думаю, что ваше использование слова механизм здесь нечеткое. Я не вижу ни одной веской причины не наследовать от List<>, если он соответствует вашей модели. И если кто-то может объяснить, как это сказывается на производительности списка, пожалуйста, сделайте это!

avatar
phoog
21 мая 2020 в 19:48
0

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

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

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

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

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

public class MoreConvenientList<T> : List<T>  IMoreConvenientListInterface<T> { }
avatar
marsh-wiggle
20 июля 2019 в 14:55
9

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

Один аспект отсутствует. Классы, наследующие от 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

avatar
cdiggins
7 октября 2018 в 05:01
3

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Замечания об отказе от Boilerplate

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

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

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

cmxl
9 августа 2020 в 08:15
1

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

avatar
Null511
20 января 2018 в 02:43
5

Хотя у меня нет сложного сравнения, как в большинстве этих ответов, я хотел бы поделиться своим методом решения этой ситуации. Расширяя 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
{
    ...
}
avatar
Eniola
9 июля 2016 в 17:14
3

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

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

Может быть, понятие игроков, делающих ставку на PlayerCollection, соответствует вашему предпочтительному подходу?

public class PlayerCollection : Collection<Player> 
{ 
}

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

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

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

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

    public PlayerCollection Players { get; protected set; } = new PlayerCollection();
}
avatar
Alexey
27 апреля 2015 в 11:35
4

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

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

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

Mauro Sampietro
7 июля 2015 в 13:57
1

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

Alexey
7 июля 2015 в 19:12
1

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

avatar
Mark Brackett
6 апреля 2015 в 21:35
23

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

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

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

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

Что такое общедоступный API и почему мне это нужно?

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

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

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

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

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

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

avatar
QuentinUK
18 февраля 2014 в 02:53
11

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

Superbest
18 февраля 2014 в 17:19
1

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

avatar
Ivan Nikitin
16 февраля 2014 в 07:59
3

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

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

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

avatar
user1852503
16 февраля 2014 в 07:56
33

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

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

string team = new string();

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

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

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

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

Superbest
16 февраля 2014 в 20:34
2

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

user1852503
17 февраля 2014 в 05:59
6

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

avatar
Shital Shah
16 февраля 2014 в 07:50
9

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

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

avatar
Disillusioned
15 февраля 2014 в 20:05
20

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

.

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

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


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

avatar
Nicolas Dorier
12 февраля 2014 в 13:57
10

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

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

team.Players.ByName("Nicolas")

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

team.ByName("Nicolas")

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

club.Players.ByName("Nicolas")

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

team.Players.ByName("Nicolas") 

или

team.ByName("Nicolas")

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

Superbest
12 февраля 2014 в 13:59
10

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

Nicolas Dorier
12 февраля 2014 в 14:02
3

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

Superbest
12 февраля 2014 в 14:31
0

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

Nicolas Dorier
12 февраля 2014 в 21:47
0

Извините, SuperBest, я не имел в виду, что ваш вопрос бесполезен или что вы слишком полагаетесь на сообщество. Это хороший вопрос, иначе бы не удосужился дать ответ. Я хочу сказать, что если кто-то говорит: «Не делайте этого» и не объясняет почему, вы должны игнорировать совет, учитывая влияние на то, что произойдет, если вы ошибаетесь. Замена List <T> на Collection <T> не проблема современных инструментов и шаблонов рефакторинга. (даже для большого проекта). Как вы сказали, List <T> имеет некоторое преимущество перед Collection <T>, текущий принятый ответ - это философский аргумент, а не практический.

Sam Leach
14 февраля 2014 в 20:54
2

team.ByName("Nicolas") означает, что "Nicolas" - это название команды.

sara
15 сентября 2015 в 15:45
1

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

Tvde1
18 июня 2019 в 07:53
0

Если следовать закону Деметры, его следует называть team.GetPlayerByName("Nicolas") или team.GetRetiredPlayerByName("John").

avatar
Sam Leach
12 февраля 2014 в 11:41
54

Что, если у 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 a Y?

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

supercat
11 сентября 2014 в 20:48
0

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

supercat
11 сентября 2014 в 20:48
0

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

sara
1 февраля 2016 в 09:56
2

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

avatar
El Zorko
11 февраля 2014 в 22:27
45

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

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

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

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

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

Имейте в виду, что наследование подразумевает отношение «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», если только вы не глубоко вникаете в абстрактность, особенно по мере того, как проходит время и точные данные и поведение объектов в коде. изменения. Это не должно заставлять вас всегда исключать наследование от классов коллекции; но это может показаться подозрительным.

avatar
Cruncher
11 февраля 2014 в 21:42
20

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

myTeam.subList(3, 5);

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

Sam Leach
14 февраля 2014 в 21:01
3

Может, если бы вы назвали его myTeam.subTeam(3, 5);

Cruncher
18 февраля 2014 в 15:52
3

@SamLeach Если это правда, тогда вам все равно понадобится композиция, а не наследование. Поскольку subList больше не будет даже возвращать Team.

avatar
ArturoTena
11 февраля 2014 в 18:03
14

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

Помните: «Все модели неправильные, но некоторые полезны». - Джордж Э. П. Бокс

​​Не существует "правильного пути", только полезный.

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

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

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

- издание 2

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

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

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

ArturoTena
11 февраля 2014 в 18:23
0

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

Disillusioned
15 февраля 2014 в 18:59
2

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

sara
15 сентября 2015 в 15:40
4

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

ArturoTena
15 сентября 2015 в 22:20
0

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

avatar
Satyan Raina
11 февраля 2014 в 16:56
70

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

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

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

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

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

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

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

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

Fred Mitchell
11 февраля 2014 в 21:26
8

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

Tony Delroy
17 февраля 2014 в 10:26
1

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

Satyan Raina
17 февраля 2014 в 11:07
1

@TonyD Что ж, я поднимаю вопрос не о том, что нужно проверять, «делает ли структура данных больше, чем то, что полезно». Это необходимо для проверки «Если структура данных Parent делает что-то, что не имеет отношения к делу, бессмысленно или противоречит интуиции по отношению к поведению, которое может подразумевать класс Child».

Tony Delroy
17 февраля 2014 в 11:54
0

@SatyanRaina: и из них нет ничего плохого в том, чтобы иметь возможность делать дополнительные вещи, которые не имеют отношения к делу или бессмысленны, пока они на самом деле не ставят под угрозу операции, которые имеют смысл и будут использоваться. Например, если в сбалансированном двоичном дереве происходит итерация элементов в порядке сортировки по ключу, а в этом нет необходимости, это тоже не проблема. Например. «порядок включения игроков, [являющийся] описательным для состояния команды»: если порядок не требуется или несколько желаемых порядков все еще могут быть эффективно и удобно сформированы, конкретный порядок по умолчанию не причиняет вреда.

Satyan Raina
17 февраля 2014 в 14:51
2

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

Tony Delroy
18 февраля 2014 в 01:30
0

@SatyanRaina: пожалуйста, не меняйте тему с случайного поведения в рамках желаемого API на принципиально нарушенное происхождение. В частности, ваши тесты «порядок включения ...» и «... включены / отброшены на основе их последовательного положения ...» не являются хорошими тестами несоответствия списка, они просто установить, что в списке есть некоторые ненужные свойства. Вы предлагаете HashSet, что является разумным советом, но, используя вашу логику, я мог бы сказать: «Требуется ли поиск O (1) по ключу? Нет», «возможно, тест производительности проходит, несмотря на неэффективный алгоритм». Тестирование - это гонка вооружений.

Satyan Raina
18 февраля 2014 в 10:07
1

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

Tim B
25 февраля 2016 в 14:09
0

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

Mr Anderson
1 сентября 2016 в 19:40
0

Вы перепутали родительско-дочерние отношения с наследованием. Совершенно разные.

Satyan Raina
27 декабря 2019 в 07:00
1

@MrAnderson Не могли бы вы уточнить?

avatar
xpmatteo
11 февраля 2014 в 16:51
17

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

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

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

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

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

Mauro Sampietro
22 декабря 2014 в 08:32
1

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

xpmatteo
23 декабря 2014 в 12:20
3

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

Mauro Sampietro
23 декабря 2014 в 12:49
0

Трудно получить правильные модели, после того как они будут созданы, они окажутся единственными полезными. Если модель может быть использована неправильно, это концептуально неверно. coderhelper.com/a/21706411/711061

avatar
Mauro Sampietro
11 февраля 2014 в 16:25
29

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

Логически неверно:

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

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

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}
Servy
11 февраля 2014 в 16:48
22

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

rball
11 февраля 2014 в 17:11
2

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

Mauro Sampietro
13 февраля 2014 в 13:36
3

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

sara
15 сентября 2015 в 15:28
2

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

Mauro Sampietro
16 сентября 2015 в 07:46
2

Вопрос не в инкапсуляции. Я сосредотачиваюсь на том, чтобы не расширять тип, если вы не хотите, чтобы этот тип вел себя более определенным образом. Эти поля должны быть автосвойствами в C # и методами get / set на других языках, но здесь, в этом контексте, это совершенно излишне.

avatar
Nean Der Thal
11 февраля 2014 в 15:45
166
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

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

Ben
13 апреля 2015 в 17:13
14

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

SiD
20 июля 2016 в 12:24
3

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

Davide Cannizzo
12 декабря 2017 в 15:18
2

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

E. Verdi
21 октября 2020 в 06:25
1

@Ben На мой взгляд, ответ Nean Der Thal правильный. Футбольная команда состоит из руководителей (тренера, помощника и т. Д.), Игроков (и важное примечание: «игроки, выбранные для матча», потому что не все игроки в команде будут выбраны, например, для матча лиги чемпионов), администраторов и т. Д. . Футбольный клуб - это что-то вроде людей в офисе, на стадионе, болельщиков, правления клуба и, наконец, что не менее важно: несколько команд (например, первая команда, женская команда, молодежные команды и т. Д.). верьте всему, что говорит Википедия;)

Ben
12 ноября 2020 в 16:04
0

@ E.Verdi Я думаю, что на втором снимке на заднем плане изображен стадион, и, как вы предположили, он выглядит так, будто представляет клуб, а не команду. В конце концов, определения этих терминов являются просто семантикой (то, что я мог бы назвать командой, другие могли бы назвать составом и т. Д.). Я думаю, дело в том, что то, как вы моделируете свои данные, зависит от того, для чего они будут использоваться. Это хороший момент, и я думаю, что фотографии помогают это показать :)

avatar
Paul J Abernathy
11 февраля 2014 в 14:56
12

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

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

avatar
Tim B
11 февраля 2014 в 10:58
128

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

В данном случае:

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

или

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

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

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

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

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

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

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

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

Cruncher
12 февраля 2014 в 13:47
1

Если перейти к пункту 2, то для списка не имеет смысла иметь иметь список.

Mauro Sampietro
9 августа 2016 в 13:16
0

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

Tim B
9 августа 2016 в 14:24
7

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

Mauro Sampietro
10 августа 2016 в 07:17
0

@TimB .. мы действительно говорим об одном и том же ?! en.wikipedia.org/wiki/Composition_over_inheritance

Tim B
10 августа 2016 в 08:55
5

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

Andrew Rondeau
25 августа 2017 в 19:24
0

Это вопрос о композиции и наследовании. Темы, которые поднял ОП, в конечном итоге показывают, что, по его мнению, он на самом деле не понимает разницы между отношениями «есть» и «имеет-а». OP также неправильно понимает поведение по сравнению с реализациями.

Julia McGuigan
1 февраля 2018 в 17:42
3

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

avatar
stefan.schwetschke
11 февраля 2014 в 10:10
31

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

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

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

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

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

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

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

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

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

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

IList<Player> footballTeam = ...

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

type FootballTeam = IList<Player>

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

avatar
myermian
11 февраля 2014 в 03:26
181

Вау, в вашем сообщении множество вопросов и замечаний. Большинство рассуждений, которые вы получаете от 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.
}
peterh
11 февраля 2014 в 08:44
0

FootballGroup: List <FootballPlayers> мне кажется абсолютно приемлемым и очень эффективным. Никаких реальных технических причин, почему эта конструкция не заслуживает права на жизнь, объяснено не было, только философские аргументы.

Brian
11 февраля 2014 в 14:05
0

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

myermian
11 февраля 2014 в 16:33
4

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

Mark Hurd
12 февраля 2014 в 02:10
6

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

myermian
12 февраля 2014 в 02:25
0

@jberger: я всегда предпочитаю использовать LINQ-версию Enumerable.Count<TSource>(IEnumerable<TSource>), так что если я когда-нибудь изменю базовый тип, он всегда будет работать, пока это IEnumerable<T>. При использовании List <T> .Count наблюдается очень незначительное снижение производительности. Однако однажды я могу решить, что мне нужна новая структура данных, реализующая IEnumerable <T>, но ничего больше, кто знает. См. Coderhelper.com/a/981283/347172 для получения дополнительной информации. :)

Navin
12 февраля 2014 в 03:54
11

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

Groo
12 февраля 2014 в 13:00
0

@Navin: это не относится к классу List<T>. Collection<T> класс - , предполагается, что наследуется для расширения его функциональности по мере необходимости.

myermian
12 февраля 2014 в 13:47
0

@Groo: Вы понимаете, что он не переходит от O (1) к O (n) , если последовательность имеет тип ICollection<T> или ICollection. В таких случаях функция LINQ просто возвращается к использованию методов ICollection<T>.Count / ​​ICollection.Count.

Groo
12 февраля 2014 в 14:34
0

@ m-y: Да, извините, это верно, я не подумал об этом. Он также выполняет эту оптимизацию с помощью First, Last и других методов расширения, которые могут получить выгоду от прямого доступа к членам ICollection, поэтому на самом деле это не так уж и плохо.

Jonathan Allen
19 апреля 2016 в 22:16
0

Если вы напишете public IList<T> Roster, потребитель платит штраф за производительность. Намного быстрее использовать for-each вместо List<T>, чем IList<T>, например, более чем double на моей машине.

myermian
19 апреля 2016 в 23:53
0

@JonathanAllen: Ключевое слово foreach работает не так (см. Coderhelper.com/a/3679993/347172). В этом случае базовым типом по-прежнему является List<T>, что означает, что он будет вызывать GetEnumerator() для типа и использовать этот перечислитель для перебора.

Jonathan Allen
20 апреля 2016 в 19:58
2

Если вы вызовете List<T>.GetEnumerator(), вы получите структуру с именем Enumerator. Если вы вызовете IList<T>.GetEnumerator(), вы получите переменную типа IEnumerable<T>, которая содержит упакованную версию указанной структуры. В первом случае foreach будет вызывать методы напрямую. В последнем случае все вызовы должны виртуально отправляться через интерфейс, что делает каждый вызов медленнее. (На моей машине примерно в два раза медленнее.)

myermian
20 апреля 2016 в 21:11
0

@JonathanAllen: любой вызов GetEnumerator() вернет тип, который реализует IEnumerable<T> (или IEnumerable) в зависимости от использования. Однако базовый тип (в этом случае) тот же. Это структура Enumerator. См. Справочный источник: linksource.microsoft.com/#mscorlib/system/collections/… ... все они возвращают один и тот же тип.

Jonathan Allen
20 апреля 2016 в 23:39
3

Вы полностью упускаете суть. foreach не заботится о том, реализует ли Enumerator интерфейс IEnumerable<T>. Если он сможет найти нужные ему методы на Enumerator, он не будет использовать IEnumerable<T>. Если вы действительно декомпилируете код, вы увидите, как foreach избегает использования вызовов виртуальной диспетчеризации при итерации по List<T>, но будет с IList<T>.

Jonathan Allen
20 апреля 2016 в 23:41
1

Фактически, GetEnumerator даже не нужно возвращать что-то IEnumerable. Вы можете создать свой собственный перечислитель, который имеет правильные методы, но не реализует этот интерфейс, и foreach все равно будет работать.

JAlex
10 августа 2021 в 16:46
0

Вам необходимо указать T как общий параметр, public class FootballTeam<T> ..., иначе поле List<T> _roster имеет недопустимый синтаксис.

avatar
Dmitry S.
11 февраля 2014 в 03:25
36

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

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

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