题 为什么不从List 继承?


在规划我的程序时,我经常从一连串的想法开始:

足球队只是一个足球运动员名单。因此,我应该代表它:

var football_team = new List<FootballPlayer>();

此列表的顺序表示球员在名单中列出的顺序。

但我后来才意识到,除了仅仅是球员名单之外,球队还有其他属性,必须记录下来。例如,本季运行总分,当前预算,统一颜色,a string 代表团队的名称等。

那么我认为:

好吧,足球队就像一个球员名单,但另外,它有一个名字(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,我会遇到什么困难?

为什么它甚至重要?列表是一个列表。什么可能改变?我可能想要改变什么?

最后,如果微软不希望我继承 List,他们为什么不上课 sealed

我还应该使用什么呢?

显然,对于自定义集合,Microsoft提供了一个 Collection 应该扩展的类而不是 List。但是这堂课很光,并没有很多有用的东西, AddRange, 例如。 jvitor83的回答 提供了该特定方法的性能原理,但是如何缓慢 AddRange 不比没有好 AddRange

继承自 Collection 比继承更多的工作 List,我认为没有任何好处。当然微软不会告诉我无缘无故地做额外的工作,所以我不禁感到我在某种程度上误解了某些东西,并继承了 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#方式是什么,“逻辑上”(也就是说,“对人类的思维”)只是一个 list 的 things 有几个铃铛和口哨?

是继承自 List<T> 总是不能接受?什么时候可以接受?为什么/为什么不呢?程序员在决定是否继承时必须考虑什么 List<T> 或不?


979
2018-02-11 03:01


起源


那么,您的问题是否真的何时使用继承(Square是一个矩形,因为在请求通用Rectangle时提供Square总是合理的)以及何时使用合成(此外,FootballTeam还有一个FootballPlayers列表,另外到其他属性,就像它的“基本”,如名称)?如果代码中的其他地方在他们期待一个简单的List时向他们传递了一个FootballTeam,那么其他程序员是否会感到困惑? - Flight Odyssey
如果我告诉你一个足球队是一家商业公司,哪个OWNS有一份球员合同列表而不是一个有一些额外属性的球员名单呢? - STT LCU
这真的很简单。足球队是一支球员吗?显然不是,因为就像你说的那样,还有其他相关属性。足球队是否有球员名单?是的,所以它应该包含一个列表。除此之外,组合通常比继承更好,因为以后更容易修改。如果您同时找到继承和组合逻辑,请使用组合。 - Tobberoth
@FlightOdyssey:假设你有一个方法SquashRectangle,它接受一个矩形,将其高度减半,并使其宽度加倍。现在传入一个矩形 发生 是4x4但是是矩形类型,是4x4的正方形。调用SquashRectangle后对象的尺寸是多少?对于矩形,它明显是2x8。如果正方形是2x8那么它不再是正方形;如果它不是2x8,则对相同形状的相同操作会产生不同的结果。结论是一个可变的正方形 不 一种矩形。 - Eric Lippert
@Gangnus:你的陈述完全是假的;一个真正的子类需要具有一个功能 超 超类的。一个 string 需要做一切 object 可以做 和更多。 - Eric Lippert


答案:


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

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

要求任何十位熟悉足球存在的非计算机程序员填补空白:

A football team is a particular kind of _____

难道 任何人 说“有一些花里胡哨的足球运动员名单”,还是他们都说“运动队”或“俱乐部”或“组织”?你认为足球队是谁 一种特殊的球员名单 是在你的人类思想和你的人类思想中。

List<T> 是一个 机制。足球队是一个 业务对象  - 也就是说,一个代表某个概念的对象 业务领域 该计划。不要混合那些!一支足球队 是一种 球队;它 有一个 名册,名册 是一个球员名单。名单不是 特别类型的球员名单。名册  球员名单。所以制作一个名为的房产 Roster 这是一个 List<Player>。并成功 ReadOnlyList<Player> 当你在这里时,除非你相信每个了解足球队的人都会从名单中删除球员。

是继承自 List<T> 总是不能接受?

不能接受谁?我?没有。

什么时候可以接受?

当你构建一个机制时 扩展了 List<T> 机制

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

我在建一个 机制 或者a 业务对象

但那是很多代码!我能为所有这些工作获得什么?

你花了更多的时间来输入你的问题,它会让你为相关成员编写转发方法 List<T> 五十多次。你显然不害怕冗长,我们在这里讨论的代码非常少;这是几分钟的工作。

UPDATE

我更多地考虑了一下,还有另一个原因就是不要将足球队的模型列为球员名单。事实上,将足球队建模为一个糟糕的主意  一份球员名单。团队作为/拥有一个球员名单的问题是你得到的是一个 快照 的团队 在某个时刻。我不知道你的这个课程的商业案例是什么,但如果我有一个代表足球队的课程,我想问一些问题,比如“有多少海鹰队员因2003年至2013年的伤病错过了比赛?”或者“之前为另一支球队效力的丹佛球员在场上的同比增幅最大?”要么 ”今年小猪队一路走了吗?

也就是说,在我看来,足球队的模型很好 一系列历史事实 例如当一名球员被招募,受伤,退役等时。显然当前的球员名单是一个重要的事实,应该是前锋和中锋,但是你可能还有其他有趣的事情需要这个对象需要更多的历史观点。


1140
2018-02-11 05:43



虽然其他答案非常有用,但我认为这个答案最直接地解决了我的问题。至于夸大代码量,你说得对,但最终并没有那么多工作,但是当我在程序中包含一些代码而不理解为什么要包含它时,我很容易感到困惑。 - Superbest
@Superbest:很高兴我能提供帮助!你是正确的听取这些疑虑;如果你不明白为什么要写一些代码,要么弄明白,要么编写你理解的不同代码。 - Eric Lippert
@Mehrdad但你似乎忘记了优化的黄金规则:1)不要这样做,2)不要这样做,3)如果没有先做性能配置文件来显示需要优化的事情,就不要这样做。 - AJMansfield
@Mehrdad:老实说,如果你的应用程序要求你关心虚拟方法的性能负担,那么任何现代语言(C#,Java等)都不适合你。我这边有一台可以运行的笔记本电脑 模拟 我小时候玩的每个街机游戏 同时。这让我不必担心这里和那里的间接水平。 - Eric Lippert
@Superbest:我们现在 无疑 在“组成不继承”的频谱结束。火箭是 由火箭部分组成。火箭不是“一种特殊的零件清单”!我建议你进一步削减它;为什么列表,而不是 IEnumerable<T>  - 一个 序列? - Eric Lippert


最后,有人建议将List包装成一些东西:

这是正确的方法。 “不必要的罗嗦”是一种看待这种情况的坏方法。你写的时候它有明确的含义 my_team.Players.Count。你想要算上球员。

my_team.Count

..什么也没有。算什么?

团队不是一个列表 - 不仅仅是一个玩家列表。一支球队拥有球员,因此球员应该参与其中(成员)。

如果你是  担心它过于冗长,你总是可以暴露团队中的属性:

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

..which变成:

my_team.PlayerCount

这现在有意义并且坚持 得墨忒耳定律

你还应该考虑坚持 复合重用原理。通过继承 List<T>,你说的是一个团队 是一个 球员名单和暴露出不必要的方法。这是不正确的 - 正如你所说,一个团队不仅仅是一个球员名单:它有一个名字,经理,董事会成员,培训师,医务人员,工资帽等。通过让你的团队成员包含一个球员名单,你'说'团队 有一个 球员名单“,但它也可以有其他东西。


232
2018-02-11 03:05



我将措辞改为“详细”以使其更清晰。我不明白你为什么这么说 my_team.Count “毫无意义”。对我而言,“统计团队”是一个非常有意义的概念。我明白为什么你可能会坚持认为一个人真的“算上一个团队的球员” - 这是一个哲学上的区别吗?还是有一个实际的原因,为什么包装更好?我抱怨额外的工作,因为每次我回到那段代码我会想,“等等,为什么我把它包起来?为什么不继承呢?”我也无法提出解释它的评论。 - Superbest
@Superbeast如果我对你说:“我今天加入了X队”。你会认为我是一名球员吗?或者要求澄清我在团队中的角色? - Simon Whitehead
我不是故意说点,但我认为你是球员。而且,如果你加入另一个职位,我希望你明确说出来。 - Superbest
一般来说,你应该尽可能避免继承。部分原因是在.net中,我们只能从一个类继承。所以一旦你继承了 List,你不能继承别的东西。因此,如果您以后想要使用 ObservableCollection (对于数据绑定),您将引入一个重大变化。 - Aron
@SimonWhitehead我不同意你的建议,看看“软件架构的POV”。问题空间中的语义分析结果应该完全不受计算机空间约束的影响。对我来说,最初的问题是错误的语义分析:“Johny加入X队”并不意味着团队是一个球员名单。 - franssu


哇,你的帖子有很多问题和要点。你从微软获得的大多数推理都是恰到好处。让我们从一切开始 List<T>

  • List<T>   高度优化。它的主要用途是用作对象的私有成员。
  • 微软没有对它进行封锁,因为有时你可能想要创建一个具有更友好名称的类: 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.
}

