为什么不从List<T>继承?

在规划我的程序时,我经常从这样的一连串想法开始:

一个足球队只是一个足球运动员的名单。因此,我应该代表它:

var football_team = new List<FootballPlayer>();

此列表的顺序表示球员在名册中列出的顺序。

但我后来意识到,除了球员名单之外,球队还有其他必须记录的属性。例如,本赛季的总比分、当前预算、统一的颜色、代表球队名称的string等。

于是我想:

好的,足球队就像一个球员列表,但除此之外,它还有一个名字(astring)和一个持续的总得分(aint)。. NET没有提供用于存储足球队的类,所以我将制作自己的类。最相似和最相关的现有结构是List<FootballPlayer>,所以我将从它继承:

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

但事实证明,指南说你不应该从#0继承。我对这个指导方针在两个方面感到非常困惑。

为什么不呢?

显然#0在某种程度上优化了性能。为什么?如果我扩展List,会导致什么性能问题?到底会破坏什么?

我看到的另一个原因是List是微软提供的,我无法控制它,所以在暴露了一个“公共API”之后,我无法稍后更改它。但我很难理解这一点。什么是公共API,我为什么要关心?如果我当前的项目没有也不可能有这个公共API,我可以放心地忽略这一准则吗?如果我继承了List,事实证明我需要一个公共API,我会有什么困难?

这有什么关系?列表就是列表。有什么可能改变?我可能想改变什么?

最后,如果微软不希望我从List继承,为什么他们不做类sealed

我还能用什么?

显然,对于自定义集合,Microsoft提供了一个Collection类,它应该扩展而不是List。但是这个类非常简单,并且没有很多有用的东西,例如如#2jvitor83的答案为该特定方法提供了性能原理,但是缓慢的AddRange为什么不比没有AddRange好呢?

Collection继承比从List继承要多得多,我看不到任何好处。当然,微软不会无缘无故地让我做额外的工作,所以我不禁觉得我在某种程度上误解了什么,继承Collection实际上不是我问题的正确解决方案。

我已经看到了实现IList之类的建议。只是没有。这是几十行样板代码,对我没有任何好处。

最后,有些人建议用某种东西包装List

class FootballTeam{public List<FootballPlayer> Players;}

这有两个问题:

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

  2. 它只是没有任何意义。一个足球队没有“有”一个球员名单。它是球员名单。你不会说“约翰·麦克足球先生加入了某个团队的球员”。你会说“约翰加入了某个团队”。你不会在“字符串的字符”中添加一个字母,你会在字符串中添加一个字母。你不会把一本书添加到图书馆的书中,你会把一本书添加到图书馆。

我意识到“引擎盖下”发生的事情可以说是“将X添加到Y的内部列表中”,但这似乎是一种非常违反直觉的思考世界的方式。

我的问题(摘要)

什么是表示数据结构的正确C#方式,“逻辑上”(也就是说,“对人类思维”)只是things中的list,只有一些花里胡哨的东西?

List<T>继承总是不可接受的吗?什么时候可以接受?为什么/为什么不?程序员在决定是否从List<T>继承时必须考虑什么?

235830 次浏览

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

如果您想使用LINQ,实现ICollection<T>IEnumerable<T>等通用接口会更有意义。

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

哇,你的帖子有一大堆问题和观点。你从微软得到的大部分推理都很中肯。让我们从List<T>开始吧

  • List<T>高度优化。它的主要用途是用作对象的私有成员。
  • 微软没有密封它,因为有时你可能想创建一个名称更友好的类:class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }。现在它和var list = new MyList<int, string>();一样简单。
  • CA1002:不要公开泛型列表:基本上,即使你计划作为唯一的开发人员使用这个应用程序,开发良好的编码实践也是值得的,因此它们会逐渐灌输给你和第二天性。如果你需要任何消费者拥有索引列表,你仍然可以将列表公开为IList<T>。这允许你以后更改类中的实现。
  • 微软使Collection<T>非常通用,因为它是一个通用的概念……名称说明了一切;它只是一个集合。还有更精确的版本,如SortedCollection<T>ObservableCollection<T>ReadOnlyCollection<T>等,每个版本都实现IList<T>,但没有List<T>
  • Collection<T>允许覆盖成员(即添加、删除等),因为它们是虚拟的。List<T>没有。
  • 你问题的最后一部分是正确的。一支足球队不仅仅是一份球员名单,所以它应该是一个包含球员名单的类。想想组合vs继承。一支足球队已经是一份球员名单(名册),它不是一份球员名单。

