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

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


首先,它与可用性有关。如果使用继承,Team类将公开纯粹为对象操作而设计的行为(方法)。例如,AsReadOnly()或CopyTo(obj)方法对团队对象没有意义。与AddRange(items)方法不同,您可能需要一个更具描述性的AddPlayers(players)方法。

如果您想使用LINQ,实现诸如ICollection<T>或IEnumerable<T>之类的通用接口将更有意义。

如前所述,合成是正确的方法。只需将玩家列表作为一个私有变量来实现。


哇,你的帖子有很多问题和观点。你从微软那里得到的大部分理由都是正确的。让我们从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.
}

这里有一些很好的答案。我想补充以下几点。

正确的C#表示数据结构的方式是什么?数据结构“逻辑上”(也就是说,“对人类的大脑”)只是一个带有一些铃铛和口哨的列表?

请任何十位熟悉足球存在的非计算机程序员填写空白:

足球队是一种特殊的_____

有没有人说“足球运动员名单”,或者他们都说“运动队”、“俱乐部”或“组织”?你认为一支足球队是一个特殊类型的球员名单,这是你的人的想法,也是你个人的想法。

列表<T>是一种机制。足球队是一个业务对象——也就是说,一个表示程序业务领域中某个概念的对象。不要把它们混在一起!足球队是一种球队;它有一个名册,名册是一个球员名单。名册不是一种特定的球员名单。名册是球员名单。因此,创建一个名为“花名册”的属性,它是一个列表<Player>。如果你不相信任何一个了解足球队的人都可以从名单中删除球员,那就让它成为ReadOnlyList<Player>。

从列表<T>继承总是不可接受的吗?

谁不能接受?我不

什么时候可以接受?

当您构建扩展List<T>机制的机制时。

程序员在决定是否从List<T>继承时,必须考虑什么?

我是在构建机制还是业务对象?

但这是很多代码!我做这些工作能得到什么?

你花了更多的时间来输入你的问题,这可能需要你为List<T>的相关成员写50次转发方法。你显然不害怕冗长,我们这里讨论的是非常少量的代码;这是几分钟的工作。

更新

我想了想,还有一个理由不把足球队列为球员名单。事实上,将一支足球队建模为也有球员名单可能是一个坏主意。一支球队有一个球员名单的问题是,你得到的是球队在某一时刻的快照。我不知道你在这门课上的商业案例是什么,但如果我有一门代表足球队的课,我想问它一些问题,比如“2003年至2013年期间,有多少海鹰队球员因伤缺席比赛?”或“之前为另一支球队效力的丹佛队球员的跑动码数同比增幅最大?”或是“小猪队今年一路走好吗?”

也就是说,在我看来,足球队似乎是一个历史事实的集合,例如球员何时被招募、受伤、退役等。显然,目前的球员名册是一个重要的事实,可能应该是前锋和中锋,但可能还有其他有趣的事情需要更多的历史视角。


这取决于上下文

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

语义不清晰

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

类是可扩展的

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

类更复杂

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

结论/注意事项

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

IList<Player> footballTeam = ...

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

type FootballTeam = IList<Player>

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


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

在这种特定情况下:

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

or

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

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

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

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

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

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


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

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


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

以前的代码意味着:一群来自街头的人踢足球,他们碰巧有一个名字。类似于:

总之,这个代码(来自m-y的答案)

