为什么不继承List <T>?


1398

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

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

var football_team = new List<FootballPlayer>();

该列表的顺序代表了球员在名单中的排列顺序。

但是后来我意识到,除了仅列出球员名单以外,球队还具有其他属性,必须加以记录。例如,本赛季的总得分,当前预算,统一的颜色,string代表球队名称的a 等。

所以我想:

好的,足球队就像一个球员名单,但另外,它还有一个名称(a string)和一个总得分(an 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. 只是没有任何意义。一支足球队没有“拥有”球员名单。这球员名单。您不会说“ John McFootballer已加入SomeTeam的球员”。您说“约翰加入了SomeTeam”。您没有在“字符串的字符”中添加字母,而是在字符串中添加了字母。您没有将书添加到图书馆的书中,而是将书添加到图书馆。

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

我的问题(总结)

什么是代表一个数据结构,其中,“逻辑”(即,“以人的心灵”)仅仅是一个正确的C#这样listthings与一些花俏?

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


90
那么,您的问题真的是什么时候使用继承(方型是矩形),因为在请求通用矩形时总是提供方型是合理的),以及何时使用合成(足球队有-足球运动员列表,此外)其他与它一样“基本”的属性(例如名称)?如果在代码的其他地方,当您期望一个简单的List时,您将他们传递给FootballTeam,会让其他程序员感到困惑吗?
奥德赛航班

77
如果我告诉你一个足球队是一家商业公司,它拥有一份球员合同清单,而不是拥有一些其他财产的球员清单,那该怎么办?
STT LCU 2014年

75
这真的很简单。足球队是球员名单吗?显然不是,因为正如您所说,还有其他相关属性。足球队有队员名单吗?是的,因此它应该包含一个列表。除此之外,一般来说,组合优于继承,因为以后更容易修改。如果您同时发现继承和合成逻辑,请使用合成。
Tobberoth

45
@FlightOdyssey:假设您有一个SquashRectangle方法,它采用一个矩形,将其高度减半,并将其宽度加倍。现在传入一个恰好是4x4但类型为长方形的矩形,以及一个4x4的正方形。调用SquashRectangle之后对象的尺寸是多少?对于矩形,显然是2x8。如果正方形是2x8,则它不再是正方形;如果不是2x8,则对相同形状的相同操作会产生不同的结果。结论是,可变正方形不是矩形。
埃里克·利珀特

29
@Gangnus:您的陈述完全是错误的;真正的子类需要具有作为类的超集的功能。string需要A 做所有object可以做的事以及更多
埃里克·利珀特

Answers:


1564

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

什么是表示数据结构的正确C#方法,“逻辑上”(也就是说,“对人类而言”)只是一个带有一些花哨的东西的列表?

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

足球队是_____的一种特殊类型

难道有人说“有一些花俏的足球运动员名单”,还是他们都说“运动队”或“俱乐部”或“组织”?您认为足球队是一种特殊的球员名单,这在您的人心中和您的人心中都是存在的。

List<T>是一种机制。足球队是一个业务对象,即代表该程序业务领域中某些概念的对象。不要混那些!足球队是一种球队。它有一个名册,一个名册是一个球员名单。名册不是特定的球员名单。名册球员名单。因此,让一个名为属性Roster这是一个List<Player>。并使其ReadOnlyList<Player>当你在它,除非你相信大家谁知道一个足球队获得从名册上删除的球员。

List<T>永远继承是不可接受的吗?

谁不能接受?我?没有。

什么时候可以接受?

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

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

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

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

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

更新

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

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


84
尽管其他答案非常有帮助,但我认为这个答案最直接地解决了我的担忧。至于夸张的代码量,您说的很对,毕竟工作量不大,但是当我在程序中包含一些代码却不理解为什么要包含代码时,我确实很容易感到困惑。
极好的2014年

127
@Superbest:很高兴我能帮上忙!您听这些怀疑是正确的。如果您不明白为什么要编写一些代码,请找出原因,或者编写自己理解的其他代码。
埃里克·利珀特

