ChatGPT解决这个技术问题 Extra ChatGPT

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

在计划我的程序时,我经常会从这样一个思路开始:

足球队只是足球运动员的名单。因此,我应该用以下方式表示它: var football_team = new List();此列表的顺序代表球员在名册中列出的顺序。

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

那么我想:

好吧,一个足球队就像一个球员名单,但另外,它有一个名字(一个字符串)和一个总得分(一个 int)。 .NET 没有提供用于存储足球队的类,所以我将创建自己的类。最相似和相关的现有结构是 List,所以我将继承它: class FootballTeam : List { public string TeamName;公共 int RunningTotal }

但事实证明,a guideline says you shouldn't inherit from List<T>。我在两个方面完全被这个指导方针弄糊涂了。

为什么不?

显然是List is somehow optimized for performance。怎么会这样?如果我扩展 List,会导致什么性能问题?究竟会破坏什么?

我看到的另一个原因是 List 是由 Microsoft 提供的,我无法控制它,所以 I cannot change it later, after exposing a "public API"。但我很难理解这一点。什么是公共 API,我为什么要关心?如果我当前的项目没有也不太可能有这个公共 API,我可以安全地忽略这个指南吗?如果我确实继承自 List 并且 结果我需要一个公共 API,我会有什么困难?

为什么它甚至重要?一个列表就是一个列表。有什么可能改变?我可能想要改变什么?

最后,如果 Microsoft 不希望我从 List 继承,他们为什么不创建类 sealed

我还应该使用什么?

显然,对于自定义集合,Microsoft 提供了一个 Collection 类,它应该被扩展而不是 List。但是这个类非常简单,没有很多有用的东西,例如such as AddRangejvitor83's answer 为该特定方法提供了性能原理,但是慢速 AddRange 不比没有 AddRange 好?

Collection 继承比从 List 继承要多得多,而且我看不出有什么好处。微软肯定不会无缘无故告诉我做额外的工作,所以我不禁觉得我在某种程度上误解了一些东西,继承 Collection 实际上不是解决我问题的正确方法。

我看到了一些建议,例如实施 IList。就是不行。这是几十行样板代码,对我没有任何好处。

最后,有人建议将 List 包装在一些东西中:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

这有两个问题:

它使我的代码不必要地冗长。我现在必须调用 my_team.Players.Count 而不仅仅是 my_team.Count。值得庆幸的是,使用 C# 我可以定义索引器以使索引透明化,并转发内部 List 的所有方法……但那是很多代码!所有这些工作我能得到什么?简单明了没有任何意义。足球队没有“拥有”球员名单。这是球员名单。您不会说“John McFootballer 已加入 SomeTeam 的球员”。您说“John 已加入 SomeTeam”。您不会将字母添加到“字符串的字符”,而是将字母添加到字符串。您不会将一本书添加到图书馆的书籍中,而是将一本书添加到图书馆。

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

我的问题(总结)

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

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

那么,您的问题真的是何时使用继承(Square is-a Rectangle,因为在请求通用 Rectangle 时提供 Square 总是合理的)以及何时使用组合(此外,一个 FootballTeam 有一个 FootballPlayers 列表其他与它一样“基本”的属性,比如名字)?如果其他程序员在期望一个简单的列表时在代码的其他地方传递了一个 FootballTeam,他们会感到困惑吗?
如果我告诉你足球队是一家商业公司,它拥有一份球员合同列表,而不是拥有一些额外属性的球员名单?
这真的很简单。一个足球队是一个球员名单吗?显然不是,因为就像你说的,还有其他相关的属性。足球队有球员名单吗?是的,所以它应该包含一个列表。除此之外,组合通常比继承更可取,因为以后更容易修改。如果您发现继承和组合同时符合逻辑,请使用组合。
@FlightOdyssey:假设您有一个 SquashRectangle 方法,它采用一个矩形,将其高度减半,并将其宽度加倍。现在传入一个恰好是 4x4 但类型为矩形的矩形和一个 4x4 的正方形。调用 SquashRectangle 后对象的尺寸是多少?对于矩形,它显然是 2x8。如果正方形是 2x8,则它不再是正方形;如果不是 2x8,那么对相同形状的相同操作会产生不同的结果。结论是可变正方形不是一种矩形。
@Gangnus:您的陈述完全是错误的;真正的子类需要具有作为超类的超集 的功能。需要 string 才能完成 object 可以做的所有事情以及更多