如果我写这段代码,这个类可能看起来像这样:

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#表示数据结构的方式,“逻辑上”(也就是说,“人类思维”)只是一个带有一些花里胡哨的事物列表?

问任何十个熟悉足球存在的非计算机程序员来填补空白:

足球队是一种特殊的_____

任何人是说“有一些花里胡哨的足球运动员名单”,还是他们都说“运动队”、“俱乐部”或“组织”?你认为足球队是一种特殊的球员名单的概念在你的人类头脑中,只有你的人类头脑。

List<T>是一个机制。足球队是一个业务对象——也就是说,一个代表程序业务域中某个概念的对象。不要把它们混在一起!一支足球队是一种;它有一个花名册,花名册是一份球员名单。花名册不是Roster0。花名册Roster1是一个球员名单。所以做一个名为Roster的属性,这是一个List<Player>。当你在那里的时候,让它成为ReadOnlyList<Player>,除非你相信每个了解足球队的人都可以从花名册上删除球员。

List<T>继承总是不可接受的吗?

谁不能接受?我?不。

什么时候可以接受?

当你建立一个扩展#0机制的机制时。

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

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

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

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

更新

我又想了一下,还有另一个理由不把足球队建模为球员名单。事实上,把足球队建模为球员名单可能是个坏主意。作为/拥有球员名单的球队的问题是,你得到的是球队在某一时刻的第一名。我不知道你对这个类的商业案例是什么,但如果我有一个代表足球队的类,我会问它一些问题,比如“2003年至2013年间有多少海鹰队球员因伤缺席比赛?"或者"以前为另一支球队效力的丹佛球员的码数同比增幅最大?"或"小猪队今年一路走下去了吗?"

也就是说,在我看来,一支足球队似乎可以很好地模拟为历史事实的汇编,例如当一名球员被招募,受伤,退役等时,显然当前的球员名单是一个重要的事实,可能应该是前沿和中心,但可能还有其他有趣的事情你想用这个对象来做,需要更多的历史视角。

这取决于背景

当你把你的团队看成一个球员名单时,你是在把足球队的“理念”投射到一个方面:你把“团队”简化为你在球场上看到的人。这种投射只在特定的背景下是正确的。在不同的背景下,这可能是完全错误的。想象一下,你想成为球队的赞助商。所以你必须和球队的经理交谈。在这种情况下,球队被投射到经理名单上。这两个名单通常不会有太多重叠。其他背景是现在和以前的球员,等等。

语义学

因此,将团队视为其参与者列表的问题在于,其语义取决于上下文,并且当上下文更改时无法扩展。此外,很难表达您正在使用的上下文。

类是可扩展的

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

类更复杂

在某些情况下,创建一个额外的类可能是多余的。每个类定义都必须通过类加载器加载,并由虚拟机缓存。这会降低运行时性能和内存。当你有一个非常特定的上下文时,将足球队视为球员列表可能是可以的。但在这种情况下,你真的应该使用 IList ,而不是从中派生的类。

结论/考虑

当你有一个非常具体的上下文时,可以将一个团队视为一个玩家列表。例如,在一个方法内部,完全可以这样写:

IList<Player> footballTeam = ...

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

type FootballTeam = IList<Player>

但是,当上下文更广泛甚至不清楚时,您不应该这样做。特别是当您创建一个新类时,其将来可能使用它的上下文不清楚。警告标志是当您开始向您的类添加附加属性(团队名称,教练等)时。这是一个明确的迹象,表明将使用该类的上下文不是固定的,将来会发生变化。在这种情况下,您不能将团队视为球员列表,但您应该将球员列表(当前活跃,未受伤等)建模为团队的属性。

这是组成 vs继承的典型例子。

在这种情况下:

球队是一份有附加行为的球员名单吗

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

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

  1. 您不能限制访问(例如,阻止人们更改名册)。无论您是否需要/想要它们,您都可以获得所有List方法。

  2. 如果你也想列出其他事情会发生什么。例如,球队有教练、经理、球迷、设备等。其中一些很可能是他们自己的列表。

  3. 您限制了继承的选项。例如,您可能想要创建一个通用的Team对象,然后让BasebalTeam、足球队等从该对象继承。要从List继承,您需要从Team继承,但这意味着所有不同类型的团队都必须具有该名册的相同实现。

组合-包括一个对象,在对象内部提供您想要的行为。

继承-您的对象成为具有您想要的行为的对象的实例。

