为什么不继承 List <T>?

在计划程序时,我通常会像这样思考:

足球队只是足球运动员的名单。因此,我应该用:

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 提供,并且我无法控制它,因此以后在公开 “public API” 之后就无法更改它 。但是我很难理解这一点。什么是公共 API,我为什么要关心?如果我当前的项目没有并且不太可能拥有此公共 API,那么我可以安全地忽略此指南吗?如果我确实从List继承而来但事实证明我需要一个公共 API,我会遇到什么困难?

为什么如此重要?列表就是列表。有什么可能改变?我可能要更改什么?

最后,如果 Microsoft 不希望我从List继承,他们为什么不将类sealed

我还应该使用什么?

显然,对于自定义集合,Microsoft 提供了Collection类,应该扩展该类而不是List 。但是此类非常裸露,并且没有很多有用的东西, 例如AddRangejvitor83 的答案提供了该特定方法的性能原理,但是慢速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. 只是没有任何意义。一支足球队没有 “拥有” 球员名单。这球员名单。您不会说 “John McFootballer 已加入 SomeTeam 的球员”。您说 “约翰加入了 SomeTeam”。您没有在 “字符串的字符” 中添加字母,而是在字符串中添加了字母。您没有将书添加到图书馆的书中,而是将书添加到图书馆。

我意识到,“幕后” 发生的事情可以说是 “将 X 添加到 Y 的内部列表中”,但这似乎是一种非常反常的思考世界的方式。

我的问题(总结)

什么是 C#表示数据结构的正确方法,“逻辑上”(也就是说,“对人类而言”)只是一小部分thingslist

List<T>继承始终是不可接受的吗?什么时候可以接受?为什么 / 为什么不呢?程序员在决定是否从List<T>继承时必须考虑什么?

答案

这里有一些很好的答案。我将向他们补充以下几点。

什么是 C#表示数据结构的正确方法,“逻辑上”(也就是说,“对人类而言”)只是一小部分东西的清单?

让任何十位熟悉足球存在的非计算机编程人员来填补空白:

A football team is a particular kind of _____

有人说过 “有几分风吹草动的足球运动员名单”,还是他们都说过 “运动队” 或“俱乐部” 或“组织”?您认为足球队是一种特殊的球员名单,这在您的人心中和您的人心中都是存在的。

List<T>是一种机制 。足球队是一个业务对象 ,即代表该程序业务领域中某些概念的对象。不要混合这些!足球队是一种球队。它有一个名册,一个名册是一个球员名单 。名册不是特定的球员名单 。名册球员名单。因此,使一个名为Roster的属性成为List<Player> 。并在使用时将其设置为ReadOnlyList<Player> ,除非您认为知道足球队的每个人都将从名单中删除球员。

List<T>继承始终是不可接受的吗?

谁不能接受?我?没有。

什么时候可以接受?

在构建扩展List<T>机制的机制时

程序员在决定是否从List<T>继承时必须考虑什么?

我是在建立机制还是业务对象

但这是很多代码!我能从所有工作中得到什么?

您花了更多时间输入问题,这将使您为List<T>的相关成员编写转发方法超过 50 次。您显然不怕冗长,在这里我们谈论的代码很少。这是几分钟的工作。

更新

我再三考虑一下,还有另一个原因不将足球队建模为球员名单。实际上,将足球队建模为也球员名单可能不是一个好主意。拥有 / 拥有队员列表的球队存在的问题是,您所拥有的只是某个时刻快照 。我不知道您在这堂课上的业务案例是什么,但是如果我有一堂代表足球队的课,我想问一下这样的问题:“有多少海鹰球员因 2003 年至 2013 年受伤而缺席比赛?” 还是 “以前为另一支球队效力的丹佛球员在码数上的同比增长最大?” 或 “ 今年猪头人一路走?

就是说,在我看来,一支足球队的模型被很好地模拟为历史事实的集合,例如球员被招募,受伤,退休等的历史事实 。显然,目前的球员名册是一个重要的事实,应该很重要。居中,但您可能还需要对这个对象进行其他有趣的操作,这些操作需要更多的历史视角。

哇,您的帖子中有很多问题和观点。您从 Microsoft 获得的大多数理由都是正确的。让我们从有关List<T>一切开始

  • List<T> 高度优化。它的主要用法是用作对象的私有成员。
  • Microsoft 并未密封它,因为有时您可能想创建一个名称更友好的class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... } 。现在,就像执行var list = new MyList<int, string>();
  • CA1002:请勿公开通用列表 :基本上,即使您打算将这个应用程序用作唯一的开发人员,也值得通过良好的编码实践进行开发,因此它们会灌输给您和您的本性。如果您需要任何使用者具有索引列表,则仍然可以将列表公开为IList<T> 。这使您稍后可以在类中更改实现。
  • 微软使Collection<T>非常通用,因为它是一个通用概念。它只是一个集合。有更精确的版本,例如SortedCollection<T>ObservableCollection<T>ReadOnlyCollection<T>等,每个版本都实现IList<T>不实现 List<T>
  • Collection<T>允许重写成员(例如,Add,Remove 等),因为它们是虚拟的。 List<T>没有。
  • 问题的最后一部分就在现场。足球队不仅是一个球员列表,所以它应该是包含该球员列表的类。思考构图与继承 。足球队球员名单(名册),但不是球员名单。