public class FootballTeam
{
    // A team's name
    public string TeamName; 

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

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

    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

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

意思是:这是一支有管理层、球员、管理员等的足球队

这就是你的逻辑在图片中呈现的方式…


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

这在逻辑上是错误的:

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

这是正确的:

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

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

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

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

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

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


正如所有人都指出的那样,一队球员并不是一个球员名单。世界各地的许多人都犯了这个错误,可能是不同专业水平的人。问题往往很微妙,偶尔也很严重,就像本例中的情况一样。这样的设计是糟糕的,因为它们违反了利斯科夫替代原则。互联网上有许多很好的文章解释这个概念,例如。,http://en.wikipedia.org/wiki/Liskov_substitution_principle

总之,类之间的父/子关系中有两条规则需要保留:

子项不应要求低于完全定义父项的特征。除了完全定义孩子之外,父母不应要求任何特征。

换句话说,父级是子级的必要定义,子级是父级的充分定义。

这里有一种方法来思考解决方案并应用上述原则,这将有助于避免这种错误。我们应该通过验证父类的所有操作在结构和语义上是否对派生类有效来检验假设。

足球队是足球运动员的名单吗?(列表的所有财产是否以相同的含义应用于团队)团队是同质实体的集合吗?是的,球队是球员的集合球员的出场顺序是否描述了球队的状态?球队是否确保除非明确更改,否则顺序将被保留?没有,也没有球员是否会根据他们在球队中的顺序被纳入/淘汰?不

如您所见,只有列表的第一个特征适用于团队。因此,团队不是列表。列表将是您如何管理团队的实现细节,因此它只能用于存储玩家对象,并使用team类的方法进行操作。

在这一点上,我想指出,在我看来,Team类甚至不应该使用List实现;在大多数情况下,它应该使用Set数据结构(例如HashSet)来实现。


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

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

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

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

--已编辑

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

--第2版

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

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

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


允许人们说

myTeam.subList(3, 5);

有什么意义吗?如果不是,那么它不应该是一个列表。


设计>实施

您公开的方法和财产是一个设计决策。您从中继承的基类是一个实现细节。我觉得回到从前是值得的。

对象是数据和行为的集合。

因此,您的第一个问题应该是:

这个对象在我创建的模型中包含哪些数据?该对象在该模型中表现出什么行为?这在未来会发生什么变化?

记住,继承意味着“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”关系更常见,除非你深入抽象,尤其是随着时间的推移和代码中对象的精确数据和行为的改变。这不应该导致您总是排除从集合类继承;但它可能是暗示性的。


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

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?我的类名是什么意思?


我肮脏的秘密:我不在乎别人说什么,我也这么做。.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>。但不要添加不应该存在的约束。如果微软没有记录原因,那么这无疑是一个“最佳实践”。


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

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

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

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


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

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

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

你接着说:

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

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


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


当他们说List<T>是“优化的”时,我想他们想说的是,它没有像虚拟方法那样昂贵的功能。所以问题是,一旦在公共API中公开List<T>,就失去了执行业务规则或稍后自定义其功能的能力。但是,如果您在项目内部使用这个继承的类(而不是作为API潜在地暴露给成千上万的客户/合作伙伴/其他团队),那么如果它节省了您的时间,并且它是您想要复制的功能,那就没问题了。从List<T>继承的优点是,您消除了许多在可预见的将来永远无法定制的哑包装器代码。此外,如果您希望您的类在API的生命周期内显式地具有与List<T>完全相同的语义,那么它也可以。

我经常看到很多人做大量的额外工作,只是因为FxCop规则这么说,或者有人的博客说这是一种“糟糕”的做法。很多时候,这将代码转化为设计模式palooza的怪异。与许多准则一样,将其视为可能存在例外的准则。


让我改写你的问题。所以你可能会从不同的角度来看待这个问题。

当我需要代表一支足球队时,我明白这基本上是一个名字。比如:《老鹰》

string team = new string();

后来我意识到球队也有球员。

为什么我不能扩展字符串类型,以便它也包含一个玩家列表?

你的切入点是武断的。试着想想一个团队有什么(财产),而不是它是什么。

完成后,您可以查看它是否与其他类共享财产。想想遗产。


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

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

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


指南所说的是,公共API不应透露您是否使用列表、集合、字典、树或其他任何东西的内部设计决策。“团队”不一定是列表。您可以将其实现为列表,但公共API的用户应该在需要知道的基础上使用您的类。这允许您在不影响公共接口的情况下更改决策并使用不同的数据结构。


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

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虚拟方法。


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

虽然我没有像大多数答案那样进行复杂的比较,但我想分享一下处理这种情况的方法。通过扩展IEnumerable<T>,您可以允许Team类支持Linq查询扩展,而无需公开List<T>的所有方法和财产。

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}

首选接口而非类

类应该避免从类派生,而是实现所需的最小接口。

继承中断封装

从类派生会破坏封装:

公开有关如何实现集合的内部详细信息声明可能不合适的接口(公共函数和财产集)

除此之外,这使得重构代码变得更加困难。

类是实现细节

类是一个实现细节,应该对代码的其他部分隐藏。简而言之,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接口中采用的方法。


序列化问题

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的完整工作实现


什么时候可以接受?

引用Eric Lippert的话:

当您构建扩展List<T>机制的机制时。

例如,您厌倦了IList<T>中缺少AddRange方法:

public interface IMoreConvenientListInterface<T> : IList<T>
{
    void AddRange(IEnumerable<T> collection);
}

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }