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

足球队只是足球运动员的名单。因此,我应以以下方式表示: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>继承时,必须考虑什么?


当前回答

首选接口而非类

类应该避免从类派生,而是实现所需的最小接口。

继承中断封装

从类派生会破坏封装:

公开有关如何实现集合的内部详细信息声明可能不合适的接口(公共函数和财产集)

除此之外,这使得重构代码变得更加困难。

类是实现细节

类是一个实现细节,应该对代码的其他部分隐藏。简而言之,System.List是一种抽象数据类型的具体实现,现在和将来可能合适,也可能不合适。

从概念上讲,System.List数据类型被称为“List”这一事实有点转移视线。System.List<T>是一个可变的有序集合,它支持用于添加、插入和删除元素的摊余O(1)操作,以及用于检索元素数量或按索引获取和设置元素的O(2)操作。

接口越小,代码越灵活

在设计数据结构时,界面越简单,代码就越灵活。只需看看LINQ有多强大就可以演示这一点。

如何选择接口

当你想“列出”时,你应该先对自己说:“我需要代表一批棒球运动员”。假设你决定用一个类来建模。您首先应该做的是决定这个类需要公开的接口的最小数量。

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

我需要计数吗?如果没有,则考虑实施IEnumerable<T>此集合在初始化后是否会更改?如果没有,请考虑IReadonlyList<T>。我可以通过索引访问项目是否重要?考虑ICollection<T>我向集合中添加项目的顺序是否重要?也许是ISet<T>?如果您确实想要这些东西,那么继续执行IList<T>。

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

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

避免煮沸板的注意事项

在现代IDE中实现接口应该很容易。右键单击并选择“Implement Interface”。然后,如果需要,将所有实现转发给成员类。

也就是说,如果你发现你正在编写大量的样板,这可能是因为你暴露的函数比你应该暴露的更多。这也是你不应该从类继承的原因。

您还可以设计对您的应用程序有意义的较小接口,也许只需要几个助手扩展函数就可以将这些接口映射到您需要的任何其他接口。这是我在LinqArray库的IArray接口中采用的方法。

其他回答

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

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

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

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

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

足球队是一种特殊的_____

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

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

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

谁不能接受?我不

什么时候可以接受?

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

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

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

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

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

更新

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

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

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

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

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

换句话说,父级是子级的必要定义,子级是父级的充分定义。

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

足球队是足球运动员的名单吗?(列表的所有财产是否以相同的含义应用于团队)团队是同质实体的集合吗?是的,球队是球员的集合球员的出场顺序是否描述了球队的状态?球队是否确保除非明确更改,否则顺序将被保留?没有,也没有球员是否会根据他们在球队中的顺序被纳入/淘汰?不

如您所见,只有列表的第一个特征适用于团队。因此,团队不是列表。列表将是您如何管理团队的实现细节,因此它只能用于存储玩家对象,并使用team类的方法进行操作。

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

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

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

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

我想我不同意你的概括。一支球队不仅仅是球员的集合。一支球队有很多关于它的信息——名字、队徽、管理/行政人员的集合、教练组的集合,然后是球员的集合。因此,您的FootballTeam类应该有3个集合,而不是一个集合;如果要正确模拟真实世界。

您可以考虑一个PlayerCollection类,它像SpecializedStringCollection一样提供一些其他功能,例如在将对象添加到内部存储或从内部存储中删除之前进行验证和检查。

也许,PlayerCollection的概念更适合您的首选方法?

public class PlayerCollection : Collection<Player> 
{ 
}

然后足球队可以是这样的:

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();
}