如果我正在编写此代码,则该类可能看起来像这样:

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

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

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

    // Any additional members you want to expose/wrap.
}
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.
}

意思是:这是一支足球队,拥有管理层,球员,管理员等。类似:

该图显示了曼联队的成员(和其他属性)

这就是图片中呈现的逻辑……

这是组成继承的经典示例。

在这种情况下:

球队是否有行为更多的球员名单?

要么

团队是否是自己的对象,恰好包含一个球员列表。

通过扩展列表,您可以通过多种方式限制自己:

  1. 您不能限制访问(例如,阻止人们更改花名册)。无论是否需要 / 全部,您都会获得所有 List 方法。

  2. 如果您还想要其他清单,该怎么办?例如,团队中有教练,经理,球迷,设备等。其中一些很可能是他们自己的清单。

  3. 您可以限制继承的选项。例如,您可能要创建一个通用的 Team 对象,然后让 BaseballTeam,FootballTeam 等从该对象继承。要从 List 继承,您需要从 Team 继承,但这意味着所有各种类型的 Team 都必须具有该名册的相同实现。

合成 - 包括一个对象,该对象在对象内部提供您想要的行为。

继承 - 您的对象成为具有所需行为的对象的实例。

两者都有其用途,但这是明显的情况,其中优选组合物。

就像每个人都指出的那样,一个球员团队并不是一个球员列表。这个错误是由各地的许多人(可能是各个专业水平的人)犯的。在这种情况下,问题通常很细微,有时非常严重。这样的设计很糟糕,因为它们违反了Liskov 替代原则 。互联网上有许多很好的文章解释了这个概念,例如, http//en.wikipedia.org/wiki/Liskov_substitution_principle

总之,在类之间的父 / 子关系中有两个规则需要保留:

  • 孩子的要求不低于完全定义父母的要求。
  • 父母除了完全定义孩子之外,不需要任何特征。

换句话说,父母是对孩子的必要定义,而孩子是对父母的充分定义。

这是一种思考解决方案并应用上述原理的方法,该原理应有助于避免这种错误。应该通过验证父类的所有操作在结构和语义上对于派生类是否有效来检验假设。

  • 足球队是足球运动员名单吗? (列表的所有属性是否以相同的含义适用于团队)
    • 团队是同质实体的集合吗?是的,团队是玩家的集合
    • 包括球员的顺序是否描述了球队的状态?除非明确改变,球队是否确保顺序保持?不,不
    • 是否根据球员在球队中的先后顺序来决定是否将他们包括在内?没有

如您所见,只有列表的第一个特征适用于团队。因此,团队不是清单。列表将是您如何管理团队的实现细节,因此该列表仅应用于存储玩家对象并可以通过 Team 类的方法进行操作。

在这一点上,我想指出,我认为甚至不应该使用 List 实现 Team 类。在大多数情况下,应使用 Set 数据结构(例如 HashSet)来实现它。

如果FootballTeam拥有一支预备队和主队怎么办?

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

您将如何建模?

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

关系显然有一个,而不是一个

还是RetiredPlayers

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

根据经验,如果您想从集合中继承,请将类命名为SomethingCollection

您的SomethingCollection语义上是否有意义?仅当您的类型 Something 集合时才这样做。

FootballTeam ,听起来不对。 Team不仅仅是Collection 。正如其他答案所指出的, Team可以有教练,教练等。

FootballCollection听起来像是FootballCollection的集合,或者也许是橄榄球用具的集合。 TeamCollection ,团队的集合。

FootballPlayerCollection听起来像是球员的集合,如果您确实想这样做,那将是从List<FootballPlayer>继承的类的有效名称。

真的List<FootballPlayer>是一个非常好的类型。如果您要从方法返回它,则可能是IList<FootballPlayer>

综上所述

问你自己

  1. XY ?或 XY

  2. 我的班级名字意味着什么吗?

设计 > 实施

您公开哪些方法和属性是设计决策。您从哪个基类继承的是实现细节。我觉得值得退一步。

对象是数据和行为的集合。

因此,您的第一个问题应该是:

  • 该对象在我正在创建的模型中包含哪些数据?
  • 该对象在该模型中表现出什么行为?
  • 未来会如何变化?

请记住,继承表示 “isa”(是)关系,而组合表示 “has”(hasa)关系。考虑到您的情况,随着应用程序的发展,事情可能会走向何方。

在考虑具体类型之前,请先考虑在界面中进行思考,因为有些人发现以这种方式将大脑置于 “设计模式” 更容易。

这不是每个人在日常编码中有意识地做的事情。但是,如果您正在考虑这种主题,那么您将涉足设计领域。意识到它可以解放。

考虑设计细节

