在计划我的计划时,我通常会从这样的一系列想法开始:
足球队只是足球运动员的名单。因此,我应以以下方式表示: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>继承时,必须考虑什么?
这里有很多很好的答案,但我想谈谈我没有提到的东西:面向对象的设计是关于增强对象的能力。
您希望将所有规则、附加工作和内部细节封装在适当的对象中。以这种方式,与此交互的其他对象不必担心这一切。事实上,您希望更进一步,积极防止其他对象绕过这些内部。
从列表继承时,所有其他对象都可以将您视为列表。他们可以直接访问添加和删除玩家的方法。你会失去控制;例如:
假设你想通过了解球员是退役、辞职还是被解雇来区分球员何时离开。您可以实现一个RemovePlayer方法,该方法采用适当的输入枚举。然而,通过从List继承,您将无法阻止对Remove、RemoveAll甚至Clear的直接访问。结果,你实际上取消了足球队课程的资格。
关于封装的其他想法。。。您提出了以下问题:
这使我的代码变得不必要地冗长。我现在必须调用my_team.Players.Count而不是my_team.Count。
你是对的,这对于所有使用你团队的客户来说都是不必要的冗长。然而,这个问题与你将名单球员暴露给所有人的事实相比是很小的,这样他们就可以在没有你同意的情况下玩弄你的球队。
你接着说:
这显然没有任何意义。足球队没有球员名单。这是球员名单。你不会说“约翰·麦克足球先生加入了SomeTeam的球员”。你说“John加入了SomeTeam”。
第一点你错了:去掉“列表”这个词,很明显一支球队确实有球员。然而,你用第二个击中了要害。你不希望客户端调用ateam.Players.Add(…)。你确实希望他们调用ateam.AddPlayer(…),而你的实现(可能还有其他事情)会在内部调用Players.Add(…)。
希望您能够看到封装对于增强对象的能力是多么重要。您希望让每个类都能很好地完成其工作,而不必担心其他对象的干扰。
设计>实施
您公开的方法和财产是一个设计决策。您从中继承的基类是一个实现细节。我觉得回到从前是值得的。
对象是数据和行为的集合。
因此,您的第一个问题应该是:
这个对象在我创建的模型中包含哪些数据?该对象在该模型中表现出什么行为?这在未来会发生什么变化?
记住,继承意味着“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”关系更常见,除非你深入抽象,尤其是随着时间的推移和代码中对象的精确数据和行为的改变。这不应该导致您总是排除从集合类继承;但它可能是暗示性的。