在计划我的计划时,我通常会从这样的一系列想法开始:

足球队只是足球运动员的名单。因此,我应以以下方式表示:var football_team=新列表<FootballPlayer>();此列表的顺序表示球员在名册中的排列顺序。

但我后来意识到,除了球员名单之外,球队还有其他必须记录的财产。例如,本赛季总得分、当前预算、制服颜色、代表球队名称的字符串等。。

所以我想:

好吧,足球队就像一个球员列表,但除此之外,它还有一个名字(一个字符串)和一个连续的总得分(一个整数)。NET没有提供存储足球队的类,所以我将创建自己的类。最相似和最相关的现有结构是List<FootballPlayer>,因此我将从中继承:class FootballTeam:列表<FootballPlayer>{ 公共字符串TeamName;公共int RunningTotal}

但事实证明,一条准则说你不应该从List<t>继承。我在两个方面完全被这条准则搞糊涂了。

为什么不呢?

显然,List在某种程度上优化了性能。为什么呢如果我扩展列表,会导致什么性能问题?到底会发生什么?

我看到的另一个原因是List是由Microsoft提供的,我无法控制它,所以在暴露了一个“公共API”之后,我无法稍后更改它。但我很难理解这一点。什么是公共API?我为什么要关心?如果我当前的项目没有也不可能有这个公共API,我可以放心地忽略这个准则吗?如果我确实继承了List,结果发现我需要一个公共API,我会遇到什么困难?

为什么这很重要?列表是一个列表。什么可能会改变?我可能想要改变什么?

最后,如果微软不想让我继承List,他们为什么不把这个类封起来呢?

我还能用什么?

显然,对于自定义集合,Microsoft提供了一个Collection类,该类应该扩展而不是List。但是这个类非常简单,没有很多有用的东西,例如AddRange。jvitor83的答案为该特定方法提供了性能基础,但如何使缓慢的AddRange不比没有AddRange更好?

从收藏中继承比从列表中继承要多得多,我看不出有什么好处。当然,微软不会无缘无故地让我做额外的工作,所以我忍不住觉得我在某种程度上误解了什么,而继承Collection实际上并不是解决我问题的正确方法。

我看到了一些建议,比如实施IList。只是没有。这是几十行样板代码,我什么也没得到。

最后,一些人建议将列表包装为:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

这有两个问题:

这使我的代码变得不必要地冗长。我现在必须调用my_team.Players.Count而不是my_team.Count。幸运的是,使用C#,我可以定义索引器以使索引透明,并转发内部List的所有方法。。。但这是很多代码!我做这些工作能得到什么?这显然没有任何意义。足球队没有球员名单。这是球员名单。你不会说“约翰·麦克足球先生加入了SomeTeam的球员”。你说“John加入了SomeTeam”。您不向“字符串的字符”添加字母,而是向字符串添加字母。您不向图书馆的图书添加图书,而是向图书馆添加图书。

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

我的问题(总结)

正确的C#表示数据结构的方式是什么?数据结构“逻辑上”(也就是说,“对人类的大脑”)只是一个带有一些铃铛和口哨的列表?

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


当前回答

这里有一些很好的答案。我想补充以下几点。

正确的C#表示数据结构的方式是什么?数据结构“逻辑上”(也就是说,“对人类的大脑”)只是一个带有一些铃铛和口哨的列表?

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

足球队是一种特殊的_____

有没有人说“足球运动员名单”,或者他们都说“运动队”、“俱乐部”或“组织”?你认为一支足球队是一个特殊类型的球员名单,这是你的人的想法,也是你个人的想法。

列表<T>是一种机制。足球队是一个业务对象——也就是说,一个表示程序业务领域中某个概念的对象。不要把它们混在一起!足球队是一种球队;它有一个名册,名册是一个球员名单。名册不是一种特定的球员名单。名册是球员名单。因此,创建一个名为“花名册”的属性,它是一个列表<Player>。如果你不相信任何一个了解足球队的人都可以从名单中删除球员,那就让它成为ReadOnlyList<Player>。

从列表<T>继承总是不可接受的吗?

谁不能接受?我不

什么时候可以接受?

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

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

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

但这是很多代码!我做这些工作能得到什么?

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

更新

我想了想,还有一个理由不把足球队列为球员名单。事实上,将一支足球队建模为也有球员名单可能是一个坏主意。一支球队有一个球员名单的问题是,你得到的是球队在某一时刻的快照。我不知道你在这门课上的商业案例是什么,但如果我有一门代表足球队的课,我想问它一些问题,比如“2003年至2013年期间,有多少海鹰队球员因伤缺席比赛?”或“之前为另一支球队效力的丹佛队球员的跑动码数同比增幅最大?”或是“小猪队今年一路走好吗?”

也就是说,在我看来,足球队似乎是一个历史事实的集合,例如球员何时被招募、受伤、退役等。显然,目前的球员名册是一个重要的事实,可能应该是前锋和中锋,但可能还有其他有趣的事情需要更多的历史视角。

其他回答

设计>实施

您公开的方法和财产是一个设计决策。您从中继承的基类是一个实现细节。我觉得回到从前是值得的。

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

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

这个对象在我创建的模型中包含哪些数据?该对象在该模型中表现出什么行为?这在未来会发生什么变化?

记住,继承意味着“isa”(是a)关系,而组合意味着“hasa”(hasa)关系。在您的视图中选择适合您的情况的应用程序,记住随着应用程序的发展,事情可能会走向何方。

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

这并不是每个人在日常编码中都有意识地做到的。但是,如果你在考虑这类话题,你就在踩设计的水。意识到这一点可以让人解放。

考虑设计规范

看看MSDN或Visual Studio上的List<T>和IList<T>。查看它们公开了哪些方法和财产。在你看来,这些方法都像是有人想对足球队做的吗?

足球队逆转()对你有意义吗?footballTeam.ConvertAll<TOutput>()看起来像你想要的吗?

这不是一个技巧问题;答案可能真的是“是”。如果您实现/继承List<Player>或IList<Player>,您将无法摆脱它们;如果这对你的模型很理想,那就去做吧。

如果您决定是,这是有意义的,并且您希望您的对象可以作为玩家的集合/列表(行为)处理,因此您希望实现ICollection<Player>或IList<Player>

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#“仅”支持单个继承。(如果你在C++中尝试过这种浮夸,你可能会认为这是一件好事。)通过继承实现一个集合,通过组合实现一个,可能会感觉很脏。而Count等财产会让用户感到困惑,除非显式实现ILIst<Player>.Count和ILIst<StaffMember>.Cunt等,否则它们只会让人感到痛苦,而不会让人感到困惑。你可以看到这是怎么回事;沿着这条路思考时的直觉很可能会告诉你,朝着这个方向前进是错误的(无论对错,如果你这样做,你的同事也可能会这样做!)

简短的回答(太迟了)

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

序列化问题

One aspect is missing. Classes that inherit from List can't be serialized correctly using XmlSerializer. In that case DataContractSerializer must be used instead, or an own serializing implementation is needed.
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; }  
}

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

改用IList

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

我只是想补充一点,艾菲尔铁塔和合同设计的发明者伯特兰·梅耶(Bertrand Meyer)会让团队继承List<Player>,而不必太费唇舌。

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

然而,C#不是埃菲尔铁塔。后者支持功能的多重继承和重命名。在C#中,当您子类化时,您继承了接口和实现。您可以重写实现,但调用约定直接从超类复制。然而,在Eiffel中,您可以修改公共方法的名称,因此您可以在团队中将Add和Remove重命名为Hire和Fire。如果Team的一个实例被上传回List<Player>,调用方将使用AddandRemove来修改它,但将调用Hire和Fire虚拟方法。

如果您的类用户需要List拥有的所有方法和财产,您应该从中派生类。如果他们不需要,请将List括起来,并为类用户实际需要的方法制作包装器。

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

对于小型应用程序,您也可以考虑选择另一种不那么严格的语言。Ruby、JavaScript——任何允许您编写更少代码的东西。

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

您希望将所有规则、附加工作和内部细节封装在适当的对象中。以这种方式,与此交互的其他对象不必担心这一切。事实上,您希望更进一步,积极防止其他对象绕过这些内部。

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

假设你想通过了解球员是退役、辞职还是被解雇来区分球员何时离开。您可以实现一个RemovePlayer方法,该方法采用适当的输入枚举。然而,通过从List继承,您将无法阻止对Remove、RemoveAll甚至Clear的直接访问。结果,你实际上取消了足球队课程的资格。


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

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

你是对的,这对于所有使用你团队的客户来说都是不必要的冗长。然而,这个问题与你将名单球员暴露给所有人的事实相比是很小的,这样他们就可以在没有你同意的情况下玩弄你的球队。

你接着说:

这显然没有任何意义。足球队没有球员名单。这是球员名单。你不会说“约翰·麦克足球先生加入了SomeTeam的球员”。你说“John加入了SomeTeam”。

第一点你错了:去掉“列表”这个词,很明显一支球队确实有球员。然而,你用第二个击中了要害。你不希望客户端调用ateam.Players.Add(…)。你确实希望他们调用ateam.AddPlayer(…),而你的实现(可能还有其他事情)会在内部调用Players.Add(…)。


希望您能够看到封装对于增强对象的能力是多么重要。您希望让每个类都能很好地完成其工作,而不必担心其他对象的干扰。