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

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


当前回答

我肮脏的秘密:我不在乎别人说什么,我也这么做。.NET Framework以“XxxxCollection”(我头上的例子是UIElementCollection)传播。

那么,是什么阻止了我说:

team.Players.ByName("Nicolas")

当我发现它比

team.ByName("Nicolas")

此外,我的PlayerCollection可能会被其他类使用,如“俱乐部”,而不需要任何代码重复。

club.Players.ByName("Nicolas")

昨天的最佳实践可能不是明天的最佳实践。大多数最佳做法背后没有任何原因,大多数只是社区之间的广泛共识。与其问社区,当你这样做时,它是否会责怪你,不如问问自己,什么更容易阅读和维护?

team.Players.ByName("Nicolas") 

or

team.ByName("Nicolas")

真正地你有什么疑问吗?现在,您可能需要处理其他技术限制,这些限制会阻止您在实际用例中使用List<T>。但不要添加不应该存在的约束。如果微软没有记录原因,那么这无疑是一个“最佳实践”。

其他回答

仅仅因为我认为其他答案与足球队是“是”列表<足球运动员>还是“有”列表<橄榄球运动员>的切线基本一致,这并不能回答这个问题。

OP主要要求澄清从列表<T>继承的指南:

一条准则说,你不应该继承List<t>。为什么不呢?

因为List<T>没有虚拟方法。在您自己的代码中,这不是一个问题,因为您通常可以相对轻松地切换实现,但在公共API中,这可能是一个更大的问题。

什么是公共API?我为什么要关心?

公共API是您向第三方程序员公开的接口。思考框架代码。请记住,引用的指南是“.NET Framework设计指南”,而不是“.NET应用程序设计指南”。这是有区别的,而且一般来说,公共API设计要严格得多。

如果我当前的项目没有也不可能有这个公共API,我可以放心地忽略这个准则吗?如果我确实继承了List,结果发现我需要一个公共API,我会遇到什么困难?

差不多,是的。您可能需要考虑其背后的基本原理,看看它是否适用于您的情况,但如果您没有构建公共API,那么您不需要特别担心API问题,例如版本控制(这是其中的一个子集)。

如果您将来添加了公共API,您要么需要从实现中抽象出API(不直接暴露List<T>),要么违反指南,可能会带来未来的痛苦。

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

这取决于上下文,但由于我们以足球队为例,假设你不能添加足球运动员,如果这会导致球队超过工资上限。一种可能的添加方式是:

 class FootballTeam : List<FootballPlayer> {
     override void Add(FootballPlayer player) {
        if (this.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
 }

啊。。。但不能重写Add,因为它不是虚拟的(出于性能原因)。

如果您在一个应用程序中(基本上,这意味着您和所有调用程序都是一起编译的),那么现在可以改为使用IList<T>并修复任何编译错误:

 class FootballTeam : IList<FootballPlayer> {
     private List<FootballPlayer> Players { get; set; }

     override void Add(FootballPlayer player) {
        if (this.Players.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
     /* boiler plate for rest of IList */
 }

但是,如果你已经公开暴露给第三方,你只是做了一个破坏性的更改,这将导致编译和/或运行时错误。

TL;DR-指南适用于公共API。对于私有API,您可以随心所欲。

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

在这种特定情况下:

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

or

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

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

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

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

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

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

我想我不同意你的概括。一支球队不仅仅是球员的集合。一支球队有很多关于它的信息——名字、队徽、管理/行政人员的集合、教练组的集合,然后是球员的集合。因此,您的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();
}

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

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

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

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


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

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

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

你接着说:

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

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


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

这取决于上下文

当你把你的球队作为一个球员列表时,你把足球队的“想法”归结为一个方面:你把“球队”缩小到你在球场上看到的人。这一预测仅在特定情况下是正确的。在不同的背景下,这可能是完全错误的。想象一下你想成为球队的赞助商。所以你必须和团队的经理谈谈。在这种情况下,团队将被列入其经理名单。这两个列表通常不会重叠太多。其他情况包括当前玩家与前玩家等。

语义不清晰

因此,将一个团队视为其球员列表的问题在于,它的语义取决于上下文,并且当上下文发生变化时无法扩展。此外,很难表达您所使用的上下文。

类是可扩展的

当您使用只有一个成员的类(例如IListactivePlayers)时,您可以使用成员的名称(以及其注释)来明确上下文。当有其他上下文时,只需添加一个额外的成员。

类更复杂

在某些情况下,创建一个额外的类可能是过度的。每个类定义都必须通过类加载器加载,并由虚拟机缓存。这会消耗运行时性能和内存。当你有一个非常具体的背景时,可以将足球队作为球员列表。但在这种情况下,您应该只使用IList,而不是从它派生的类。

结论/注意事项

当你有一个非常具体的背景时,可以将一个团队作为一个球员列表。例如,在方法内部,完全可以写:

IList<Player> footballTeam = ...

使用F#时,甚至可以创建类型缩写:

type FootballTeam = IList<Player>

但当背景更广泛甚至不清楚时,你不应该这样做。当您创建一个新的类,而该类将来可能使用的上下文不清楚时,情况尤其如此。警告标志是当你开始向你的班级添加额外的属性(球队名称、教练等)时。这是一个明确的标志,表明班级的使用环境不是固定的,将来会改变。在这种情况下,您不能将球队视为球员列表,但您应该将球员列表(当前活跃、未受伤等)建模为球队的属性。