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

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


当前回答

序列化问题

One aspect is missing. Classes that inherit from List can't be serialized correctly using XmlSerializer. In that case DataContractSerializer must be used instead, or an own serializing implementation is needed.
public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized
    // There is no error, the data is simply not there.
    string AnyPropertyInDerivedFromList { get; set; }     
}

public class Demo
{
    // this properties will be seralized
    string AnyPropetyInDemo { get; set; }  
}

进一步阅读:当类从List继承时,XmlSerializer不会序列化其他属性

改用IList

我个人不会继承List,而是实现IList。Visual Studio将为您完成这项工作,并创建一个完整的工作流程。看这里:如何获得IList的完整工作实现

其他回答

这取决于“团队”对象的行为。如果它的行为就像一个集合,那么可以先用一个普通的List来表示它。然后,您可能会注意到,您一直在复制在列表上迭代的代码;此时,您可以选择创建一个FootballTeam对象来包装球员列表。FootballTeam类成为在球员列表上迭代的所有代码的主场。

这使我的代码变得不必要地冗长。我现在必须调用my_team.Players.Count而不是my_team.Count。幸运的是,使用C#,我可以定义索引器以使索引透明,并转发内部List的所有方法。。。但这是很多代码!我做这些工作能得到什么?

封装。你的客户不需要知道足球队内部发生了什么。你的客户都知道,它可以通过在数据库中查找玩家列表来实现。他们不需要知道,这会改进您的设计。

这显然没有任何意义。足球队没有球员名单。这是球员名单。你不会说“约翰·麦克足球先生加入了SomeTeam的球员”。你说“John加入了SomeTeam”。您不向“字符串的字符”添加字母,而是向字符串添加字母。您不向图书馆的图书添加图书,而是向图书馆添加图书。

准确地说:)你会说footballTeam.Add(john),而不是footballTeam.List.Add(john)。内部列表将不可见。

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

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

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

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

--已编辑

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

--第2版

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

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

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

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

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

我只是想补充一点,艾菲尔铁塔和合同设计的发明者伯特兰·梅耶(Bertrand Meyer)会让团队继承List<Player>,而不必太费唇舌。

在他的书《面向对象的软件构造》中,他讨论了GUI系统的实现,其中矩形窗口可以有子窗口。他只是让Window继承自Rectangle和Tree<Window>,以重用实现。

然而,C#不是埃菲尔铁塔。后者支持功能的多重继承和重命名。在C#中,当您子类化时,您继承了接口和实现。您可以重写实现,但调用约定直接从超类复制。然而,在Eiffel中,您可以修改公共方法的名称,因此您可以在团队中将Add和Remove重命名为Hire和Fire。如果Team的一个实例被上传回List<Player>,调用方将使用AddandRemove来修改它,但将调用Hire和Fire虚拟方法。

如果您的类用户需要List拥有的所有方法和财产,您应该从中派生类。如果他们不需要,请将List括起来,并为类用户实际需要的方法制作包装器。

这是一个严格的规则,如果你编写了一个公共API,或者其他很多人都会使用的代码。如果你有一个很小的应用程序,并且开发者不超过2个,你可以忽略这个规则。这将为您节省一些时间。

对于小型应用程序,您也可以考虑选择另一种不那么严格的语言。Ruby、JavaScript——任何允许您编写更少代码的东西。