E
Eric Lippert

这里有一些很好的答案。我会向他们补充以下几点。

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

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

足球队是一种特殊的____

有人说“花里胡哨的足球运动员名单”,还是都说“运动队”或“俱乐部”或“组织”?您认为足球队是特定类型的球员名单的想法存在于您的人类思想中,并且仅存在于您的人类思想中。

List<T> 是一个机制。足球队是一个业务对象——也就是说,一个代表程序业务领域中的某个概念的对象。不要混合这些!足球队是一种球队;它有一个名册,一个名册是一个球员名单。名册不是特定类型的球员名单。名册球员名单。因此,创建一个名为 Roster 的属性,它是一个 List<Player>。并且在你参与的时候让它ReadOnlyList<Player>,除非你相信每个了解足球队的人都会从名单中删除球员。

从 List 继承总是不可接受的吗?

谁不能接受?我?不。

什么时候可以接受?

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

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

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

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

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

更新

我对此进行了更多思考,还有另一个理由不将足球队建模为球员名单。事实上,将足球队建模为拥有球员名单可能是个坏主意。球队作为/拥有球员名单的问题在于,你所拥有的是球队在某一时刻快照。我不知道你对这门课的商业案例是什么,但如果我有一个代表足球队的课,我想问它这样的问题:“2003 年至 2013 年间有多少海鹰队球员因伤缺席比赛?”或“之前为另一支球队效力的哪位丹佛球员的码数同比增幅最大?”或“Did the Piggers go all the way this year?

也就是说,在我看来,一支足球队被很好地建模为历史事实的集合,例如球员何时被招募、受伤、退役等。显然,目前的球员名单是一个重要的事实,可能应该是前面和 -中心,但您可能还想对这个对象做一些其他有趣的事情,这些事情需要更多的历史视角。


尽管其他答案非常有帮助,但我认为这个答案最直接地解决了我的担忧。至于夸大代码量,你说得对,最终它并没有那么多工作,但是当我在我的程序中包含一些代码而不理解我为什么包含它时,我确实很容易感到困惑。
@Superbest:很高兴我能帮上忙!你听这些质疑是对的;如果你不明白为什么要编写一些代码,要么弄清楚,要么编写你理解的不同代码。
@Mehrdad但是您似乎忘记了优化的黄金法则:1)不要这样做,2)不要这样做,3)如果没有首先进行性能分析以显示需要优化的内容,请不要这样做。
@Mehrdad:老实说,如果您的应用程序需要您关心虚拟方法的性能负担,那么任何现代语言(C#、Java 等)都不适合您。我这里有一台笔记本电脑,可以模拟我小时候玩的每一个街机游戏。这让我不必担心这里和那里的间接级别。
@Superbest:现在我们肯定处于光谱的“组合而不是继承”端。火箭由火箭部件组成。火箭不是“一种特殊的零件清单”!我建议你进一步修剪它;为什么是列表,而不是 IEnumerable<T> - 序列
N
Nigel

哇,你的帖子有一大堆问题和要点。你从微软那里得到的大部分推理都是正确的。让我们从有关 List<T> 的一切开始

List 是高度优化的。它的主要用途是用作对象的私有成员。

Microsoft 没有密封它,因为有时您可能想要创建一个具有更友好名称的类:class MyList : List> { ... }。现在就像 var list = new MyList(); 一样简单。

CA1002:不要公开通用列表:基本上,即使您打算将此应用程序用作唯一的开发人员,使用良好的编码实践进行开发也是值得的,因此它们会成为您的第二天性。如果您需要任何消费者拥有索引列表,您仍然可以将列表公开为 IList。这使您可以稍后更改类中的实现。

Microsoft 使 Collection 非常通用,因为它是一个通用概念……名称说明了一切;它只是一个集合。还有更精确的版本,例如 SortedCollection、ObservableCollection、ReadOnlyCollection 等。每个版本都实现了 IList,但没有实现 List

Collection 允许覆盖成员(即添加、删除等),因为它们是虚拟的。 List 没有。

你问题的最后一部分是正确的。足球队不仅仅是一个球员名单,所以它应该是一个包含球员名单的类。想想组合与继承。足球队有一个球员名单(名单),它不是球员名单。

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

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.
}

