为什么不从List <T>继承?

Sup*_*est 1299 .net c# oop inheritance list

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

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

var football_team = new List<FootballPlayer>();
Run Code Online (Sandbox Code Playgroud)

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

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

那么我认为:

好吧,足球队就像一个球员名单,但另外,它有一个名称(a string)和一个总分(a int)..NET不提供用于存储足球队的类,所以我将创建自己的类.最相似和相关的现有结构是List<FootballPlayer>,所以我将继承它:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}
Run Code Online (Sandbox Code Playgroud)

但事实证明,指南说你不应该继承List<T>.我在两个方面完全被这个指南搞糊涂了.

为什么不?

显然List是以某种方式针对性能进行了优化.怎么会这样?如果我延长会给我带来什么性能问题List?究竟会打破什么?

我看到的另一个原因List是微软提供的,我无法控制它,所以我在以后暴露"公共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; 
}
Run Code Online (Sandbox Code Playgroud)

这有两个问题:

  1. 它使我的代码不必要地冗长.我现在必须打电话my_team.Players.Count而不是公正my_team.Count.值得庆幸的是,使用C#我可以定义索引器以使索引透明,并转发内部的所有方法List......但这是很多代码!我能为所有这些工作获得什么?

  2. 它只是简单没有任何意义.足球队没有"拥有"球员名单.这球员名单.你没有说"John McFootballer加入了SomeTeam的球员".你说"约翰加入了SomeTeam".您不要在"字符串的字符"中添加字母,而是在字符串中添加字母.您不会将书籍添加到图书馆的书籍中,而是将图书添加到图书馆.

我意识到"引擎盖下"发生的事情可以说是"将X添加到Y的内部列表中",但这似乎是一种非常反直觉的思考世界的方式.

我的问题(总结)

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

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

Eri*_*ert 1467

这里有一些很好的答案.我会向他们添加以下几点.

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

问任何十个熟悉足球存在的非计算机程序员填写空白:

A football team is a particular kind of _____
Run Code Online (Sandbox Code Playgroud)

没有任何人说"有一些花俏的足球运动员名单",还是他们都说"运动队"或"俱乐部"或"组织"?你认为橄榄球队是一种特殊的球员名单的想法就在于你的人类思想和人类的思想.

List<T>是一种机制.Football team是一个业务对象 - 也就是代表程序业务领域中某个概念的对象.不要混合那些!足球队是一种团队; 它有一个名单,名单是一个球员名单.名单不是一种特殊的球员名单.名单一个球员名单.因此,让一个名为属性Roster这是一个List<Player>.ReadOnlyList<Player>除非你相信每个了解足球队的人都会从球员名单中删除球员,否则在你参加比赛的同时做到这一点.

继承List<T>总是不可接受的?

谁不能接受?我?没有.

什么时候可以接受?

当您构建扩展List<T>机制的机制时.

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

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

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

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

UPDATE

我更多地考虑了一下,还有另一个原因就是不要将足球队的模型列为球员名单.事实上,将足球队建模为拥有球员名单可能是一个坏主意.团队作为/拥有一个玩家列表的问题在于,你所获得的是团队在某个时刻快照.我不知道你们这个班级的商业案例是什么,但是如果我有一个代表足球队的课程,我想问一下诸如"有多少海鹰队球员在2003年至2013年期间因伤缺席比赛?"之类的问题.或者"之前为另一支球队效力的丹佛球员在场上的同比增幅最大?" 或者" 今年小猪队一路走了吗? "

