在计划我的计划时,我通常会从这样的一系列想法开始:
足球队只是足球运动员的名单。因此,我应以以下方式表示: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接口中采用的方法。
设计>实施
您公开的方法和财产是一个设计决策。您从中继承的基类是一个实现细节。我觉得回到从前是值得的。
对象是数据和行为的集合。
因此,您的第一个问题应该是:
这个对象在我创建的模型中包含哪些数据?该对象在该模型中表现出什么行为?这在未来会发生什么变化?
记住,继承意味着“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”关系更常见,除非你深入抽象,尤其是随着时间的推移和代码中对象的精确数据和行为的改变。这不应该导致您总是排除从集合类继承;但它可能是暗示性的。