@Brian:除了不能使用别名创建泛型之外,所有类型都必须是具体的。在我的示例中,List<T> 被扩展为一个私有内部类,它使用泛型来简化 List<T> 的使用和声明。如果扩展名只是 class FootballRoster : List<FootballPlayer> { },那么是的,我可以使用别名。我想值得注意的是,您可以添加其他成员/功能,但它是有限的,因为您不能覆盖 List<T> 的重要成员。
如果这是 Programmers.SE,我会同意当前的答案投票范围,但是,因为它是一个 SO 帖子,所以这个答案至少试图回答 C# (.NET) 特定问题,即使有明确的一般性设计问题。
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden 现在 that 是不从 List 继承的一个很好的理由。其他答案的哲学太多了。
如果调用 List<T>.GetEnumerator(),则会得到一个名为 Enumerator 的结构。如果你调用 IList<T>.GetEnumerator(),你会得到一个类型为 IEnumerable<T> 的变量,它恰好包含所述结构的装箱版本。在前一种情况下,foreach 将直接调用方法。在后者中,所有调用都必须通过接口虚拟分派,从而使每个调用变慢。 (在我的机器上大约慢两倍。)
你完全没有抓住重点。 foreach 不关心 Enumerator 是否实现 IEnumerable<T> 接口。如果它可以在 Enumerator 上找到它需要的方法,那么它就不会使用 IEnumerable<T>。如果您实际反编译代码,您可以看到 foreach 在迭代 List<T> 时如何避免使用虚拟调度调用,但会使用 IList<T>
F
Faither
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

以前的代码意思是:一群来自街头踢足球的人,他们碰巧有一个名字。就像是:

https://i.stack.imgur.com/PqKUd.jpg

无论如何,这段代码(来自我的回答)

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.
}

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

https://i.stack.imgur.com/4QQ31.jpg

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


我希望你的第二个例子是足球俱乐部,而不是足球队。足球俱乐部有经理和管理员等。From this source:“足球队是一组球员的统称......可以选择这样的球队与对手进行比赛球队,代表一个足球俱乐部……”。所以我认为一个团队球员列表(或者更准确地说是球员的集合)。
更优化的private readonly List<T> _roster = new List<T>(44);
必须导入 System.Linq 命名空间。
@Ben 在我看来,Nean Der Thal 的回答是正确的。一支足球队包含管理层(教练、助理等)、球员(以及一个重要说明:“球员被选中参加比赛”,因为并非球队中的所有球员都会被选中参加例如冠军联赛)、管理员等. 足球俱乐部就像办公室里的人、体育场、球迷、俱乐部董事会,最后但同样重要的是:多支球队(如一线队、女队、青年队等)不要相信维基百科所说的一切;)
@E.Verdi 我认为第二张照片的背景是一个体育场,正如您所建议的那样,它使它看起来代表一个俱乐部而不仅仅是一支球队。归根结底,这些术语的定义只是语义(我可能称之为团队,其他人可能称之为名册等)。我认为重点在于,如何对数据进行建模取决于它的用途。这是一个很好的观点,我认为这些图片有助于展示这一点:)
P
Peter Mortensen

这是 compositioninheritance 的经典示例。

在这种特定情况下:

团队是具有附加行为的玩家列表吗

或者

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

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

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

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

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

两者都有其用途,但这是一个明显的案例,其中组合更可取。