110
2018-02-11 03:26



“你的帖子有一整套问题“ - 好吧,我确实说过我很困惑。感谢链接到@ Readonly的问题,我现在看到我的问题的一个重要部分是更一般的组合与继承问题的特殊情况。 - Superbest
@Brian:除了你不能使用别名创建泛型之外,所有类型都必须具体。在我的例子中, List<T> 作为私有内部类扩展,使用泛型来简化使用和声明 List<T>。如果扩展只是 class FootballRoster : List<FootballPlayer> { },然后是的,我可以使用别名代替。我想值得注意的是,您可以添加其他成员/功能,但它是有限的,因为您无法覆盖重要成员 List<T>。 - m-y
如果这是Programmers.SE,我同意当前的答案投票范围,但是,因为它是一个SO帖子,这个答案至少试图回答C#(.NET)特定的问题,即使有明确的一般性设计问题。 - Mark Hurd
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden 现在 那 是一个不从List继承的好理由。其他答案有太多的哲学。 - Navin
@m-y:我怀疑你的意思是“一大堆”问题,因为侦探是个侦探。 - batty


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

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

People playing football

无论如何,这段代码(来自m-y的回答)

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.
}

意思是:这是一支拥有管理,球员,管理员等的足球队。如:

Image showing members (and other attributes) of the Manchester United team