看一下 MSDN 或 Visual Studio 上的 List 和 IList 。查看它们公开的方法和属性。在您看来,这些方法是否都像某人想要对 FootballTeam 进行的操作?

footballTeam.Reverse()对您有意义吗? footballTeam.ConvertAll ()是否看起来像您想要的东西?

这不是一个棘手的问题。答案可能确实是 “是”。如果实现 / 继承 List 或 IList ,那么您将被束之高阁;如果最适合您的模型,请执行此操作。

如果您决定是,那是有道理的,并且您希望对象可以作为玩家的集合 / 列表(行为)来对待,因此,您一定要实现 ICollection 或 IList。名义上:

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

如果您希望对象包含玩家(数据)的集合 / 列表,因此希望该集合或列表成为属性或成员,则一定要这样做。名义上:

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

您可能会觉得您希望人们只能枚举一组玩家,而不是计数,添加或删除他们。 IEnumerable 是一个完全有效的选项。

您可能会觉得这些接口根本没有对您的模型有用。这不太可能(IEnumerable 在许多情况下很有用),但仍然有可能。

任何试图告诉您其中一种在每种情况下都是绝对和绝对错误的人都被误导了。任何试图在任何情况下绝对正确地告诉您的人都被误导了。

继续实施

一旦决定了数据和行为,就可以决定实施。这包括您通过继承或组合依赖哪些具体类。

这可能不是很大的一步,并且人们经常将设计和实现混为一谈,因为很可能在一两秒之内就可以完成所有的工作并开始打字。

思想实验

一个人工的例子:正如其他人所提到的,团队并不总是 “只是” 一群球员。您是否为球队维护了比赛得分的集合?在您的模型中,团队可以与俱乐部互换吗?如果是这样,并且如果您的团队是一个球员集合,那么也许它也是一个员工集合和 / 或一个分数集合。然后,您最终得到:

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

尽管进行了设计,但是在 C#中,无论如何都无法通过继承 List 来实现所有这些功能,因为 C#“only” 支持单一继承。 (如果您在 C ++ 中尝试过这种恶意的做法,您可能会认为这是一件好事。)通过继承实现一个集合,而通过组合实现一个集合可能会感到肮脏。除非您显式实现 ILIst .Count 和 IList .Count 等,否则诸如 Count 之类的属性将使用户感到困惑,然后它们只是痛苦而不是令人困惑。您可以看到前进的方向;在思考这个途径时的直觉可能会告诉您,朝这个方向前进是不对的(对与错,如果您以这种方式实施,您的同事也可能会错!)

简短答案(为时已晚)

关于不从集合类继承的指南不是特定于 C#的,您会在许多编程语言中找到它。这是智慧而不是法律。原因之一是,实际上,在可理解性,可实现性和可维护性方面,人们认为组合通常胜过继承。在现实世界 / 领域对象中,找到有用且一致的 “hasa” 关系比有用且一致的 “isa” 关系更为常见,除非您深入了解抽象,尤其是随着时间的流逝以及代码中对象的精确数据和行为变化。这不应导致您总是排除从集合类继承的可能性;但这可能是暗示性的。

首先,它与可用性有关。如果使用继承,则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 
}

这取决于上下文

当您将团队视为球员列表时,您正在将足球团队的 “想法” 投射到一个方面:将 “团队” 缩减为在场上看到的人。此预测仅在特定情况下是正确的。在不同的情况下,这可能是完全错误的。假设您想成为团队的赞助商。因此,您必须与团队经理交谈。在这种情况下,将团队投影到其经理列表中。而且这两个列表通常不会重叠太多。其他背景是当前与以前的参与者等。

语义不明确

因此,将团队视为其参与者列表的问题在于其语义取决于上下文,并且在上下文更改时无法扩展。另外,很难表达您正在使用哪个上下文。

类是可扩展的

当您使用仅具有一个成员的类(例如IList activePlayers )时,可以使用成员的名称(以及其注释)来使上下文清晰。如果有其他上下文,则只需添加一个其他成员。

类更复杂

在某些情况下,创建一个额外的类可能会过大。每个类定义必须通过类加载器加载,并将由虚拟机缓存。这会花费您运行时性能和内存。当您有非常特定的环境时,可以考虑将一支足球队视为球员名单。但是在这种情况下,您实际上应该只使用IList ,而不是从其派生的类。

结论 / 注意事项

当您有非常特定的上下文时,可以将一个团队视为球员列表。例如,在方法内部完全可以编写:

IList<Player> footballTeam = ...

使用 F#时,甚至可以创建类型缩写:

type FootballTeam = IList<Player>

但是,当上下文更广泛甚至不清楚时,您不应这样做。当您创建一个新类时,尤其是在这种情况下,将来可能会在哪个上下文中使用该类尚不清楚。当您开始向班级添加其他属性(团队名称,教练等)时,会出现一个警告标志。这清楚地表明使用该类的上下文不是固定的,将来会更改。在这种情况下,您不能将团队视为球员列表,但应将(当前活跃,未受伤等)球员列表建模为团队的属性。