48
@Mehrdad但是,您似乎忘记了优化的黄金法则:1)不这样做,2)尚未完成,3)在没有首先执行性能配置文件以显示需要优化的内容之前就不要这样做。
AJMansfield 2014年

107
@Mehrdad:老实说,如果您的应用程序要求您关心虚拟方法的性能负担,那么任何现代语言(C#,Java等)都不适合您。我有一台笔记本电脑在这里,可以运行一个模拟每街机游戏玩我作为一个孩子的同时。这使我不必担心这里和那里的间接级别。
埃里克·利珀特

44
@Superbest:现在,我们绝对处于“组成不继承”的范畴。火箭由火箭零件组成。火箭不是“一种特殊的零件清单”!我建议你进一步削减;为什么列出而不是IEnumerable<T>- 序列
埃里克·利珀特

161

哇,您的帖子中有很多问题和观点。您从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>允许成员(即添加,删除等)被覆盖,因为它们是虚拟的。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.
}

6
您的帖子充满了所有疑问 ”-好吧,我确实说过我很困惑。感谢您链接到@Readonly的问题,我现在看到我的问题很大一部分是更一般的Composition vs. Inheritance问题的特例。
极好的2014年

3
@Brian:除了不能使用别名创建泛型外,所有类型都必须是具体的。在我的示例中,List<T>扩展为私有内部类,该内部类使用泛型来简化的使用和声明List<T>。如果扩展名仅仅是class FootballRoster : List<FootballPlayer> { },则可以使用别名代替。我想应该指出您可以添加其他成员/功能,但这是有限的,因为您无法覆盖的重要成员List<T>

6
如果这是Programmers.SE,则我同意当前的答案投票范围,但是,因为它是SO帖子,所以即使存在一定的一般性,该答案也至少会尝试回答C#(.NET)特定问题。设计问题。
马克·赫德2014年

7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden现在,这是不继承List的一个很好的理由。其他答案有太多哲学。
纳文2014年

10
@my:我怀疑您是在说“问题”,因为侦探是侦探。
2014年

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

先前的代码意味着:一群在街上踢足球的家伙,他们碰巧有个名字。就像是:

踢足球的人

无论如何,这段代码(根据我的回答)

public class FootballTeam
{
    // A team's name
    public string TeamName; 

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

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

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

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

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

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

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


9
我希望您的第二个例子是足球俱乐部,而不是足球队。足球俱乐部有经理和管理员等。 根据这个来源:“足球队是赋予一组球员的统称...可以选择这些球队与对方球队进行比赛,以代表足球俱乐部...”。因此,我认为球队球员名单(或更准确地说,是球员名单)。
2015年

3
更优化private readonly List<T> _roster = new List<T>(44);
SiD

2
有必要导入System.Linq名称空间。
Davide Cannizzo

1
视觉效果:+1
V.7

115

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

在这种情况下:

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

要么

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

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

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

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

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

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

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

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


1
在#2上扩展,列表具有-列表没有意义。
Cruncher 2014年

首先,这个问题与合成与继承无关。答案是OP不想实现更特定类型的列表,因此他不应该扩展List <>。我很困惑,因为您在stackoverflow上的得分很高,应该清楚地知道这一点,并且人们信任您所说的话,因此,到现在为止,至少有55个人赞成并对其思想感到困惑,他们相信可以建立一种方法或另一种方法一个系统,但显然不是!#no-rage
Mauro Sampietro'8

6
@sam他想要列表的行为。他有两个选择,他可以扩展列表(继承),也可以在对象(合成)中包括一个列表。也许您误解了问题或答案的一部分,而不是55个人错了,对吗?:)
Tim B

4
是。这是一个简陋的案例,只有一个上级和下级,但我将针对他的具体问题给出更一般的解释。他可能只有一个团队,并且可能总是只有一个列表,但是选择仍然是继承列表或将其作为内部对象。一旦您开始包括多种类型的球队(足球,板球等等),而他们又不仅仅是一个球员列表,您就会看到您如何面对完整的组成与继承问题。在这种情况下,从全局上看很重要,这样可以尽早避免错误的选择,而错误的选择又意味着以后的重构。
Tim B