两者都有其用途,但这是一个明显的情况,其中组合是优选的。

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

你会怎么做?和很多事情一样……这取决于上下文。我会用的指南是,为了从另一个类继承,真的应该有一个“是”关系。所以如果你写一个名为BMW的类,它可以继承自Car,因为宝马真的是一辆汽车。马类可以继承自Mammal类,因为马在现实生活中实际上是哺乳动物,任何Mammal功能都应该与马相关。但是你能说一个团队是一个列表吗?从我可以看出,看起来团队并不是一个列表。所以在这种情况下,我将有一个List作为成员变量。

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

以前的代码意思是:一群在街上踢足球的人,他们碰巧有一个名字。比如:

人们踢足球

无论如何,这个代码(来自m-y的答案)

public class FootballTeam{// A team's namepublic 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> playerspublic string TeamName;public int RunningTotal}

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

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

封装。您的客户不需要知道足球队内部发生了什么。对于您所有的客户来说,它可以通过在数据库中查找球员名单来实现。他们不需要知道,这改进了您的设计。

这显然没有任何意义。一支足球队没有“拥有”球员名单。这是球员名单。你不会说“约翰·麦克足球先生加入了某个团队的球员”。你会说“约翰加入了某个团队”。你不会在“字符串的字符”中添加一个字母,你会在字符串中添加一个字母。你不会在图书馆的书中添加一本书,你会在图书馆中添加一本书。

确切地说:)你会说足球队。添加(约翰),而不是足球队。列表。添加(约翰)。内部列表将不可见。

正如每个人都指出的那样,一个球员团队不是一个球员名单。这个错误是很多人都在犯的,也许是在不同的专业水平上。通常问题是微妙的,偶尔非常严重,就像在这种情况下一样。这样的设计是不好的,因为这些违反了李斯科夫替换原理。互联网上有很多很好的文章解释了这个概念,例如,http://en.wikipedia.org/wiki/Liskov_substitution_principle

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

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

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

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

  • 足球队是足球运动员的列表吗?(列表的所有属性是否适用于具有相同含义的球队)
    • 团队是同质实体的集合吗?是的,团队是玩家的集合
    • 球员的加入顺序是否能说明球队的状态,球队是否确保顺序保持不变,除非明确改变?否,否
    • 球员是否应该根据他们在团队中的顺序位置被包括/删除?没有

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

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

表示数据结构的正确 C#方式是什么…

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

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

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

--编辑

我最好的答案是……这取决于。从List继承将使此类的客户端暴露于可能不应该暴露的方法,主要是因为足球团队看起来像一个业务实体。

--第2版

我真的不记得我在“不要过度设计”评论中提到的是什么了。虽然我相信KISS思维模式是一个很好的指南,但我想强调的是,从List继承业务类会产生比它解决的更多的问题,因为抽象泄漏

另一方面,我相信在有限的情况下,简单地从List继承是有用的。正如我在上一版中所写的,这取决于。每个案例的答案都受到知识、经验和个人偏好的严重影响。

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

是否允许人们说

myTeam.subList(3, 5);

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

设计>实施

你公开什么方法和属性是一个设计决策,你继承什么基类是一个实现细节。我觉得回到前者是值得的。

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

你的第一个问题应该是:

  • 此对象在我正在创建的模型中包含哪些数据?
  • 这个对象在这个模型中表现出什么行为?
  • 这种情况将来会如何改变?

请记住,继承意味着“isa”(是)关系,而组合意味着“有”(hasa)关系。根据您的视图中的情况选择合适的一个,并记住随着应用程序的发展,事情可能会发展到哪里。

在考虑具体类型之前,请考虑在界面中进行思考,因为有些人发现这样更容易将他们的大脑置于“设计模式”中。

这不是每个人在日常编码中都有意识地做的事情。但是,如果你正在考虑这种主题,你就进入了设计水域。意识到它可以是一种解放。

考虑设计细节

看看MSDN或Visual Studio上的List<T>IList<T>。看看它们公开了哪些方法和属性。这些方法看起来都像有人想对您视图中的FootballTeam做的事情吗?

footballTeam.Reverse()对你有意义吗?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++试过这个恶意,你可能会认为这是一件好事。)通过继承实现一个集合,通过组合实现一个集合可能会感觉脏。除非你明确实现ILIst<Player>.CountIList<StaffMember>.Count等,否则Count等属性会让用户感到困惑,然后它们只是痛苦而不是困惑。你可以看到这将走向何方;沿着这条路思考时的直觉很可能会告诉你朝着这个方向前进是错误的(无论对错,如果你以这种方式实施,你的同事也可能会这样做!)