这是你的逻辑在图片中呈现的方式......


104
2018-02-11 15:45



它现在需要的只是一个SackManager()方法...... - Phil Cooper
我希望你的第二个例子是足球俱乐部,而不是足球队。足球俱乐部有经理和管理员等。 从这个来源:“足球队是一群人的集体名称 玩家 ......这样的球队可以被选中参加对阵对手的比赛,代表一个足球俱乐部......“。所以我认为这是一支球队 是 玩家列表(或者更准确地说是玩家的集合)。 - Ben
更优化 private readonly List<T> _roster = new List<T>(44); - SiD
有必要导入 System.Linq 命名空间。 - Davide Cannizzo


这是一个典型的例子 组成 VS 遗产

在这个特定情况下:

该团队是否具有添加行为的玩家列表

要么

团队是否属于自己的对象,恰好包含一系列玩家。

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

  1. 您无法限制访问权限(例如,阻止人们更改名单)。无论是否需要/想要它们,您都可以获得所有List方法。

  2. 如果你想要列出其他东西,会发生什么。例如,团队有教练,经理,粉丝,设备等。其中一些可能就是他们自己的名单。

  3. 您限制了继承选项。例如,您可能想要创建一个通用的Team对象,然后继承BaseballTeam,FootballTeam等。要从List继承,您需要从Team继承,但这意味着所有不同类型的团队都被迫拥有该名册的相同实现。

合成 - 包括在对象中提供所需行为的对象。

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

两者都有它们的用途,但这是一个明显的情况,其中组成是优选的。


81
2018-02-11 10:58



扩展到#2,对List来说没有意义 有一个 名单。 - Cruncher
首先,问题与构成与继承无关。答案是OP不想实现更具体的列表,所以他不应该扩展List <>。我扭曲了因为你在stackoverflow上有很高的分数并且应该清楚地知道这一点并且人们相信你说的话,所以到目前为止,55个投票赞成并且他们的想法混淆的人相信一种方式或者另一种方式可以构建一个系统,但显然不是! #无愤怒 - sam
@sam他想要列表的行为。他有两个选择,他可以扩展列表(继承)或者他可以在他的对象(组合)中包含一个列表。也许你误解了问题或答案的一部分,而不是55个人错了,你说得对吗? :) - Tim B
是。这是一个退化的案例,只有一个超级和次级,但我对他的具体问题给出了更一般的解释。他可能只有一个团队,并且可能总是有一个列表,但选择仍然是继承列表或将其作为内部对象包含在内。一旦你开始包括多种类型的球队(足球。板球等等),他们开始不仅仅是一个球员名单,你会看到你如何拥有完整的组合与继承问题。在这样的情况下看大局是很重要的,以避免早期错误的选择,然后意味着很多重构。 - Tim B
OP没有要求撰写,但用例是X:Y问题的一个明确的例子,其中4个面向对象编程原则中的一个被破坏,误用或未完全理解。更明确的答案是编写一个专门的集合,如Stack或Queue(似乎不适合用例)或者理解组合。足球队不是足球运动员名单。 OP是否明确要求它是无关紧要的,如果不了解抽象,封装,继承和多态,他们就无法理解答案,因此,X:Y amok - J.T. McGuigan


正如大家所指出的那样,一队球员不是球员名单。这个错误是由世界各地的许多人做出的,也许是在各种专业水平上。通常问题是微妙的,偶尔也很严重,就像在这种情况下一样。这样的设计很糟糕,因为这些违反了 利斯科夫替代原则。互联网有很多很好的文章解释这个概念,例如, http://en.wikipedia.org/wiki/Liskov_substitution_principle 


43
2018-02-11 16:56



列表与集合的好处。这似乎只是一个太常见的错误。当集合中只能有一个元素实例时,某种集合或字典应该是实现的首选候选者。拥有一个拥有两名同名球员的球队是可以的。将一个玩家同时包含两次是不行的。 - Fred Mitchell
虽然更重要的是要问“数据结构能否合理地支持所有有用的东西(理想情况下是短期和长期)”,而不是“数据结构是否做得多而不是有用”,这一点更为重要。 - Tony Delroy
@TonyD嗯,我提出的要点不是应该检查“数据结构是否超出有用的数量”。它是检查“如果父数据结构对Child类可能暗示的行为做了一些无关紧要,无意义或反直觉的事情”。 - Satyan Raina
@SatyanRaina:其中,只要它们实际上不会损害有意义且将被使用的操作,那么能够做出无关或无意义的额外事情并没有错。例如,如果平衡二叉树碰巧按键排序顺序迭代元素并且这不是必需的,那么它实际上也不是问题。例如。 “包含玩家的顺序[正在]描述团队的状态”:如果不需要订单,或者仍然可以有效地方便地形成可能需要的多个订单,则特定的默认订单不会造成伤害。 - Tony Delroy
@TonyD实际上存在从父亲派生的无关特征的问题,因为在许多情况下它会使否定测试失败。程序员可以扩展Human {eat();跑();来自大猩猩的写();} {eat();跑(); swing();}认为人类没有任何问题,具有能够摆动的额外特征。然后在游戏世界中,你的人类突然开始绕过所有假设的土地障碍,只需在树上摆动。除非明确规定,否则实际的人类应该无法摆动。这样的设计使api非常容易被滥用和混淆 - Satyan Raina


怎么样? 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. 我的班级名字是什么意思吗?


34
2018-02-12 11:41



