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

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


当前回答

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

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,您可以随心所欲。

其他回答

如果足球队有一支预备队和主力队呢?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
}

你将如何建模?

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

这种关系显然是有一个,而不是一个。

还是退役球员?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
    List<FootballPlayer> RetiredPlayers { get; set; }
}

根据经验,如果您想从集合继承,请将类命名为SomethingCollection。

你的SomethingCollection在语义上有意义吗?只有当你的类型是Something的集合时才这样做。

就足球队而言,这听起来不太对。团队不仅仅是集合。正如其他答案所指出的,一个团队可以有教练、教练等。

FootballCollection听起来像是足球的集合,也可能是足球用品的集合。TeamCollection,团队的集合。

FootballPlayerCollection听起来像是一个球员集合,如果你真的想这样做的话,它将是继承自List<FootballPlayer>的类的有效名称。

真正的列表<FootballPlayer>是一个非常好的类型。如果您从方法返回IList<FootballPlayer>,则可能是这样。

总而言之

问问你自己

X是Y吗?或者有X和Y?我的类名是什么意思?

足球队不是足球运动员的名单。足球队是由足球运动员名单组成的!

这在逻辑上是错误的:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

这是正确的:

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}

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

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

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

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

--已编辑

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

--第2版

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

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

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

哇,你的帖子有很多问题和观点。你从微软那里得到的大部分理由都是正确的。让我们从List<T>开始

列表<T>高度优化。它的主要用途是用作对象的私有成员。Microsoft没有密封它,因为有时您可能希望创建一个具有更友好名称的类:class MyList<T,TX>:List<CustomObject<T,Something<TX>{…}。现在,这就像做var-list=newMyList<int,string>();一样简单;。CA1002:不要暴露通用列表:基本上,即使你计划将此应用程序作为唯一的开发人员,也值得用良好的编码实践进行开发,因此它们会逐渐灌输给你和第二天性。如果您需要任何消费者拥有索引列表,您仍然可以将列表公开为IList<T>。这允许您稍后在类中更改实现。Microsoft使集合<T>非常通用,因为它是一个通用概念。。。名字说明了一切;这只是一个集合。还有更精确的版本,如SortedCollection<T>、ObservableCollection<T’、ReadOnlyCollection<T〕等,它们都实现了IList<T>,但不实现List<T>。集合<T>允许重写成员(即Add、Remove等),因为它们是虚拟的。列表<T>没有。你问题的最后一部分是正确的。足球队不仅仅是一个球员名单,所以它应该是一个包含球员名单的班级。思考组成与继承。足球队有一个球员名单(名册),而不是球员名单。


如果我正在编写这段代码,那么类可能看起来像这样:

public class FootballTeam<T>//generic class
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    // Yes. I used LINQ here. This is so I don't have to worry about
    // _roster.Length vs _roster.Count vs anything else.
    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

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

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