简短的回答(太晚了)

关于不从集合类继承的准则并不是C#特定的,你会在许多编程语言中找到它。这是公认的智慧而不是法律。一个原因是在实践中,组合被认为经常在可理解性、可实现性和可运维性方面胜过继承。在现实世界/领域对象中,找到有用和一致的“hasa”关系比有用和一致的“isa”关系更常见,除非你深入抽象,尤其是随着时间的推移和代码中对象的精确数据和行为的变化。这不应该让你总是排除从集合类继承的可能性;但它可能是提示性的。

如果FootballTeam有一个预备队和主队呢?

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

你会如何建模?

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

关系显然是有一个而不是是一个

RetiredPlayers

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

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

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

FootballTeam的情况下,听起来不太对。Team不仅仅是Collection。正如其他答案所指出的那样,Team可以有教练,培训师等。

FootballCollection听起来像是足球的集合,或者是足球用具的集合。TeamCollection,团队的集合。

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

实际上List<FootballPlayer>是一个非常好的处理类型。如果您从方法返回它,可能是IList<FootballPlayer>

总结

问问你自己

  1. X aY?还是X aY

  2. 我的类名称意味着他们是什么?

我肮脏的秘密:我不在乎人们说什么,我做到了。. NET Framework以“XxxxCollection”(UIElementCollection for top of my head示例)传播。

是什么阻止了我说:

team.Players.ByName("Nicolas")

当我发现它比

team.ByName("Nicolas")

此外,我的PlayerCollection可能会被其他类使用,例如“Club”,而没有任何代码重复。

club.Players.ByName("Nicolas")

昨天的最佳实践可能不是明天的最佳实践。大多数最佳实践背后没有理由,大多数只是社区之间的广泛共识。当你这样做时,不要问社区是否会责怪你,问问自己,什么更具可读性和可维护性?

team.Players.ByName("Nicolas")

team.ByName("Nicolas")

真的吗?你有疑问吗?现在也许你需要处理其他技术限制,这些限制阻止你在实际用例中使用List。但是不要添加不应该存在的限制。如果微软没有记录为什么,那么这肯定是一个不知从何而来的“最佳实践”。

这里有很多很好的答案,但我想触及一些我没有看到提到的东西:面向对象的设计是关于赋予对象权力的。

你想把你所有的规则、额外的工作和内部细节封装在一个合适的对象中。这样,与此交互的其他对象就不必担心这一切。事实上,你想更进一步,积极地防止其他对象绕过这些内部。

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

假设你想通过知道一个玩家是退休、辞职还是被解雇来区分他们何时离开。你可以实现一个RemovePlayer方法,它接受适当的输入枚举。然而,通过从List继承,你将无法阻止直接访问RemoveRemoveAll甚至Clear。结果,你实际上已经d被授权了你的FootballTeam类。


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

它使我的代码不必要地冗长。我现在必须调用my_team. Player. Count而不仅仅是my_team. Count。

你是对的,如果所有客户都使用你的团队,那将是不必要的冗长。然而,与你已经向所有人公开List Players这样他们就可以在未经你同意的情况下摆弄你的团队这一事实相比,这个问题非常小。

你接着说:

这显然没有任何意义。一支足球队没有“拥有”球员名单。这是球员名单。你不会说“约翰·麦克足球运动员加入了某个团队的球员”。你说“约翰加入了某个团队”。

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


希望您能看到封装对赋予对象权力的目标有多重要。您希望允许每个类做好自己的工作,而不必担心来自其他对象的干扰。

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

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

让我重写你的问题,这样你就可以从不同的角度看待这个问题。

当我需要代表一支足球队时,我知道它基本上是一个名字。比如:“老鹰队”

string team = new string();

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

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

你进入问题的切入点是任意的。试着想想团队(属性)是什么,而不是它

这样做之后,您可以查看它是否与其他类共享属性。并考虑继承。

如果您的类用户需要**List拥有的所有方法和属性,您应该从中派生您的类。如果他们不需要它们,请附上List并为您的类用户实际需要的方法制作包装器。

如果您编写公共api或任何其他将被许多人使用的代码,这是一个严格的规则。如果您有一个小应用程序并且不超过2名开发人员,您可以忽略此规则。这将节省您一些时间。

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