3
OP并未要求组合,但是用例是X:Y问题的明确示例,其中四个面向对象的编程原理之一被破坏,滥用或未完全理解。更为明确的答案是编写一个专门的集合(如Stack或Queue(看起来不适合用例))或了解组成。足球队不是足球运动员名单。OP是否明确要求它无关紧要,如果不了解抽象,封装,继承和多态性,他们将无法理解答案,因此,X:Y amok
Julia McGuigan

67

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

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

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

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

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

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

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

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


8
列表与集合上的优势。这似乎只是一个很常见的错误。如果集合中元素的一个实例只有一个,则某种集合或字典应该是实现的首选。一支有两名同名球员的球队是可以的。一次让一名玩家同时出现两次是不正确的。
弗雷德·米切尔

1
+1是一些优点,尽管更重要的是要问“一个数据结构是否可以合理地支持所有有用的东西(理想的是短期还是长期的)”,而不是“该数据结构做得比有用的事还多”。
Tony Delroy 2014年

1
@TonyD好吧,我要提出的观点不是应该检查“数据结构是否比有用的功能更多”。将检查“父级数据结构是否执行了与子类可能暗示的行为无关,无意义或违反直觉的行为”。
Satyan Raina 2014年

1
@TonyD从父级获得不相关的特征实际上是一个问题,因为在许多情况下,它会通过否定测试。程序员可以扩展Human {eat(); 跑(); 大猩猩的write();} {eat(); 跑(); swing();}认为具有摆动能力的附加功能的人没有任何问题。然后在游戏世界中,您的人类突然开始在树上摇摆,从而绕过了所有假定的陆地障碍。除非明确指定,否则实际的人不能摆动。这样的设计使api非常容易受到滥用和混淆
Satyan Raina 2014年

1
@TonyD我也不建议Player类也应从HashSet派生。我建议Player类应在大多数情况下通过HashSet通过Composition实现,而这完全是实现级别的细节,而不是设计级别的细节(这就是为什么我将其作为答案的旁注)。如果有这样一种实现的合理依据,则很可能使用列表来实现。因此,要回答您的问题,是否需要通过键进行O(1)查找?不可以。因此也不应从HashSet扩展Player。
Satyan Raina 2014年

50

如果FootballTeam主队有后备队怎么办?

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

您将如何建模?

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

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

还是RetiredPlayers

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

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

您的SomethingCollection语义有意义吗?仅当您的类型是以下内容的集合时才这样做Something

在这种情况下FootballTeam听起来不正确。一个Team比一个多Collection。一个Team正如其他答案所指出的,可以有教练,培训师等。

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

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

确实List<FootballPlayer>是一个非常好的类型。也许IList<FootballPlayer>如果您要从方法中返回它。

综上所述

问你自己

  1. X一个Y?还是 X一个Y

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


如果每一个球员的类型,将其分类为属于两种DefensivePlayersOffensivePlayers或者OtherPlayers,它可能是合法有用的可能由一个希望将代码中使用的类型List<Player>,但也包含成员DefensivePlayersOffsensivePlayersSpecialPlayers类型IList<DefensivePlayer>IList<OffensivePlayer>IList<Player>。一个人可以使用一个单独的对象来缓存单独的列表,但是将它们封装在与主列表相同的对象中似乎会更干净[使用列表枚举器的无效...
supercat 2014年

...以作为以下事实的提示:主列表已更改,子列表在下次访问时需要重新生成]。
supercat 2014年

2
虽然我同意所提出的观点,但看到有人提出设计建议并建议在业务对象中公开露面的公开List <T>,这确实让我感到非常
震惊

38

设计>实施

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

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

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

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

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

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

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

考虑设计细节

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

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

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

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

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#“ only”支持单一继承。(如果您在C ++中尝试过这种恶意的做法,您可能会认为这是一件好事。)通过继承实现一个集合,而通过组合实现一个集合可能会感到肮脏。除非您显式实现ILIst <Player> .Count和IList <StaffMember> .Count等,否则诸如Count之类的属性将使用户感到困惑,然后它们只是痛苦而不是令人困惑。您可以看到前进的方向;在思考这个途径时的直觉可能会告诉您,朝这个方向前进是不对的(对与错,如果您以这种方式实施,您的同事也可能会错!)

