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

足球队只是足球运动员的名单。因此,我应以以下方式表示: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#方法。。。

记住,“所有的模型都是错误的,但有些模型是有用的。”

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

选择一个对您和/或您的用户有用的。就是这样。经济发展,不要过度设计。编写的代码越少,需要调试的代码就越少。(阅读以下版本)。

--已编辑

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

--第2版

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

另一方面,我认为在少数情况下,仅从List继承是有用的。正如我在前一版中所写的,这取决于情况。每个案例的答案都受到知识、经验和个人偏好的严重影响。

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

其他回答

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

如果您想使用LINQ,实现诸如ICollection<T>或IEnumerable<T>之类的通用接口将更有意义。

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

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

记住,“所有的模型都是错误的,但有些模型是有用的。”

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

选择一个对您和/或您的用户有用的。就是这样。经济发展,不要过度设计。编写的代码越少,需要调试的代码就越少。(阅读以下版本)。

--已编辑

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

--第2版

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

另一方面,我认为在少数情况下,仅从List继承是有用的。正如我在前一版中所写的,这取决于情况。每个案例的答案都受到知识、经验和个人偏好的严重影响。

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

这是一个组合与继承的经典例子。

在这种特定情况下:

球队是一个有附加行为的球员列表吗

or

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

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

您不能限制访问(例如,阻止人员更改名册)。您可以获得所有List方法,无论您是否需要它们。如果你还想列出其他事情,会发生什么。例如,球队有教练、经理、球迷、设备等。其中一些可能是他们自己的名单。你限制了继承的选择。例如,您可能希望创建一个通用的Team对象,然后让BaseballTeam、FootballTeam等继承该对象。要从列表继承,您需要从团队继承,但这意味着所有不同类型的团队都必须对该名册进行相同的实现。

合成-包括一个对象,它给出了你想要的对象内部的行为。

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

两者都有各自的用途,但这是一个明显的情况,即合成更可取。

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

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

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

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


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

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

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

你接着说:

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

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


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

首选接口而非类

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

继承中断封装

从类派生会破坏封装:

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

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

类是实现细节

类是一个实现细节,应该对代码的其他部分隐藏。简而言之,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接口中采用的方法。