扩展#2,一个列表拥有一个列表是没有意义的。
首先,这个问题与组合与继承无关。答案是 OP 不想实现更具体的列表,所以他不应该扩展 List<>。我很扭曲,因为你在 stackoverflow 上的得分很高,应该清楚地知道这一点,人们相信你说的话,所以现在至少有 55 人投票赞成并且他们的想法很困惑,他们相信任何一种方式都可以构建一个系统,但显然不是! #无怒
@sam 他想要列表的行为。他有两个选择,他可以扩展列表(继承),或者他可以在他的对象中包含一个列表(组合)。也许你误解了问题或答案的一部分,而不是 55 个人错了,你是对的? :)
是的。这是一个退化的案例,只有一个 super 和 sub,但我对他的具体问题给出了更一般的解释。他可能只有一个团队,并且可能总是有一个列表,但选择仍然是继承该列表或将其作为内部对象包含在内。一旦您开始包含多种类型的球队(足球、板球等),并且他们开始不仅仅是一个球员名单,您就会看到您如何拥有完整的组成与继承问题。在这种情况下,着眼大局很重要,以避免及早出现错误的选择,这意味着以后需要进行大量的重构。
OP 没有要求组合,但用例是 X:Y 问题的明确示例,其中 4 个面向对象编程原则之一被破坏、误用或未完全理解。更明确的答案是编写一个专门的集合,如堆栈或队列(似乎不适合用例)或理解组合。足球队不是足球运动员名单。 OP是否明确要求它是无关紧要的,如果不了解抽象,封装,继承和多态性,他们将无法理解答案,因此,X:Y amok
S
Satyan Raina

正如大家所指出的,一队球员不是球员名单。这个错误是由世界各地的许多人所犯的,也许在不同的专业水平上。通常问题是微妙的,有时非常严重,就像在这种情况下一样。这样的设计很糟糕,因为它们违反了 Liskov 替换原则。互联网上有很多很好的文章解释了这个概念,例如,http://en.wikipedia.org/wiki/Liskov_substitution_principle

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

一个孩子不应该要求比完全定义父母的特征少。

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

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

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

足球队是足球运动员的名单吗? (列表的所有属性是否以相同的含义应用于团队)团队是同质实体的集合吗?是的,团队是玩家的集合 包含玩家的顺序是否描述了团队的状态,团队是否确保保留序列,除非明确更改?否,否 是否预计球员会根据他们在球队中的顺序位置而被纳入/放弃?不

团队是同质实体的集合吗?是的,团队是玩家的集合

包含球员的顺序是否描述了球队的状态,球队是否确保保留顺序,除非明确改变?不,不

球员是否会根据他们在球队中的顺序位置被包括/放弃?不

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

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


在 List 与 Set 上取得了不错的成绩。这似乎是一个太常见的错误。当集合中只能有一个元素的实例时,某种集合或字典应该是实现的首选候选者。有两个同名球员的球队是可以的。不能同时包含一个玩家两次。
+1 有一些好处,尽管更重要的是要问“数据结构能否合理地支持所有有用的东西(理想情况下是短期和长期)”,而不是“数据结构做的比有用的多”。
@TonyD好吧,我要提出的观点不是应该检查“数据结构是否比有用的更多”。它是检查“如果父数据结构做了一些与子类可能暗示的行为无关、无意义或违反直觉的事情”。
@TonyD 实际上存在从父级派生的不相关特征的问题,因为在许多情况下它会通过否定测试。程序员可以扩展 Human{ eat();跑(); write();} 来自大猩猩{ eat();跑(); swing();} 认为具有摆动能力的人没有任何问题。然后在游戏世界中,你的人类突然开始绕过所有假定的陆地障碍,只需在树上摆动即可。除非明确规定,实际的人类不应该能够摆动。这样的设计使 api 很容易被滥用和混淆
@TonyD 我也不建议 Player 类应该从 HashSet 派生。我建议 Player 类“在大多数情况下”应该通过 Composition 使用 HashSet 来实现,这完全是实现级别的细节,而不是设计级别的细节(这就是为什么我提到它作为我的答案的旁注)。如果这种实现有正当理由,它可以很好地使用列表来实现。因此,要回答您的问题,是否有必要通过按键进行 O(1) 查找?不,因此也不应该从 HashSet 扩展 Player。
S
Sam Leach

如果 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>

总之

问你自己

是 Y 吗?还是有 X 和 Y?我的班级名称是什么意思吗?


如果每个玩家的类型都将其分类为属于 DefensivePlayersOffensivePlayersOtherPlayers,那么拥有一个可以由需要 List<Player> 但也包含在内的代码使用的类型可能是合法有用的IList<DefensivePlayer>IList<OffensivePlayer>IList<Player> 类型的成员 DefensivePlayersOffsensivePlayersSpecialPlayers。可以使用单独的对象来缓存单独的列表,但是将它们封装在与主列表相同的对象中似乎更干净[使用列表枚举器的失效...
...作为主列表已更改并且子列表在下次访问时需要重新生成的事实的提示]。
虽然我同意所提出的观点,但看到有人提供设计建议并建议在业务对象中公开一个带有公共 getter 和 setter 的具体 List 真的让我心碎:(
T
Toby Speight

设计 > 实施

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

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

所以你的第一个问题应该是:

这个对象在我创建的模型中包含哪些数据?

该对象在该模型中表现出什么行为?

这在未来会如何变化?

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


D
Dmitry S.

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

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

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


P
Peter Mortensen

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

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

string team = new string();

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

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

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

完成此操作后,您可以查看它是否与其他类共享属性。并考虑继承。


这是一个很好的观点 - 人们可以将团队视为一个名字。但是,如果我的应用程序旨在处理玩家的动作,那么这种想法就不太明显了。无论如何,问题似乎最终归结为组合与继承。
这是一种值得看的方式。作为旁注,请考虑这一点:一个有钱人拥有几支足球队,他把一个作为礼物送给朋友,朋友改变了球队的名字,解雇了教练,并替换所有球员。朋友队在果岭上遇到了男队,当男队输球时,他说:“我不敢相信你用我给你的球队打败了我!”这个人是对的吗?你会怎么检查这个?
C
Chad

这取决于上下文

当您将您的球队视为球员名单时,您将足球队的“理念”投射到一个方面:您将“球队”简化为您在球场上看到的人。这种预测仅在特定情况下是正确的。在不同的情况下,这可能是完全错误的。想象一下,您想成为团队的赞助商。因此,您必须与团队的经理交谈。在这种情况下,团队被投射到其经理列表中。这两个列表通常不会重叠太多。其他上下文是当前与以前的参与者等。

语义不明

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

类是可扩展的

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

类更复杂

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

结论/注意事项

当您有非常具体的背景时,可以将团队视为球员列表。例如,在一个方法中完全可以这样写:

IList<Player> footballTeam = ...

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

type FootballTeam = IList<Player>

但是当上下文更广泛甚至不清楚时,你不应该这样做。当您创建一个新类时,尤其是在未来可能使用它的上下文不清楚的情况下,情况尤其如此。一个警告信号是当您开始向您的班级添加其他属性(团队名称、教练等)时。这清楚地表明,将使用该类的上下文不是固定的,并且将来会发生变化。在这种情况下,您不能将球队视为球员列表,但您应该将(当前活跃、未受伤等)球员列表建模为球队的属性。


M
Mauro Sampietro

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

这在逻辑上是错误的:

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

这是正确的:

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

这不能解释为什么。 OP 知道大多数其他程序员都有这种感觉,他只是不明白为什么这很重要。这只是真正提供问题中已经存在的信息。
这也是我首先想到的,他的数据是如何被错误地建模的。所以原因是,你的数据模型不正确。
为什么不从列表继承?因为他不想要更具体的列表。
我不认为第二个例子是正确的。您正在暴露状态,因此正在破坏封装。状态应该隐藏/在对象内部,并且改变这种状态的方法应该是通过公共方法。究竟哪些方法需要根据业务需求来确定,但它应该基于“现在需要这个”,而不是“在某些时候可能会派上用场,我不知道”。保持你的 api 紧密,你会节省时间和精力。
封装不是问题的重点。如果您不希望该类型以更具体的方式运行,我将专注于不扩展该类型。这些字段应该是 c# 中的自动属性和其他语言中的 get/set 方法,但在这种情况下,这完全是多余的
M
Mark Brackett

仅仅因为我认为其他答案几乎与足球队是“是”List<FootballPlayer>还是“有”List<FootballPlayer>的切线相吻合,这实际上并没有按照书面形式回答这个问题。

OP 主要要求澄清从 List<T> 继承的准则:

一条准则说您不应该从 List 继承。为什么不?

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

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

公共 API 是您向 3rd 方程序员公开的接口。想想框架代码。请记住,所引用的指南是“.NET Framework 设计指南”而不是“.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 */
 }

但是,如果您已经公开暴露给第 3 方,那么您只是做了一个会导致编译和/或运行时错误的重大更改。

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


C
Cruncher

是否允许人们说

myTeam.subList(3, 5);

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


如果您将其称为 myTeam.subTeam(3, 5);,它可能会
@SamLeach 假设这是真的,那么您仍然需要组合而不是继承。因为 subList 甚至不再返回 Team
D
Disillusioned

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

您希望将所有规则、附加工作和内部细节封装在适当的对象中。这样,与此交互的其他对象就不必担心这一切。事实上,你想更进一步,主动阻止其他对象绕过这些内部结构。

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

假设您想通过了解球员是否退休、辞职或被解雇来区分球员何时离开。您可以实现一个采用适当输入枚举的 RemovePlayer 方法。但是,通过从 List 继承,您将无法阻止对 RemoveRemoveAll 甚至 Clear 的直接访问。结果,您实际上取消了您的 FootballTeam 类。

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

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

你是对的,这对于所有客户使用你的团队来说都是不必要的冗长。但是,与您将 List Players 暴露给所有人和杂项的事实相比,这个问题非常小,因此他们可以在未经您同意的情况下摆弄您的团队。

你接着说:

简单明了没有任何意义。足球队没有“拥有”球员名单。这是球员名单。您不会说“John McFootballer 已加入 SomeTeam 的球员”。您说“John 已加入 SomeTeam”。

第一点你错了:去掉“名单”这个词,很明显一支球队确实有球员。
但是,你用第二个字打中了头。您不希望客户调用 ateam.Players.Add(...)。您确实希望他们呼叫 ateam.AddPlayer(...)。您的实现将(可能除其他外)在内部调用 Players.Add(...)

希望您能看到封装对于赋予对象权力的目标是多么重要。你想让每个类都能很好地完成它的工作,而不用担心来自其他对象的干扰。


x
xpmatteo

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

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

封装。您的客户不需要知道 FootballTeam 内部发生了什么。对于您所有的客户都知道,它可以通过在数据库中查找玩家列表来实现。他们不需要知道,这可以改善您的设计。

简单明了没有任何意义。足球队没有“拥有”球员名单。这是球员名单。您不会说“John McFootballer 已加入 SomeTeam 的球员”。您说“John 已加入 SomeTeam”。您不会将字母添加到“字符串的字符”,而是将字母添加到字符串。您不会将一本书添加到图书馆的书籍中,而是将一本书添加到图书馆。

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


OP 想了解如何对现实进行建模,但随后将足球队定义为足球运动员列表(这在概念上是错误的)。这就是问题。在我看来,任何其他论点都会误导他。
我不同意球员名单在概念上是错误的。不必要。正如其他人在此页面中所写,“所有模型都是错误的,但有些是有用的”
正确的模型很难获得,一旦完成,它们就是有用的。如果一个模型可以被滥用,那么它在概念上是错误的。 stackoverflow.com/a/21706411/711061
A
ArturoTena

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

请记住,“所有模型都是错误的,但有些模型是有用的。” -George E. P. Box

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

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

-- 编辑

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

-- 第 2 版

我真的不记得我在“不要过度设计”评论中指的是什么。虽然我认为 KISS 思维方式是一个很好的指导,但我想强调的是,从 List 继承业务类会产生比它解决的问题更多的问题,因为 abstraction leakage

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

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


我最好的答案是......这取决于。从 List 继承会将此类的客户端暴露给可能不应该暴露的方法,主要是因为 FootballTeam 看起来像一个业务实体。我不知道我是否应该编辑这个答案或发布另一个,有什么想法吗?
关于“List 继承会将此类的客户端暴露给可能不应该暴露的方法”这一点正是使从 List 继承成为绝对禁忌的原因。一旦暴露出来,随着时间的推移,它们会逐渐被滥用和误用。现在通过“经济发展”节省时间很容易导致未来十倍的节省损失:调试滥用并最终重构以修复继承。 YAGNI 原则也可以理解为:你不需要 List 中的所有方法,所以不要暴露它们。
“不要过度设计。你写的代码越少,你需要调试的代码就越少。” <-- 我认为这是误导。继承的封装和组合不是过度工程,也不会导致更多的调试需求。通过封装,您限制了客户可以使用(和滥用)您的类的方式的数量,因此您需要测试和输入验证的入口点更少。从 List 继承,因为它快速简单,因此会导致更少的错误,这是完全错误的,它只是糟糕的设计,而糟糕的设计会导致比“过度工程”更多的错误。
@kai 我在每一点都同意你的观点。我真的不记得我在“不要过度设计”评论中指的是什么。 OTOH,我相信在少数情况下简单地从 List 继承是有用的。正如我在后续版本中所写,这取决于。每个案例的答案都深受知识、经验和个人偏好的影响。就像生活中的一切。 ;-)
P
Paul J Abernathy

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

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


Q
QuentinUK

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


回想起来,在 @EricLippert 和其他人的解释之后,您实际上已经为我的问题的 API 部分给出了很好的答案 - 除了您所说的之外,如果我这样做class FootballTeam : List<FootballPlayers>,我班的用户将能够告诉我通过使用反射从 List<T> 继承,看到对足球队没有意义的 List<T> 方法,并且能够在 List<T> 中使用 FootballTeam,所以我会向客户透露实施细节(不必要地)。
m
marsh-wiggle

序列化问题

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; }  
}

进一步阅读:When a class is inherited from List<>, XmlSerializer doesn't serialize other attributes

改用 IList

就我个人而言,我不会从 List 继承,而是实现 IList。 Visual Studio 将为您完成这项工作并创建一个完整的工作实施。看这里:How to get a full working implementation of IList


S
Shital Shah

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

我经常看到很多人只是因为 FxCop 规则这样说或者某人的博客说这是一种“坏”的做法而做了大量的额外工作。很多时候,这会将代码变成设计模式 palooza 的怪异之处。与许多指南一样,将其视为可能有例外的指南。


P
Peter Mortensen

我的肮脏秘密:我不在乎人们说什么,我做到了。 .NET Framework 使用“XxxxCollection”(我的头顶示例的 UIElementCollection)传播。

那么是什么阻止我说:

team.Players.ByName("Nicolas")

当我发现它比

team.ByName("Nicolas")

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

club.Players.ByName("Nicolas")

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

team.Players.ByName("Nicolas") 

或者

team.ByName("Nicolas")

真的。你有任何疑问吗?现在,您可能需要使用其他技术限制来阻止您在实际用例中使用 List。但是不要添加不应该存在的约束。如果微软没有记录原因,那么它肯定是一个“最佳实践”。


虽然在适当的时候有勇气和主动挑战公认的智慧很重要,但我认为在开始挑战之前先了解为什么公认的智慧首先被接受是明智的。 -1 因为“忽略该准则!”对于“为什么存在此指南?”不是一个好的答案。顺便说一句,社区并没有在这种情况下“责怪我”,而是提供了一个令人满意的解释,成功说服了我。
实际上,在不解释的情况下,你唯一的办法就是突破界限,测试不怕违规。现在。问问自己,即使 List 不是一个好主意。将继承从 List 更改为 Collection 会有多痛苦?一个猜测:无论使用重构工具的代码长度如何,都比在论坛上发帖要少。现在这是一个很好的问题,但不是一个实际的问题。
澄清一下:我当然不会等待 SO 的祝福来继承我想要的任何东西,但我的问题的重点是理解这个决定应该考虑的因素,以及为什么这么多有经验的程序员似乎有决定与我所做的相反。 Collection<T>List<T> 不同,因此在大型项目中可能需要进行大量工作才能进行验证。
team.ByName("Nicolas") 表示 "Nicolas" 是团队的名称。
“不要添加不应该存在的约束”相反,不要暴露任何你没有充分理由暴露的东西。这不是纯粹主义或盲目狂热,也不是最新的设计时尚潮流。它是基本的面向对象设计 101,初学者级别。为什么这个“规则”存在并不是什么秘密,它是几十年经验的结果。
N
Null511

虽然我没有像大多数这些答案那样进行复杂的比较,但我想分享我处理这种情况的方法。通过扩展 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
{
    ...
}

A
Alexey

我只想补充一点,埃菲尔的发明者和合同设计的伯特兰·迈耶 (Bertrand Meyer) 会从 List<Player> 继承 Team,而无需眨眼。

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

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


这里的问题不在于多重继承、mixins(等)或如何处理它们的缺失。在任何语言中,即使在人类语言中,团队也不是人员列表,而是由人员列表组成。这是一个抽象的概念。如果 Bertrand Meyer 或任何人通过继承 List 来管理团队是错误的。如果你想要一个更具体的列表,你应该继承一个列表。希望你同意。
这取决于继承对你来说是什么。就其本身而言,它是一种抽象操作,并不意味着两个类处于“是一种特殊的”关系,尽管它是主流的解释。但是,如果语言设计支持继承,则可以将继承视为实现重用的机制,即使组合是当今首选的替代方案。
I
Ivan Nikitin

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

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

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


E
Eniola

我想我不同意你的概括。团队不仅仅是球员的集合。一支球队有更多关于它的信息——名字、徽章、管理/行政人员的集合、教练组的集合,然后是球员的集合。所以正确地,你的 FootballTeam 类应该有 3 个集合,而不是本身就是一个集合;如果要正确建模现实世界。

您可以考虑一个 PlayerCollection 类,它像 Specialized StringCollection 一样提供一些其他功能 - 例如在将对象添加到内部存储或从内部存储中删除之前进行验证和检查。

也许,PlayerCollection 的概念更适合您的首选方法?

public class PlayerCollection : Collection<Player> 
{ 
}

然后 FootballTeam 看起来像这样:

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();
}

c
cdiggins

优先使用接口而不是类

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

继承打破封装

从类 breaks encapsulation 派生:

公开有关如何实现您的集合的内部细节

声明一个可能不合适的接口(一组公共函数和属性)

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

类是实现细节

类是一个实现细节,应该对代码的其他部分隐藏。简而言之,System.List 是抽象数据类型的特定实现,现在和将来可能合适,也可能不合适。

从概念上讲,System.List 数据类型称为“列表”这一事实有点牵强附会。 System.List<T> 是一个可变有序集合,支持用于添加、插入和删除元素的分期 O(1) 操作,以及用于检索元素数量或按索引获取和设置元素的 O(1) 操作。

接口越小代码越灵活

在设计数据结构时,接口越简单,代码就越灵活。只需看看 LINQ 的强大功能就可以证明这一点。

如何选择接口

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

一些可以帮助指导此过程的问题:

我需要计数吗?如果不考虑实现 IEnumerable

这个集合在初始化后会改变吗?如果不考虑 IReadonlyList

我可以按索引访问项目重要吗?考虑 ICollection

我将项目添加到集合中的顺序重要吗?也许它是一个 ISet

如果您确实想要这些东西,那么请继续实施 IList

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

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

关于避免样板的注意事项

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

也就是说,如果你发现你正在编写大量样板文件,那可能是因为你暴露了比你应该更多的功能。这与您不应该从类继承的原因相同。

您还可以设计对您的应用程序有意义的更小的接口,也许只需几个辅助扩展函数来将这些接口映射到您需要的任何其他接口。这是我在自己的 IArray 界面中为 LinqArray library 采用的方法。


创建接口将首先防止您从“错误”的类派生,因为您可以准确地看到您正在寻找的行为。那是我的第一个想法。
p
phoog

什么时候可以接受?

引用 Eric Lippert 的话:

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

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

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

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