简短答案(为时已晚)

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


34

首先,它与可用性有关。如果使用继承,则Team该类将公开纯粹用于对象操作的行为(方法)。例如,AsReadOnly()CopyTo(obj)方法对于团队对象没有意义。除了AddRange(items)方法之外,您可能还需要更具描述性的AddPlayers(players)方法。

如果要使用LINQ,则实现通用接口(例如ICollection<T>IEnumerable<T>则更有意义)。

如前所述,合成是正确的方法。只需将玩家列表实现为私有变量即可。


30

足球队不是足球运动员的清单。一支足球队一名足球运动员组成!

这在逻辑上是错误的:

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

这是正确的:

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

19
这不能解释原因。OP知道大多数其他程序员都有这种感觉,他只是不明白为什么它很重要。这实际上只是提供问题中已经存在的信息。
Servy 2014年

2
这也是我想到的第一件事,即他的数据是如何被错误地建模的。因此,原因是您的数据模型不正确。
rball

3
为什么不从列表继承?因为他不想要更具体的列表。
Mauro Sampietro 2014年

2
我不认为第二个例子是正确的。您正在暴露状态,从而破坏了封装。状态应在对象内部隐藏/内部,并且通过公共方法更改此状态。确切地说,哪种方法是根据业务需求确定的,但应该以“现在需要此”为基础,而不是“可能在某些时候不方便”为基础。保持api紧密,您将节省时间和精力。
萨拉

1
封装不是问题的重点。如果您不希望该类型以更特定的方式表现,我将重点放在不扩展该类型上。这些字段在c#中应该是自动属性,在其他语言中应该是get / set方法,但是在这种情况下,这是完全多余的
Mauro Sampietro 2015年

28

这取决于上下文

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

语义不明确

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

类是可扩展的

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

类更复杂

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

结论/注意事项

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

IList<Player> footballTeam = ...

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

type FootballTeam = IList<Player>

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


27

让我重写您的问题。因此您可能会从不同的角度看待主题。

当我需要代表一支足球队时,我知道它基本上是一个名字。如:“老鹰”

string team = new string();

后来我意识到球队也有球员。

为什么我不能仅仅扩展字符串类型,以便它还包含一个播放器列表?

您进入问题的切入点是任意的。试着想一想一个团队拥有什么(属性),而不是它什么。

完成之后,您可以查看它是否与其他类共享属性。并考虑继承。


1
这是一个好点-可以将团队视为一个名字。但是,如果我的应用程序旨在与玩家的行为打交道,那么这种想法就不太明显了。无论如何,问题似乎最终归结为组成与继承。
极好的

6
这是一种值得看的方法。请注意:一个有钱人拥有几支足球队,他给一个朋友送礼物作为礼物,朋友改了球队的名字,解雇了教练​​,并替换所有玩家。朋友队在果岭上遇到了男子队,而随着男子队的失利,他说:“我不敢相信你会在我给你的球队中击败我!” 这个人是正确的吗?您将如何检查?
user1852503

20

仅仅因为我认为其他答案与足球队是“是” List<FootballPlayer>还是“具有”的切线有关List<FootballPlayer>,而实际上这并不能按照书面回答这个问题。

OP主要要求澄清有关继承自List<T>以下方面的准则:

一条准则说,您不应该继承List<T>。为什么不?

因为List<T>没有虚方法。在您自己的代码中,这不是什么大问题,因为您通常可以以相对较少的痛苦切换出实现,但是在公共API中则可能要大得多。

什么是公共API,我为什么要关心?

公共API是您向第三方程序员公开的接口。考虑框架代码。并请记住,所引用的准则是“ .NET Framework设计准则”,而不是“ .NET 应用程序设计准则”。有所不同,并且-通常来说-公共API设计要严格得多。

如果我当前的项目没有并且不太可能拥有此公共API,那么我可以安全地忽略此指南吗?如果我确实从List继承而来,但事实证明我需要一个公共API,我会遇到什么困难?