也就是说,在我看来,足球队很好地模仿了历史事实的集合,例如当一名球员被招募,受伤,退役等时.显然,当前的球员名单是一个重要的事实,应该是前面和 -中心,但是对于需要更多历史视角的对象,您可能还有其他有趣的事情要做.

  • @Superbest:很高兴我能帮到你!你是正确的听取这些疑虑; 如果你不明白为什么要写一些代码,要么弄明白,要么编写你理解的不同代码. (120认同)
  • @Mehrdad:老实说,如果你的应用程序要求你关心虚拟方法的性能负担,那么任何现代语言(C#,Java等)都不适合你.我在这里有一台笔记本电脑可以同时运行我作为一个孩子*玩的每个街机游戏的*模拟*.这使我不必担心这里和那里的间接水平. (100认同)
  • 虽然其他答案非常有用,但我认为这个答案最直接地解决了我的问题.至于夸大代码量,你说得对,但最终并没有那么多工作,但是当我在程序中包含一些代码而不理解为什么要包含它时,我很容易感到困惑. (79认同)
  • @Mehrdad但你似乎忘记了优化的黄金规则:1)不要这样做,2)不要这样做,3)如果没有先做性能配置文件来显示需要优化的东西,就不要这样做. (44认同)
  • @Superbest:现在我们绝对是*在"组合不继承"的结尾.火箭由火箭部件组成*.火箭不是"一种特殊的零件清单"!我建议你进一步削减它; 为什么是列表,而不是`IEnumerable <T>` - *序列*? (41认同)
  • @MattJohnson:谢谢你的链接.如何教授OOP的一个讽刺是,"银行账户"通常是给学生的早期练习.当然,如何教它们模拟它与银行软件中真实银行账户建模的方式完全相反.在真实软件中,帐户是可以计算余额的借方和贷方的仅附加列表,而不是在更新发生时销毁和替换的可变余额. (7认同)
  • Piggers今年并没有全力以赴.那好吧. (6认同)
  • 所谓的"机制"和"业务对象"之间的区别并没有解释为什么List <T>不应该扩展到"业务对象".每个对象都是"机制",尽管并不总是像这样的通用元素那么清晰和简单. (4认同)
  • @EricLippert:如果您不需要担心它,那么您不必担心它.如果你这样做,那么你做.我所说的只是当它成为瓶颈时,继承而不是组合可以成为一种解决方案.你没理由因为遇到某个瓶颈而抛弃整个语言...... (4认同)
  • 抱歉,埃里克(Eric),我不同意您的假设,即一支球队不是球员名单。我建议一个“足球队”是球员名单,而一个“足球俱乐部”是球员名单。俱乐部有经理和历史等,但一个团队可能只存在一场比赛。我认为答案和评论已经交替使用“团队”和“俱乐部”这两个术语,这导致了混乱。[团队一词的定义]:“一群球员……”,所以我认为一个球队是一个球员名单。 (4认同)
  • @Ben你在"团队"和"俱乐部"之间的区别在大多数人没有首先为特定的背景定义它们时会丢失.在我读你的评论之前,如果你问我足球队和足球俱乐部之间的区别,我会说足球队在北美有很多线路的球场上玩一个细长的球,而一个足球俱乐部在英国,在球场上用几个线条打球.换句话说,团队和俱乐部之间的差异大致类似于卡车和卡车之间的差异. (4认同)
  • 我没有看到的一点是,足球队很可能迟早会包含除了球员之外的其他东西.(设备?*其他*员工?)并且列表中包含更多列表(而不是该列表的子项)将会很奇怪.我之前做过这个.这很奇怪. (4认同)
  • @WernerCD:我绝对同意.原始海报没有给出我们该项目的业务是什么,只是足球队参与其中.如果这是游戏的模拟,这是一回事.如果它打算模拟现实,那就完全是另一个. (3认同)
  • 我完全同意你的上一次发言.很明显*每个人*在这里都同意 - 足球队不是*球员名单.问题在于它完全忽略了问题的关键点.在这个问题中使用这是一个不幸的例子,因为每个人对它的内心反应都完全掩盖了实际有效的问题.可能更好的说他想扩展`List <T>`以获得他的`Roster`属性中的一些小功能.然后,我们可能已经能够讨论MS建议不要扩展`List <T>`的原因,以及何时适合这样做. (3认同)
  • “什么时候可以接受?*当您正在构建一种扩展`List&lt;T&gt;`机制的机制时。*”...或者当您必须关心额外间接级别的性能时,似乎没有人关心这一点曾经提到过。 (2认同)
  • "显然,目前的球员名单是一个重要的事实,应该是前沿和中锋,但是你可能想要对这个需要更多历史观点的对象做其他有趣的事情." - 考虑一下YAGNI?当然你可能在近期或远期的某个时候需要看看那些东西......但你现在需要吗?建立未来需求的可能性(可能存在或可能不存在......)正在浪费精力. (2认同)
  • 关于您的更新:我实际上是在尝试制作一个 Delta-V 计算器,它消耗了一系列火箭部件(具有质量和燃料量等属性)。事实上,火箭没有单一的零件清单,但它的每个阶段都有不同的零件集。我通过给火箭一个 `List&lt;Stage&gt;` 来处理它,其中每个 `Stage` 都有一个 `List&lt;RocketPart&gt;` - 尽管我对这个特定程序之外的一般问题感到好奇。 (2认同)
  • @EricLippert - 如果你还没有,你应该看一下[Fowler's Temporal Patterns](http://martinfowler.com/eaaDev/timeNarrative.html)的文章.:)但是,您是否需要这样做很大程度上取决于业务用例*和存储/检索*要求.例如,您可能希望对象仅表示快照,但让您的数据库基础结构了解时间方面.我完成了[在这个项目中](https://github.com/mj1856/RavenDB-TemporalVersioning). (2认同)
  • @Mehrdad:重申Eric Lippert所说的,考虑到运行时已经在做的所有事情,有些东西总是比选择组合更糟糕的瓶颈.任何如此离奇以至于纯粹从继承中移动得更快的东西可能不应该用C#编程,这肯定已经影响了性能. (2认同)
  • @SeanLynch:The Oatmeal的作者住在西雅图,我强烈怀疑Piggers实际上是海鹰队.所以是PIGGERS今年所有的方式都是WOOO! (2认同)
  • @my:更好的指标是什么? (2认同)
  • [这里是另一句名言](http://en.wikipedia.org/wiki/Football_team):“足球队是赋予一组球员的集体名称……可以选择在比赛中踢球。代表足球俱乐部”。在体育运动中,“团队”一词是一组运动员的统称。因此,我认为说一支球队是一个球员名单(或更准确地说是一个球员集合)是合理的。 (2认同)
  • 我敢肯定,其他答案已经有了这点:即使您在哪里创建模型,而只对当前的一组玩家感兴趣,列表也将是错误的。列表可能包含重复项,并且列表的第一位表示该顺序很重要。您不希望在花名册中重复,而在花名册中,顺序并不重要。不要使用列表,而应该使用集合。如果您想将属性映射到玩家,则应使用地图/词典,而不要像列表那样浮躁。 (2认同)
  • @RobertAchmann关键是使公开列表的* public属性*为只读。当然,列表的内部表示对于Team类仍然是可变的。 (2认同)

m-y*_*m-y 149

哇,你的帖子有很多问题和要点.从微软获得的大多数推理都是恰到好处的.让我们从一切开始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.
}
Run Code Online (Sandbox Code Playgroud)

  • @my:我怀疑你的意思是"回避"问题,因为侦探是个侦探. (9认同)
  • `Collection <T>允许覆盖成员(即添加,删除等).现在****是不从List继承的好理由.其他答案有太多的哲学. (6认同)
  • "*你的帖子有一整套问题*" - 好吧,我确实说过我很困惑.感谢您链接到@ Readonly的问题,我现在看到我的问题的很大一部分是更一般的组合与继承问题的特殊情况. (5认同)
  • 如果这是Programmers.SE,我同意目前的答案投票范围,但是,因为它是一个SO帖子,这个答案至少试图回答C#(.NET)特定问题,即使有明确的一般性设计问题. (5认同)
  • 你完全没有抓住重点。`foreach` 不关心 `Enumerator` 是否实现了 `IEnumerable&lt;T&gt;` 接口。如果它可以在`Enumerator` 上找到它需要的方法,那么它就不会使用`IEnumerable&lt;T&gt;`。如果您实际反编译代码,您可以看到 foreach 在迭代 `List&lt;T&gt;` 时如何避免使用虚拟调度调用,但会使用 `IList&lt;T&gt;`。 (3认同)
  • @Brian:除了不能使用别名创建泛型外,所有类型都必须是具体的。在我的示例中,“ List &lt;T&gt;”被扩展为私有内部类,该类使用泛型简化了“ List &lt;T&gt;”的用法和声明。如果扩展名只是`class FootballRoster:List &lt;FootballPlayer&gt; {}`,那么可以使用别名代替。我想应该指出您可以添加其他成员/功能,但这是有限的,因为您不能覆盖`List &lt;T&gt;`的重要成员。 (2认同)
  • 如果你调用 `List&lt;T&gt;.GetEnumerator()`,你会得到一个名为 `Enumerator` 的结构体。如果你调用 `IList&lt;T&gt;.GetEnumerator()`,你会得到一个 `IEnumerable&lt;T&gt;` 类型的变量,它恰好包含所述结构的盒装版本。在前一种情况下,foreach 将直接调用方法。在后者中,所有调用都必须通过接口进行虚拟调度,从而使每次调用变慢。(在我的机器上大约慢两倍。) (2认同)

Nea*_*hal 136

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}
Run Code Online (Sandbox Code Playgroud)

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

人们在踢足球

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

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.
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 我希望你的第二个例子是足球俱乐部,而不是足球队.足球俱乐部有经理和管理员等[来自此来源](http://en.wikipedia.org/wiki/Football_team):"足球队是一组**球员的集体名称**..可以选择这样的球队参加对阵对手的比赛,代表足球俱乐部......".所以我认为一个团队_是一个球员名单(或者更准确地说是球员的集合). (7认同)
  • 更优化的`private readonly List &lt;T&gt; _roster = new List &lt;T&gt;(44);` (3认同)
  • 有必要导入`System.Linq`命名空间。 (2认同)

Tim*_*m B 112

这是组合继承的典型例子.

在这个特定情况下:

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

要么

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

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

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

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

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

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

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

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

  • @sam他想要列表的行为.他有两个选择,他可以扩展列表(继承)或者他可以在他的对象(组合)中包含一个列表.也许你误解了问题或答案的一部分,而不是55个人错了,你说得对吗?:) (6认同)
  • 是.这是一个堕落的案例,只有一个超级和次级,但我对他的具体问题给出了更一般的解释.他可能只有一个团队,并且可能总是有一个列表,但选择仍然是继承列表或将其作为内部对象包含在内.一旦你开始包括多种类型的球队(足球.板球等等),他们开始不仅仅是一个球员名单,你会看到你如何拥有完整的组合与继承问题.在这样的情况下看大局是很重要的,以避免早期错误的选择,这意味着以后会进行大量的重构. (4认同)
  • OP并未要求组合,但是用例是X:Y问题的明确示例,其中四个面向对象的编程原理之一被破坏,滥用或未完全理解。更为明确的答案是编写一个专门的集合(例如Stack或Queue(看起来不适合用例))或了解组成。足球队不是足球运动员名单。OP是否明确要求是无关紧要的,如果不了解抽象,封装,继承和多态性,他们将无法理解答案,因此,X:是 (3认同)

Sat*_*ina 66

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

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

  • 儿童应该不要求完全定义父母的特征.
  • 除了完全定义Child之外,Parent不应该要求任何特征.

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

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

  • 足球队是足球运动员的名单吗?(列表的所有属性是否适用于具有相同含义的团队)
    • 团队是一个同质实体的集合吗?是的,团队是玩家的集合
    • 是否包含了描述团队状态的玩家的顺序,团队是否确保序列被保留,除非明确更改?不,不
    • 球员是否会根据他们在球队中的排名位置被列入/弃牌?没有

如您所见,只有列表的第一个特征适用于团队.因此,团队不是一个列表.列表将是您如何管理团队的实现细节,因此它应该仅用于存储播放器对象并使用Team类的方法进行操作.

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

  • 列表与集合的好处.这似乎只是一个太常见的错误.当集合中只能有一个元素实例时,某种集合或字典应该是实现的首选候选者.拥有一个拥有两名同名球员的球队是可以的.将一个玩家同时包含两次是不正确的. (8认同)
  • @TonyD 嗯,我提出的观点并不是应该检查“数据结构是否超出了有用的范围”。它是为了检查“Parent 数据结构是否做了一些与 Child 类可能暗示的行为无关、无意义或违反直觉的事情”。 (2认同)
  • @TonyD 实际上存在从父级派生的不相关特征的问题,因为在许多情况下它会无法通过负面测试。程序员可以扩展 Human{eat(); 跑(); write();} 来自大猩猩{eat(); 跑(); Swing();} 认为具有能够摆动的额外特征的人类没有任何问题。然后在游戏世界中,您的人类突然开始通过在树上摆动来绕过所有假定的陆地障碍。除非明确指定,否则实际的人类不应该能够摆动。这样的设计使 api 很容易被滥用和混淆 (2认同)
  • @TonyD 我也不建议 Player 类应该从 HashSet 派生。我建议 Player 类“在大多数情况下”应该通过 Composition 使用 HashSet 来实现,这完全是一个实现级别的细节,而不是一个设计级别的细节(这就是为什么我在我的答案的旁注中提到它)。如果这种实现有合理的理由,那么它可以很好地使用列表来实现。所以要回答你的问题,是否有必要通过键进行 O(1) 查找?不可以。因此,也不应该从 HashSet 中扩展 Player。 (2认同)

Sam*_*ach 48

如果FootballTeam拥有预备队和主队,该怎么办?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

你会如何模仿?

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}
Run Code Online (Sandbox Code Playgroud)

这种关系显然有一个而不是一个.

还是RetiredPlayers

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
    List<FootballPlayer> RetiredPlayers { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

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

你的SomethingCollection语义有意义吗?只有在你的类型是一个集合时才这样做Something.

FootballTeam它听起来不对的情况下.A Team不止一个Collection.Team其他答案指出,A 可以有教练,训练师等.

FootballCollection听起来像是足球的集合,也可能是足球用具的集合.TeamCollection,一个团队的集合.

FootballPlayerCollection听起来像是一个玩家的集合,List<FootballPlayer>如果你真的想这样做的话,它将是继承的类的有效名称.

真的List<FootballPlayer>是一个非常好的类型来处理.也许IList<FootballPlayer>如果你从一个方法返回它.

综上所述

问你自己

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

  2. 我的班级名字是什么意思吗?

  • 虽然我同意这一观点,但是真的让我的灵魂分崩离析,看到有人提出设计建议,并建议在业务对象中公开具有公共getter和setter的具体List <T> :( (2认同)

El *_*rko 36

设计>实施

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

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

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

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

请记住,继承意味着"isa"(是一种)关系,而组合意味着"有一种"(hasa)关系.在您的视图中根据您的情况选择合适的一个,同时考虑到应用程序发展过程中可能出现的情况.

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

这不是每个人在日常编码中有意识地在这个级别上做的事情.但如果你正在考虑这类话题,那么你就是在设计水域中跋涉.意识到它可以解放.

考虑设计细节

查看MSDN或Visual Studio上的List <T>和IList <T>.了解他们公开的方法和属性.这些方法看起来都像是某个人想要在你的视图中对FootballTeam做些什么吗?

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

这不是一个棘手的问题; 答案可能真的是"是".如果你实现/继承List <Player>或IList <Player>,你就会被它们困住; 如果那是你的模特的理想选择,那就去做吧.

如果您决定是,那么这是有道理的,并且您希望您的对象可以作为玩家的集合/列表(行为)来处理,因此您希望通过各种方式实现ICollection或IList.名义上:

class FootballTeam : ... ICollection<Player>
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

如果您希望对象包含玩家(数据)的集合/列表,并且您希望集合或列表成为属性或成员,请务必执行此操作.名义上:

class FootballTeam ...
{
    public ICollection<Player> Players { get { ... } }
}
Run Code Online (Sandbox Code Playgroud)

您可能会觉得您希望人们只能枚举一组玩家,而不是计算它们,添加它们或删除它们.IEnumerable <Player>是一个非常有效的选项.

您可能会觉得这些接口根本不适用于您的模型.这种可能性较小(IEnumerable <T>在许多情况下都很有用),但它仍然可行.

任何试图告诉你其中一个在每种情况下都是明确和明确错误的人都是错误的.任何试图告诉你它的人在每种情况下都是绝对正确的,这是错误的.

继续实施

一旦确定了数据和行为,就可以做出有关实施的决定.这包括您通过继承或组合依赖的具体类.

这可能不是一个很大的步骤,人们经常将设计和实施混为一谈,因为很可能在一两秒内完成所有操作并开始输入.

思想实验

一个人为的例子:正如其他人所提到的,团队并不总是"只是"一组玩家.你是否为球队保留了一系列比赛分数?在你的模特中,球队是否可以与俱乐部互换?如果是这样,如果你的团队是一个球员集合,也许它也是一组员工和/或一组分数.然后你最终得到:

class FootballTeam : ... ICollection<Player>, 
                         ICollection<StaffMember>,
                         ICollection<Score>
{
    ....
}
Run Code Online (Sandbox Code Playgroud)

设计尽管如此,在C#中你现在无法通过继承List <T>来实现所有这些,因为C#"only"支持单继承.(如果你在C++中尝试过这个malarky,你可能会认为这是一件好事.)通过继承和一个通过组合实现一个集合可能会感觉很脏.除非你明确地实现ILIst <Player> .Count和IList <StaffMember> .Count等,否则Count等属性会让用户感到困惑,然后它们只是痛苦而不是混乱.你可以看到它的发展方向; 在思考这条大道的同时,直觉可能会告诉你朝这个方向前进是错误的(正确或错误的是,如果你以这种方式实施它,你的同事也可能!)

简答(太迟)

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


Dmi*_* S. 32

首先,它与可用性有关.如果使用继承,则Team该类将公开纯粹为对象操作而设计的行为(方法).例如,AsReadOnly()或者CopyTo(obj)方法对团队对象没有意义.AddRange(items)您可能需要更具描述性的AddPlayers(players)方法,而不是方法.

如果你想使用LINQ,实现一个通用的接口,如ICollection<T>IEnumerable<T>更有意义.

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


Mau*_*tro 28

足球队不是足球运动员的名单.足球队一系列足球运动员组成!

这在逻辑上是错误的:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}
Run Code Online (Sandbox Code Playgroud)

这是正确的:

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}
Run Code Online (Sandbox Code Playgroud)

  • 这并不能解释*为什么*.OP知道大多数其他程序员都有这种感觉,他只是不明白为什么这很重要.这只是提供问题中的信息. (19认同)
  • 为什么不从列表继承?因为他不想要更具体的清单. (3认同)
  • 这也是我想到的第一件事,即他的数据是如何被错误地建模的。因此,原因是您的数据模型不正确。 (2认同)
  • 我不认为第二个例子是正确的。您正在暴露状态,从而破坏了封装。状态应在对象内部隐藏/内部,并且通过公共方法更改此状态。确切地说,哪种方法可以根据业务需求确定,但是应该以“目前需要”为基础,而不是“在某些时候不方便”为基础。保持api紧密,您将节省时间和精力。 (2认同)
  • 封装不是问题的重点。如果您不希望该类型以更具体的方式运行,我将重点放在不扩展该类型。这些字段在 c# 中应该是自动属性,在其他语言中应该是 get/set 方法,但在这种情况下,这是完全多余的 (2认同)

use*_*503 27

让我改写你的问题.所以你可能会从不同的角度看待这个主题.

当我需要代表足球队时,我明白它基本上就是一个名字.喜欢:"老鹰队"

string team = new string();
Run Code Online (Sandbox Code Playgroud)

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

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

你进入问题的观点是任意的.试着想一下团队什么(属性),而不是它什么.

执行此操作后,您可以看到它是否与其他类共享属性.并考虑继承.

  • 这是看待它的一个有价值的方式.作为旁注,请考虑一下:一个有财富的人拥有几个足球队,他给一个朋友作为礼物,朋友改变球队的名字,解雇教练,并取代所有球员.朋友队在果岭上遇到男队,而男队正在输球,他说:"我不相信你是在和我给你的球队一起击败我!" 男人是对的吗?你怎么检查这个? (6认同)
  • 这是一个很好的观点 - 人们可以将团队视为一个名字。但是,如果我的应用程序旨在处理玩家的动作,那么这种想法就不那么明显了。无论如何,问题似乎最终归结为组合与继承。 (2认同)

ste*_*hke 26

这取决于具体情况

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

不明确的语义

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

类是可扩展的

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

课程更复杂

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

结论/考虑因素

当你有一个非常具体的背景时,可以将一个团队视为一个球员列表.例如,在一个方法中,写入是完全可以的

IList<Player> footballTeam = ...
Run Code Online (Sandbox Code Playgroud)

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

type FootballTeam = IList<Player>
Run Code Online (Sandbox Code Playgroud)

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


Mar*_*ett 20

仅仅因为我认为其他答案几乎与橄榄球队是"a-a" List<FootballPlayer>还是"has-a"相关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>直接暴露您的API )或违反指南以及可能的未来痛苦.

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

取决于上下文,但是因为我们使用的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!");
        }
     }
 }
Run Code Online (Sandbox Code Playgroud)

啊......但你不能,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 */
 }
Run Code Online (Sandbox Code Playgroud)

但是,如果你公开地暴露给第三方,你只是做了一个重大改变,将导致编译和/或运行时错误.

TL; DR - 指南适用于公共API.对于私有API,请执行您想要的操作.


Cru*_*her 17

允许人们说

myTeam.subList(3, 5);
Run Code Online (Sandbox Code Playgroud)

什么都没有意义?如果没有那么它不应该是List.

  • 如果你把它命名为"myTeam.subTeam(3,5);" (3认同)
  • @SamLeach假设这是真的,那么你仍然需要组合而不是继承.由于`subList`甚至不再返回`Team`. (3认同)

xpm*_*teo 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).内部列表将不可见.

  • 我不同意玩家名单在概念上是错误的.不必要.正如其他人在本页中所写,"所有模型都错了,但有些模型很有用" (3认同)

Dis*_*ned 15

这里有很多优秀的答案,但我想触及一些我没有看到过的东西:面向对象的设计就是赋予对象权力.

您希望将所有规则,其他工作和内部详细信息封装在适当的对象中.通过这种方式,与此对象交互的其他对象不必担心这一切.实际上,您希望更进一步,并积极防止其他对象绕过这些内部.

从继承时List,所有其他对象都可以将您视为List.他们可以直接访问添加和删除玩家的方法.你会失去控制力; 例如:

假设你想通过知道退役,辞职或被解雇来区分玩家离开的时间.您可以实现一个RemovePlayer采用适当的输入枚举的方法.但是,通过继承List,您将无法阻止直接访问Remove,RemoveAll甚至Clear.结果,你实际上剥夺了你的FootballTeam课程权力.


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

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

你是对的,所有客户使用你的团队都是不必要的冗长.然而,与你暴露List Players于所有和各种各样的事实相比,这个问题是非常小的,所以他们可以在未经你同意的情况下与你的团队混在一起.

你继续说:

它只是简单没有任何意义.足球队没有"拥有"球员名单.这是球员名单.你没有说"John McFootballer加入了SomeTeam的球员".你说"约翰加入了SomeTeam".

你错了第一点:删掉'list'这个词,显然团队确实有玩家.
然而,你用第二个击中头部的钉子.你不希望客户打电话ateam.Players.Add(...).你确实希望他们打电话ateam.AddPlayer(...).而你的实施将(可能在其他方面)Players.Add(...)内部调用.


希望你能看到封装对赋予对象权力的重要性.您希望允许每个班级完成其工作,而不必担心来自其他对象的干扰.


Art*_*ena 12

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

记住,"所有模型都错了,但有些模型很有用." - George EP Box

没有"正确的方法",只有一个有用的方法.

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

- 编辑

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

- 第2版

我真诚地不记得我所说的"不要过度工程"评论.虽然我认为KISS的心态是一个很好的指导,但我想强调的是,从List继承业务类会产生比解决更多的问题,因为抽象泄漏.

另一方面,我认为有限数量的案例只是简单地从List继承是有用的.正如我在上一版中所写,这取决于.每个案例的答案都受到知识,经验和个人偏好的严重影响.

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

  • "不要过度工程.你编写的代码越少,调试所需的代码就越少." < - 我认为这是误导.继承的封装和组合不是过度工程,它不会导致更多的调试需求.通过封装,您可以限制客户可以使用(和滥用)您的类的方式,从而减少需要测试和输入验证的入口点.继承"List"因为它快速而简单,因此会导致更少的错误是完全错误的,它只是糟糕的设计和糟糕的设计导致比"过度工程"更多的错误. (3认同)
  • 关于"_inheriting from`List`会使这个类的客户端暴露给可能不应该暴露的方法_"正是使得从`List`继承绝对禁止的原因.一旦暴露,它们会逐渐被滥用和误用.现在通过"经济发展"节省时间很容易导致未来节省的费用增加十倍:调试滥用行为并最终重构以修复继承.YAGNI原则也可以被认为是意思:你不需要`List`中的所有那些方法,所以不要暴露它们. (2认同)

Pau*_*thy 11

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

你做了什么?与许多事情一样......这取决于具体情况.我将使用的指南是,为了从另一个类继承,真正应该是"是一种"关系.因此,如果你写一个名为宝马的课程,它可以继承汽车,因为宝马真的是一辆汽车.马类可以继承哺乳动物类,因为马实际上是现实生活中的哺乳动物,任何哺乳动物的功能都应该与马相关.但是你能说一个团队是一个名单吗?从我所知道的情况来看,这似乎并不像一个团队真的"是一个"列表.所以在这种情况下,我会将List作为成员变量.


Nic*_*ier 9

我的肮脏秘密:我不在乎别人说什么,我也这么做..NET Framework使用"XxxxCollection"进行传播(UIElementCollection用于我的头顶示例).

那让我停下来的是什么:

team.Players.ByName("Nicolas")
Run Code Online (Sandbox Code Playgroud)

当我发现它比它好

team.ByName("Nicolas")
Run Code Online (Sandbox Code Playgroud)

此外,我的PlayerCollection可能被其他类使用,例如"Club",没有任何代码重复.

club.Players.ByName("Nicolas")
Run Code Online (Sandbox Code Playgroud)

昨天的最佳做法,可能不是明天的做法.大多数最佳实践背后都没有理由,大多数只是社区之间的广泛共识.而不是问社区是否会在你这样做时责怪你,问自己,什么更具可读性和可维护性?

team.Players.ByName("Nicolas") 
Run Code Online (Sandbox Code Playgroud)

要么

team.ByName("Nicolas")
Run Code Online (Sandbox Code Playgroud)

真.你有任何疑问吗?现在,您可能需要使用其他技术限制来阻止您在实际用例中使用List <T>.但是不要添加不应存在的约束.如果微软没有记录原因,那么它无疑是一种"最佳实践".

  • 虽然在适当的时候有勇气和主动性挑战公认的智慧是很重要的,但我认为,在开始挑战它之前,先了解为什么接受的智慧首先被接受是明智的.-1因为"忽略那条准则!" 不是一个很好的答案"为什么这个指南存在?" 顺便说一句,在这种情况下,社区并不只是"责怪我",而是提供了一个令人满意的解释,成功地说服了我. (8认同)
  • 实际上,如果不作任何解释,您唯一的办法就是突破界限,进行测试,不要害怕违规。现在。问问自己,即使List &lt;T&gt;不是一个好主意。将继承从List &lt;T&gt;更改为Collection &lt;T&gt;会多么痛苦?猜测:与重构工具无关,无论代码多长,都比在论坛上发布花费更少的时间。现在,这是一个好问题,但不是一个实际的问题。 (3认同)
  • `team.ByName("Nicolas")` 的意思是 `"Nicolas"` 是团队的名字。 (2认同)
  • “不要添加不应该存在的约束”相反,不要公开任何你没有充分理由公开的内容。这不是纯粹主义或盲目狂热,也不是最新的设计时尚趋势。它是基础面向对象设计101,初级水平。这个“规则”存在的原因并不是什么秘密,它是几十年经验的结果。 (2认同)

Que*_*nUK 8

指南所说的是公共API不应该公开内部设计决定,无论是使用列表,集合,字典,树还是其他什么."团队"不一定是列表.您可以将其实现为列表,但是您的公共API的用户应该根据需要知道您的类.这允许您更改决策并使用不同的数据结构,而不会影响公共接口.


Shi*_*hah 6

当他们说List<T>"优化"时,我认为他们想要意味着它没有像虚拟方法那样有点贵的功能.所以问题在于,一旦List<T>公共API公开,就会失去执行业务规则或稍后自定义其功能的能力.但是如果你在项目中使用这个继承的类作为内部(相对于可能暴露给成千上万的客户/合作伙伴/其他团队作为API),那么如果它节省你的时间并且它是你想要的功能可能没问题.重复.继承的优点List<T>是你消除了许多在可预见的未来永远不会被定制的愚蠢的包装代码.此外,如果您希望您的类明确具有与List<T>API的生命周期完全相同的语义,那么也可以.

我经常看到很多人做了大量的额外工作,仅仅是因为FxCop规则这么说,或者有人的博客说这是一个"坏"的做法.很多时候,这会将代码转换为设计模式palooza怪异.与许多指南一样,将其视为可能有例外的指导原则.


mar*_*gle 6

序列化问题

缺少一个方面。不能使用 XmlSerializer 正确序列化从 List 继承的类。在这种情况下,必须改用 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; }  
}
Run Code Online (Sandbox Code Playgroud)

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

改用 IList

我个人不会从 List 继承,而是实现 IList。Visual Studio 将为您完成这项工作并创建一个完整的工作实施。看这里:如何获得 IList 的完整工作实现


小智 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
{
    ...
}
Run Code Online (Sandbox Code Playgroud)