c# what the - Почему не наследовать от List <T>?





13 Answers

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

Это правильный путь. «Безусловно многословие» - это плохой способ взглянуть на это. Он имеет явное значение, когда вы пишете my_team.Players.Count . Вы хотите подсчитать игроков.

my_team.Count

.. ничего не значит. Считаете что?

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

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

public int PlayerCount {
    get {
        return Players.Count;
    }
}

.., который становится:

my_team.PlayerCount

Теперь это имеет смысл и придерживается Закона Деметры .

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

proper syntax to

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

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

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 не лучше, чем No 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> или нет?




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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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




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

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

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




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

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

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

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

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



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

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

string team = new string();

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

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

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

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




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

myTeam.subList(3, 5);

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




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

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

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

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




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

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




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




Хотя у меня нет сложного сравнения, как большинство из этих ответов, я хотел бы поделиться своим методом для обработки этой ситуации. Расширяясь 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. Если экземпляр Teamupback снова возвращается List<Player>, вызывающий будет использовать Addи Removeизменять его, но ваши виртуальные методы Hireи Fireбудут вызваны.




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

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

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

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

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

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

Классы являются частью реализации

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

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

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

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

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

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

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

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

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

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

Заметки об исключении котельной

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

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

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






Related