是的,是的。您可能需要考虑其背后的原理,以查看它是否仍然适用于您的情况,但是如果您不构建公共API,则无需特别担心诸如版本控制之类的API问题(这是一个子集)。

如果将来要添加公共API,则可能需要从实现中抽象出API(不要List<T>直接公开),否则可能会导致将来的痛苦而违反准则。

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

取决于上下文,但是由于我们只是FootballTeam作为示例-假设您不能添加a 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,请执行所需操作。


18

是否允许人们说

myTeam.subList(3, 5);

有任何意义吗?如果不是,那么它不应该是列表。


3
如果您打电话给它,可能会如此myTeam.subTeam(3, 5);
Sam Leach 2014年

3
@SamLeach假设是真的,那么您仍然需要组合而不是继承。由于subList甚至Team不再返回。
Cruncher 2014年

1
这比其他答案更能使我信服。+1
FireCubez

16

这取决于“团队”对象的行为。如果它的行为就像一个集合,则可以首先用一个普通的List表示它。然后,您可能会开始注意到,您一直在重复在列表上迭代的代码;此时,您可以选择创建一个包装球员列表的FootballTeam对象。FootballTeam类成为遍历球员列表的所有代码的所在地。

它使我的代码不必要地冗长。我现在必须调用my_team.Players.Count而不是my_team.Count。幸运的是,使用C#,我可以定义索引器以使索引透明化,并转发内部List的所有方法...但是,这需要很多代码!我能从所有工作中得到什么?

封装。您的客户不必知道FootballTeam内部的情况。就您所有的客户而言,它可能是通过在数据库中查找玩家列表来实现的。他们不需要知道,这可以改善您的设计。

只是没有任何意义。一支足球队没有“拥有”球员名单。这是球员名单。您不会说“ John McFootballer已加入SomeTeam的球员”。您说“约翰加入了SomeTeam”。您没有在“字符串的字符”中添加字母,而是在字符串中添加了字母。您没有将书添加到图书馆的书中,而是将书添加到图书馆。

恰好:)您将说出footballTeam.Add(john),而不是footballTeam.List.Add(john)。内部列表将不可见。


1
OP希望了解如何对现实进行建模,但随后将足球队定义为足球运动员列表(从概念上讲这是错误的)。这就是问题。我认为,任何其他论点都会误导他。
Mauro Sampietro 2014年

3
我不同意球员名单在概念上是错误的。不必要。正如其他人在本页中写道,“所有模型都是错误的,但有些模型是有用的”
xpmatteo 2014年

正确的模型很难获得,一旦完成,它们便是有用的模型。如果可以滥用模型,则从概念上讲是错误的。stackoverflow.com/a/21706411/711061
Mauro Sampietro 2014年

15

这里有很多很好的答案,但是我想谈谈我没有提到的东西:面向对象的设计是关于赋予对象权力

您想将所有规则,其他工作和内部细节封装在适当的对象中。这样,其他与此对象进行交互的对象就不必担心这一切。实际上,您想更进一步,积极地防止其他对象绕过这些内部组件。

从中继承时List,所有其他对象都可以将您视为列表。他们可以直接访问添加和删除播放器的方法。您将失去控制;例如:

假设您想通过了解球员是退休,辞职还是被解雇来区分他们何时离开。您可以实现一个RemovePlayer采用适当输入枚举的方法。然而,通过继承List,你将无法避免直接访问RemoveRemoveAll甚至Clear。结果,您实际上已经丧失FootballTeam上课的能力。


关于封装的其他想法...您提出了以下担忧:

它使我的代码不必要地冗长。我现在必须调用my_team.Players.Count而不是my_team.Count。

您是正确的,这对于所有客户使用您的团队来说都是多余的。但是,与您暴露的事实相比,该问题很小List Players与您无所不包,无所事事因此未经您的同意,它们可以摆弄您的团队。

您继续说:

只是没有任何意义。一支足球队没有“拥有”球员名单。这是球员名单。您不会说“ John McFootballer已加入SomeTeam的球员”。您说“约翰加入了SomeTeam”。

首先,您错了:删除“列表”一词,很明显,团队确实有球员。
但是,您用第二个击中了头部。您不希望客户致电ateam.Players.Add(...)。您确实希望他们打电话ateam.AddPlayer(...)。并且您的实现将(可能除其他外)在Players.Add(...)内部调用。


希望您能看到封装对于授权对象的重要性。您想让每个类都做好自己的工作,而不必担心受到其他对象的干扰。


12

表示数据结构的正确 C#方法是什么...

记住:“所有模型都是错误的,但有些模型是有用的。” - 乔治EP Box

没有“正确的方法”,只有一种有用的方法。

选择对您和/或您的用户有用的一种。而已。经济发展,不要过度工程。您编写的代码越少,调试所需的代码就越少。(请阅读以下版本)。

-编辑

我最好的答案是……这取决于。从List继承会将此类的客户端暴露给不应公开的方法,主要是因为FootballTeam看起来像一个业务实体。

-版本2

我真的不记得我所说的“不要过度设计”评论。虽然我相信KISS的心态是一个很好的指南,但我想强调一点,由于抽象泄漏,从List继承业务类会产生比解决的问题更多的问题。

另一方面,我相信在少数情况下,仅从List继承是有用的。如我在上一版中所写,这取决于。每种情况的答案在很大程度上受知识,经验和个人偏好的影响。

感谢@kai帮助我更准确地思考答案。


我最好的答案是……这取决于。从List继承将使此类的客户端暴露于不应公开的方法,主要是因为FootballTeam看起来像一个业务实体。我不知道该编辑该答案还是发表另一个想法?
ArturoTena 2014年

2
正是List继承自a 将使此类的客户端暴露于可能不应该公开的方法 ”这一点正是使从List绝对否定继承的原因。一旦暴露,它们就会随着时间的流逝逐渐被滥用和滥用。现在通过“经济发展”节省时间很容易导致未来节省的资金减少十倍:调试滥用行为并最终进行重构以修复继承问题。YAGNI原则也可以被理解为含义:您不会需要的所有这些方法List,因此请不要公开它们。
幻灭了

3
“不要过度设计。编写的代码越少,调试所需的代码就越少。” <-我认为这是一种误导。封装和组合的继承不是过度设计,也不会引起更多的调试需求。通过封装,您限制了客户端可以使用(和滥用)您的类的方式,因此,您需要进行测试和输入验证的入口点减少了。从List它那里继承是因为它既快速又容易,从而导致更少的错误,这完全是错误的,这只是不良的设计,不良的设计会导致比“过度设计”更多的错误。
萨拉

@kai我在每一点上都同意你的观点。我真的不记得我所说的“不要过度设计”评论。OTOH,我相信在少数情况下,仅从List继承是有用的。如我在以后的版本中所写,这取决于。每种情况的答案在很大程度上受知识,经验和个人偏好的影响。就像生活中的一切。;-)
ArturoTena 2015年

11

这让我想起了“是”与“有”的权衡。有时直接从超类继承会更容易并且更有意义。在其他时候,创建一个独立的类并包含您将从其继承的类作为成员变量更有意义。您仍然可以访问类的功能,但不绑定到接口或任何其他从类继承而来的约束。

你要做什么 与很多事物一样,它取决于上下文。我将使用的指南是,为了从另一个类继承,确实应该存在一个“是”关系。因此,如果您编写一个名为BMW的课程,那么它可能会继承自Car,因为BMW确实是汽车。马类可以从哺乳动物类继承,因为马实际上是现实生活中的哺乳动物,任何哺乳动物功能都应与马相关。但是你能说一个团队是一个名单吗?据我所知,这似乎并不是一个团队真正的“名单”。因此,在这种情况下,我将有一个List作为成员变量。


9

我肮脏的秘密:我不在乎别人怎么说,我做到了。.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没有说明原因,那么这无疑是“最佳实践”。


9
尽管有勇气和主动性在适当的时候挑战接受的智慧很重要,但我认为明智的做法是首先着手理解为什么接受的智慧首先要被接受,然后再着手挑战它。-1是因为“忽略该准则!” “为什么存在此准则”不是一个好的答案。顺便说一句,在这种情况下,社区不仅“怪我”,而且提供了令人满意的解释,成功地说服了我。
极好的2014年

3
实际上,如果不作任何解释,您唯一的办法就是突破界限,进行测试,不要害怕违规。现在。问问自己,即使List <T>不是一个好主意。将继承从List <T>更改为Collection <T>会多么痛苦?猜测:与重构工具无关,无论代码多长,都比在论坛上发布花费更少的时间。现在,这是一个好问题,但不是一个实际的问题。
Nicolas Dorier 2014年

需要澄清的是:我当然不是在等待SO的祝福从我想要的内容继承而来,但是我想问的重点是要理解该决定应考虑的因素,以及为什么这么多有经验的程序员似乎拥有决定与我相反。Collection<T>与此不同,List<T>因此在大型项目中进行验证可能需要大量工作。
极好的2014年

2
team.ByName("Nicolas")意思"Nicolas"是团队的名字。
山姆·里奇

1
“不要添加不应该存在的约束”互为矛盾,不要暴露任何您没有充分理由暴露的东西。这不是纯粹主义或盲目的狂热,也不是最新的设计时尚趋势。它是基础的面向对象设计101,初学者水平。为什么存在这个“规则”不是秘密,它是几十年经验的结果。
萨拉

8

指南所说的是,公共API不应透露您是使用列表,集合,字典,树还是其他任何东西的内部设计决策。“团队”不一定是列表。您可以将其实现为列表,但是公共API的用户应在需要了解的基础上使用您的类。这使您可以更改决策并使用其他数据结构,而不会影响公共接口。


回想起来,在对@EricLippert和其他人的解释之后,您实际上已经对我的问题的API部分给出了很好的答案-除了您所说的之外,如果我愿意class FootballTeam : List<FootballPlayers>,那么该类的用户将可以告诉我List<T>通过使用反射继承了ve ,看到了List<T>对于足球队没有意义的方法,并且能够使用FootballTeamin List<T>,因此我将向客户(不必要地)展示实现细节
极好的

7

当他们说List<T>“经过优化”时,我想他们想表示它不具有像虚拟方法这样的功能,这些功能要贵一些。因此,问题在于,一旦您List<T>API公开了公共API,您就会失去执行业务规则或以后自定义其功能的能力。但是,如果您将此继承的类用作项目内部的对象(而不是作为API暴露给成千上万的客户/合作伙伴/其他团队的对象),则可以节省时间,并且可以使用此功能,重复。从中继承的优势List<T>是,您消除了很多愚蠢的包装程序代码,这些代码在可预见的将来永远不会被定制。另外,如果您希望您的类具有与以下对象完全相同的语义List<T> 在您API的生命周期中,也可能没问题。

我经常看到很多人仅仅因为FxCop规则这么说或者某人的博客说这是“坏”的做法而做大量的额外工作。很多时候,这将代码变成设计模式的怪诞怪诞。与许多准则一样,将其视为可能会有例外的准则。


4

尽管我没有像大多数答案那样进行复杂的比较,但我想分享我的方法来处理这种情况。通过扩展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
{
    ...
}

3

如果您的类用户需要List拥有的所有方法和属性**,则应从中派生您的类。如果不需要它们,则将List括起来,为类用户实际需要的方法包装。

如果您编写公共API或许多其他人会使用的其他代码,则这是一个严格的规则。如果您的应用程序很小且开发人员不超过2个,则可以忽略此规则。这样可以节省您一些时间。

对于小型应用程序,您还可以考虑选择另一种不太严格的语言。Ruby,JavaScript-可以减少编写代码的任何内容。


3

我只是想补充一点,埃菲尔铁塔公司(Eiffel)和按合同设计的发明者伯特兰德·迈耶(Bertrand Meyer)可以Team继承List<Player>而无需打眼皮。

在他的《面向对象的软件构造》一书中,他讨论了GUI系统的实现,其中矩形窗口可以具有子窗口。他只是Window继承了两者Rectangle,并重Tree<Window>用了实现。