指导方针说的是,公共API不应该揭示你使用列表、集合、字典、树或其他什么的内部设计决策。“团队”不一定是列表。你可以将其实现为列表,但公共API的用户应该在需要知道的基础上使用你的类。这允许你在不影响公共接口的情况下改变决定并使用不同的数据结构。

仅仅因为我认为其他答案几乎与足球队是否“是”List<FootballPlayer>或“有”List<FootballPlayer>相切,这实际上并没有回答这个问题。

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

指南说你不应该从List<T>继承。为什么不呢?

因为List<T>没有虚方法。这在您自己的代码中不是问题,因为您通常可以以相对较小的痛苦切换实现-但在公共API中可能会有更大的问题。

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

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

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

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

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

这有什么关系?列表就是列表。有什么可能改变?我可能想改变什么?

这取决于上下文,但是由于我们使用FootballTeam作为示例——假设你不能添加FootballPlayer,如果它会导致团队超过工资帽。添加它的一种可能的方法是:

 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!");}}}

啊……但是你不能override Add,因为它不是virtual(出于性能原因)。

如果您在应用程序中(基本上,这意味着您和所有调用者都一起编译),那么您现在可以更改为使用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 */}

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

太长别读-指南适用于公共API。对于私有API,请随心所欲。

我只是想补充一点,埃菲尔铁塔和合同设计的发明者伯特兰·迈耶,将从List<Player>继承Team,而不必打眼皮。

在他的书面向对象的软件结构中,他讨论了一个GUI系统的实现,其中矩形窗口可以有子窗口。他只需从RectangleTree<Window>继承Window来重用实现。

然而,C#不是Eiffel。后者支持多重继承和Remove1。在C#中,当您子类时,您将继承接口和实现。您可以覆盖实现,但调用约定直接从超类中复制。然而,在Eiffel中,您可以修改公共方法的名称,因此您可以在Team中将AddRemove重命名为HireFire。如果Team的实例向上转换回List<Player>,调用者将使用AddRemove来修改它,但您的虚拟方法HireFire将被调用。

我想我不同意你的概括。一支球队不仅仅是球员的集合。一支球队有更多关于它的信息——名字、徽章、管理/行政人员的集合、教练组的集合,然后是球员的集合。所以,如果要正确地模拟现实世界,你的足球队类应该有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是抽象数据类型的特定实现,现在和将来可能合适,也可能不合适。

从概念上讲,#0数据类型称为“list”这一事实有点转移注意力。System.List<T>是一个可变的有序集合,支持用于添加、插入和删除元素的摊销O(1)操作,以及用于检索元素数量或按索引获取和设置元素的O(1)操作。

接口越小,代码越灵活

在设计数据结构时,接口越简单,代码就越灵活。看看LINQ对这一点的演示有多强大。

如何选择界面

当你想到“列表”时,你应该首先对自己说,“我需要代表一群棒球运动员”。所以假设你决定用一个类来建模它。你首先应该做的是决定这个类需要公开的最小数量的接口。

一些问题可以帮助指导这个过程:

  • 我需要计数吗?如果不考虑实施#0
  • 这个集合在初始化后会改变吗?如果不考虑#0
  • 我可以通过索引访问项目很重要吗?考虑#0
  • 我向集合中添加项目的顺序重要吗?也许它是#0
  • 如果你确实想要这些东西,那么继续实施#0

这样,您就不会将代码的其他部分与棒球运动员集合的实现细节耦合起来,并且只要您尊重接口,就可以自由更改它的实现方式。

通过采用这种方法,您会发现代码变得更容易阅读、重构和重用。

关于避免样板的注意事项

在现代IDE中实现接口应该很容易。右键单击并选择“实现接口”。然后如果需要,将所有实现转发到成员类。

也就是说,如果你发现你写了很多样板,那可能是因为你暴露了比你应该暴露的更多的函数。这与你不应该从类继承的原因相同。

您还可以设计对您的应用程序有意义的较小接口,也许只是几个辅助扩展函数来将这些接口映射到您需要的任何其他接口。

序列化问题

缺少一个方面。无法使用XmlSerializer正确序列化从List继承的类。在这种情况下,必须使用DataCompltSerializer,或者需要自己的序列化实现。
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 seralizedstring AnyPropetyInDemo { get; set; }}

更多阅读:从List<>继承类时,XmlSerializer不会序列化其他属性

改用IList

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

什么时候可以接受?

引用Eric Lippert的话:

当你建立一个机制时扩展#0机制。

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

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