一个完美的例子来说明为什么OP的想法很糟糕。 +1 - nawfal
如果每个玩家的类型将其归类为属于任何一个 DefensivePlayers, OffensivePlayers, 要么 OtherPlayers,拥有一个可以被期望a的代码使用的类型可能是合法有用的 List<Player> 但也包括成员 DefensivePlayers, OffsensivePlayers, 要么 SpecialPlayers 类型 IList<DefensivePlayer>, IList<OffensivePlayer>,和 IList<Player>。可以使用单独的对象来缓存单独的列表,但是将它们封装在与主列表相同的对象中看起来更干净[使用列表枚举器的失效... - supercat
...作为主列表已经改变并且下次访问时需要重新生成子列表这一事实的提示。 - supercat
虽然我同意这一观点,但是真的让我的灵魂分崩离析,看到有人提出设计建议,并建议在业务对象中公开具有公共getter和setter的具体List <T> :( - kai


首先,它与可用性有关。如果你使用继承,那么 Team class将公开纯粹为对象操作而设计的行为(方法)。例如, AsReadOnly() 要么 CopyTo(obj) 方法对团队对象毫无意义。而不是 AddRange(items) 你可能想要一个更具描述性的方法 AddPlayers(players) 方法。

如果要使用LINQ,请实现通用接口,如 ICollection<T> 要么 IEnumerable<T> 会更有意义。

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


27
2018-02-11 03:25





设计>实施

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

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

所以你的第一个问题应该是:

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

请记住,继承意味着“isa”(是a)关系,而组合意味着“有”(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 ++中尝试过这个malarky,你可能会认为这是一件好事。)通过继承和一个通过组合实现一个集合很可能 感觉 脏。除非你明确地实现ILIst <Player> .Count和IList <StaffMember> .Count等,否则Count等属性会让用户感到困惑,然后它们只是痛苦而不是混乱。你可以看到它的发展方向;在思考这条道路时,直觉可能会告诉你朝这个方向前进是错误的(正确或错误的是,如果你以这种方式实施它,你的同事也可能!)

简答(太迟)

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


25
2018-02-11 22:27





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

这在逻辑上是错误的:

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

这是正确的:

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

23
2018-02-11 16:25



这没有解释 为什么。 OP知道大多数其他程序员都有这种感觉,他只是不明白为什么这很重要。这只是提供问题中的信息。 - Servy
这是我第一个想到的,他的数据是如何被错误地建模的。所以原因是你的数据模型不正确。 - rball
为什么不从列表继承?因为他不想要更具体的清单。 - sam
我不认为第二个例子是正确的。您正在暴露状态,因此打破了封装。状态应该隐藏/对象内部,并且应该通过公共方法来改变这种状态。确切地说哪些方法可以从业务需求中确定,但它应该是“现在需要这个” - 基础,而不是“在某些时候我可能不方便” - 基础。保持你的api紧,你将节省自己的时间和精力。 - kai
封装不是问题的关键。如果您不希望该类型以更具体的方式运行,我会专注于不扩展类型。那些字段应该是c#中的autoproperties和其他语言中的get / set方法,但是在这种情况下这是完全多余的 - sam


这取决于具体情况

当你将你的球队视为球员名单时,你就会将足球队的“想法”投射到一个方面:你将“球队”减少到你在场上看到的人。此投影仅在某些上下文中是正确的。在不同的背景下,这可能是完全错误的。想象一下,你想成为团队的赞助商。所以你必须和团队的经理谈谈。在此背景下,团队将被列入其经理名单。而这两个列表通常不会重叠。其他情况是当前与前任球员等。

不明确的语义

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

类是可扩展的

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

课程更复杂

在某些情况下,创建一个额外的类可能有点过分。每个类定义必须通过类加载器加载,并由虚拟机缓存。这会花费您的运行时性能和内存。当你有一个非常具体的背景时,可以将足球队视为球员名单。但在这种情况下,你应该真的只使用一个 IList ,不是从中衍生出来的一类。

结论/考虑因素

当您拥有非常具体的上下文时,可以将团队视为玩家列表。例如,在一个方法中,写入是完全可以的

IList<Player> footballTeam = ...

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

type FootballTeam = IList<Player>

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


19
2018-02-11 10:10