但是,C#不是Eiffel。后者支持功能的多重继承和重命名。在C#中,当子类化时,您既继承接口又实现。您可以重写实现,但是调用约定是直接从超类复制的。但是,在Eiffel中,您可以修改公共方法的名称,因此可以在和中重命名Add和。如果将的实例向上转换回,则调用方将使用和修改它,但是将调用和的虚拟方法。RemoveHireFireTeamTeamList<Player>AddRemoveHireFire


1
这里的问题不是多重继承,mixins(等等)或如何处理它们的缺失。用任何一种语言,甚至用人类语言来说,团队都不是一个人的名单,而是由一个人的名单组成的。这是一个抽象的概念。如果Bertrand Meyer或任何通过子类化List管理团队的人做错了。如果您想要更具体的列表类型,则应该对列表进行子类化。希望你同意。
Mauro Sampietro

1
那取决于继承是什么。就其本身而言,它是一个抽象操作,并不意味着两个类处于一种“特殊的”关系中,即使这是主流的解释。但是,如果语言设计支持继承,则可以将其视为实现重用的一种机制,即使如今组合是首选的选择。
阿列克谢2015年

2

我认为我不同意您的概括。一支球队不仅仅是一群球员。团队拥有有关此的更多信息-名称,标志,管理人员/管理人员的集合,教练组的集合以及球员的集合。因此,您的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();
}

2

优先于类的接口

类应避免从类派生,而应实现必要的最小接口。

继承破坏封装

从类派生破坏封装

  • 公开有关如何实现您的收藏的内部细节
  • 声明可能不合适的接口(一组公共函数和属性)

除其他外,这使得重构代码更加困难。

类是实现细节

类是应从代码的其他部分隐藏的实现细节。简而言之,a System.List是抽象数据类型的特定实现,它在现在和将来都可能适用或可能不适用。

从概念上讲,System.List数据类型称为“列表” 的事实有点让人讨厌。A System.List<T>是一种可变的有序集合,支持用于添加,插入和删除元素的摊销O(1)操作,以及用于检索元素数量或按索引获取和设置元素的O(1)操作。

接口越小,代码越灵活

设计数据结构时,接口越简单,代码越灵活。只需看一下LINQ有多强大,就可以对此进行演示。

如何选择接口

当您认为“列出”时,您应该对自己说:“我需要代表一群棒球运动员”。因此,假设您决定使用一个类对此进行建模。您首先应该确定此类需要暴露的最小接口数量。

可以帮助指导此过程的一些问题:

  • 我需要计数吗?如果不考虑实施IEnumerable<T>
  • 初始化后,这个集合会改变吗?如果不考虑的话IReadonlyList<T>
  • 可以按索引访问项目是否重要?考虑ICollection<T>
  • 将项目添加到集合中的顺序重要吗?也许是ISet<T>
  • 如果您确实想要这些东西,请继续实施IList<T>

这样,您就不会将代码的其他部分耦合到棒球运动员集合的实现细节上,并且只要尊重界面,就可以自由更改实现方式。

通过采用这种方法,您将发现代码变得更易于阅读,重构和重用。

关于避免样板的注意事项

在现代IDE中实现接口应该很容易。右键单击并选择“实现接口”。然后根据需要将所有实现转发到成员类。

就是说,如果您发现编写了很多样板,则可能是因为您要公开的功能超出了应有的范围。这是您不应从类继承的相同原因。

您还可以设计对您的应用程序有意义的较小接口,也许还可以设计几个辅助扩展功能,以将这些接口映射到您需要的任何其他接口。这是我在自己IArrayLinqArray库接口中采用的方法


2

序列化问题

一方面缺少。从List <>继承的类无法使用XmlSerializer 正确序列化。在这种情况下,必须改用DataContractSerializer,或者需要自己的序列化实现。

public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized:
    string AnyPropertyInDerivedFromList { get; set; }     
}

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

进一步阅读:从List <>继承一个类时,XmlSerializer不会序列化其他属性


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> { }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.