c#:为什么不继承List?

当我计划我的计划时,我经常以这样的思路开始:

A football team is just a list of football players. Therefore, I should represent it with:

1
var football_team = new List<FootballPlayer>();

The ordering of this list represent the order in which the players are listed in the roster.

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

所以我想:

Okay, a football team is just like a list of players, but additionally, it has a name (a string) and a running total of scores (an int). .NET does not provide a class for storing football teams, so I will make my own class. The most similar and relevant existing structure is List, so I will inherit from it:

1
2
3
4
5
class FootballTeam : List<FootballPlayer>
{
    public string TeamName;
    public int RunningTotal
}

但是事实证明,一个指导方针说您不应该继承List。我完全被这条指导方针搞糊涂了。

为什么不呢?

显然List在某种程度上优化了性能。所以如何?如果我扩展List会导致什么性能问题?到底什么会断裂?

我看到的另一个原因是,List是由Microsoft提供的,而我对它没有控制权,所以在公开一个"公共API"之后,我无法更改它。但我很难理解这一点。什么是公共API ?我为什么要关心它?如果我当前的项目没有也不太可能有这个公共API,我可以安全地忽略这个指南吗?如果我从List继承了一个公共API,那么我将会遇到什么困难呢?

为什么这很重要?列表就是列表。有什么可能改变?我可能想要改变什么?

最后,如果微软不想让我继承List,他们为什么不创建类sealed?

我还应该用什么?

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

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

我看到了一些建议,比如实现IList。只是没有。这是几十行样板代码,没有给我带来任何好处。

最后,有人建议将List包入以下内容:

1
2
3
4
class FootballTeam
{
    public List<FootballPlayer> Players;
}

这有两个问题:

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

它就是没有任何意义。足球队没有球员名单。它是玩家列表。你不会说"John mcfootball已经加入了某个球队的球员"。你说"John加入了某个团队"。你不是在"字符串的字符"中添加一个字母,而是在字符串中添加一个字母。你不是把一本书加到图书馆的书里,而是把一本书加到图书馆里。

我意识到,"引线下"发生的事情可以说是"将X添加到Y的内部列表中",但这似乎是一种非常反直觉的世界观。

我的问题(总结)

正确的c#表示数据结构的方法是什么,"逻辑上"(也就是说,"对人类的大脑")只是listthings加上一些花哨的东西?

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


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

What is the correct C# way of representing a data structure, which,"logically" (that is to say,"to the human mind") is just a list of things with a few bells and whistles?

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

1
A football team is a particular kind of _____

有没有人说过"一些花哨的足球运动员名单",或者他们都说过"运动队"、"俱乐部"或"组织"?你认为一支足球队是一种特殊的球员名单,这种想法存在于你的人类思维中,而且只存在于你的人类思维中。

List是一种机制。足球队是一个业务对象——也就是说,一个对象表示程序业务领域中的某个概念。不要把这些!足球队是一种球队;它有一个花名册,花名册是球员的名单。花名册并不是一种特殊的球员名单。花名册是球员的名单。因此,创建一个名为Roster的属性,它是一个List。如果你不相信任何一个了解足球队的人都有权把球员从名册上删除,那就趁你还在做这件事的时候去做吧。

Is inheriting from List always unacceptable?

无法接受谁?我吗?不。

When is it acceptable?

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

What must a programmer consider, when deciding whether to inherit from List or not?

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

But that's a lot of code! What do I get for all that work?

您花了更多的时间输入您的问题,这将需要您为List的相关成员编写50次转发方法。很明显,您并不害怕冗长,我们这里讨论的是非常少量的代码;这是几分钟的工作。

更新

我对此进行了更多的思考,还有一个理由不把足球队作为球员列表来建模。事实上,用球员名单来建立足球队的模型可能是个坏主意。一个球队拥有一份球员名单的问题在于,你所拥有的只是球队在某一时刻的快照。我不知道你的业务案例是对于这门课,但是如果我有一个类代表一个足球队我想问这样的问题"多少海鹰队球员因伤错过了游戏在2003年和2013年之间?"和"什么丹佛球员以前打了另一个团队的最大码了同比增长?"或"今年Piggers一路吗?"

也就是说,在我看来,一个足球队似乎是一个历史事实的集合,比如球员何时被招募、何时受伤、何时退役等等。很明显,当前的球员名单是一个重要的事实,应该放在最前面和最中间,但是你可能想对这个对象做一些其他有趣的事情,需要一个更历史的视角。


Lastly, some suggest wrapping the List in something:

这是正确的方法。用"不必要的啰嗦"来看待这个问题是不好的。当您编写my_team.Players.Count时,它有一个显式的含义。你要计算玩家的数量。

1
my_team.Count

. .没有任何意义。计算什么?

一个团队不是一个列表——它不仅仅由一组球员组成。一个球队拥有球员,所以球员应该是其中的一员。

如果你真的担心它过于冗长,你可以从团队中公开属性:

1
2
3
4
5
public int PlayerCount {
    get {
        return Players.Count;
    }
}

. .这就变成:

1
my_team.PlayerCount

这句话现在有了意义,也符合得墨忒耳的律法。

您还应该考虑遵循复合重用原则。通过继承List,您可以说一个团队是一组玩家,并从中暴露出不必要的方法。这是不正确的——正如你所说,一支球队不仅仅是一份球员名单:它有名字、经理、董事会成员、教练、医务人员、工资上限等等。通过让您的team类包含一个球员列表,您是在说"一个球队有一个球员列表",但是它也可以有其他的东西。


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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FootballTeam
{
    // 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.
}


1
2
3
4
5
class FootballTeam : List<FootballPlayer>
{
    public string TeamName;
    public int RunningTotal;
}

以前的代码的意思是:一群在街上踢足球的家伙,他们碰巧有个名字。喜欢的东西:

People playing football

不管怎样,这个代码(来自m-y的答案)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FootballTeam
{
    // 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.
}

意思是:这是一支有管理人员、球员、管理员等的足球队。喜欢的东西:

Image showing members (and other attributes) of the Manchester United team

这就是你的逻辑是如何在图片中呈现的……


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

在这种特殊情况下:

球队是一组有附加行为的球员吗

团队是它自己的对象,碰巧包含一个球员列表。

通过扩展列表,你可以在以下几个方面限制自己:

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

如果你还想要其他东西的列表会发生什么。例如,球队有教练、经理、球迷、装备等。其中一些很可能是列表本身。

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

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

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

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


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

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

一个孩子所需要的特征,不应少于完全定义了父母的特征。除了完全定义子元素之外,父元素不应该要求任何特征。

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

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

足球队是足球运动员的名单吗?(列表的所有属性是否都适用于具有相同含义的团队)团队是同质实体的集合吗?是的,团队是一个球员的集合包含球员的顺序是否描述了球队的状态?球队是否确保保留顺序,除非明确更改?没有,没有球员是否会根据他们在球队中的顺序位置而被列入/淘汰?没有

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

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


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

1
2
3
4
5
class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
}

你会怎么建模呢?

1
2
3
4
5
class FootballTeam : List<FootballPlayer>
{
    public string TeamName;
    public int RunningTotal
}

关系显然是有a而不是a。

RetiredPlayers ?

1
2
3
4
5
6
class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
    List<FootballPlayer> RetiredPlayers { get; set; }
}

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

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

FootballTeam的情况下,它听起来并不正确。一个Team不仅仅是一个Collection。正如其他答案所指出的,一个亚南人可以有教练、教练员等。

FootballCollection听起来像是一堆足球,或者可能是一堆足球用具。TeamCollection,团队集合。

FootballPlayerCollection听起来像一个播放器集合,如果您真的想要这样做,它将是继承自List的类的有效名称。

真的List是一个非常好的处理类型。如果你从一个方法返回它,可能是IList

总之

问问你自己

XY吗?或者X a Y有吗?

我的类名是什么意思?


设计>

实施

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

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

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

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

请记住,继承意味着"isa"(is)关系,而组合意味着"has"(hasa)关系。根据您的视图中的情况选择正确的视图,要记住随着应用程序的发展,事情可能会发展到什么程度。

在考虑具体类型之前,先考虑接口的思考,因为有些人发现这样更容易让他们的大脑进入"设计模式"。

这并不是每个人在日常编码中都会有意识地做的事情。但是如果你正在考虑这类话题,你就在设计的水域中涉猎。意识到这一点是一种解放。

考虑设计细节

查看MSDN或Visual Studio上的List和IList。查看它们公开了哪些方法和属性。在你看来,这些方法都像是有人想对一支足球队做的事情吗?

reverse()对您有意义吗?足球队. convertall ()看起来像您想要的东西吗?

这不是一个刁钻的问题;答案可能是肯定的。如果你实现/继承List或IList,你就被它们卡住了;如果这对你的模型来说是理想的,那就去做。

如果您决定yes,那么这是有意义的,并且您希望您的对象可以作为一个集合/玩家列表(行为)来处理,因此您希望实现ICollection或IList,无论如何都要这样做。名义上的:

1
2
3
4
class FootballTeam : ... ICollection<Player>
{
    ...
}

如果您想要您的对象包含一个集合/玩家列表(数据),并且因此您想要集合或列表成为一个属性或成员,那么无论如何都要这样做。名义上的:

1
2
3
4
class FootballTeam ...
{
    public ICollection<Player> Players { get { ... } }
}

您可能希望人们能够只枚举一组玩家,而不是计数、添加或删除它们。IEnumerable是一个非常有效的选项。

您可能觉得这些接口在您的模型中根本没有任何用处。这是不太可能的(IEnumerable在很多情况下都很有用),但仍然是可能的。

任何试图告诉你其中之一在任何情况下都是绝对错误的人都是被误导的。任何试图在任何情况下都明确无误地告诉你它是正确的人都是被误导的。

转到实现

一旦确定了数据和行为,就可以对实现做出决策。这包括通过继承或组合所依赖的具体类。

这可能不是一个大的步骤,而且人们经常把设计和实现混为一谈,因为很有可能在一两秒钟内在您的脑海中浏览完所有内容并开始输入。

一个思维实验

一个人为的例子:正如其他人所提到的,一个团队并不总是"仅仅"由一群球员组成。你是否保存球队的比赛成绩?在你的模型中,球队和俱乐部是可以互换的吗?如果是这样,如果你的球队是一个球员的集合,也许它也是一个工作人员和/或分数的集合。最后得到:

1
2
3
4
5
6
class FootballTeam : ... ICollection<Player>,
                         ICollection<StaffMember>,
                         ICollection<Score>
{
    ....
}

尽管进行了设计,但是在c#中,无论如何都不能通过从List继承来实现所有这些,因为c#"只"支持单继承。(如果您在c++中尝试过这种胡言乱语,您可能会认为这是一件好事。)通过继承实现一个集合和通过组合实现一个集合可能会让人感觉很脏。除非实现ILIst,否则Count等属性会让用户感到困惑。计数和IList <职员>。明确地数数等等,然后他们只是痛苦而不是困惑。你可以看到它的走向;当你沿着这条路思考时,你的直觉很可能会告诉你,朝这个方向走是不对的(无论对错,如果你这样做,你的同事也可能会这么想)。

简短的答案(太晚了)

关于不从集合类继承的指导原则并不特定于c#,您会在许多编程语言中发现它。它是公认的智慧,而不是法律。一个原因是,在实践中,在可理解性、可实现性和可维护性方面,组合常常被认为胜过了继承。在现实世界/域对象中,找到有用且一致的"hasa"关系比找到有用且一致的"isa"关系更常见,除非您非常抽象,尤其是随着时间的推移,以及对象在代码更改中的精确数据和行为。这不应该导致您总是排除从集合类继承;但这可能是一种暗示。


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

如果您想使用LINQ,那么实现一个通用接口(如ICollectionIEnumerable)将更有意义。

如上所述,写作是正确的方法。只需将播放器列表实现为私有变量。


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

这在逻辑上是错误的:

1
2
3
4
5
class FootballTeam : List<FootballPlayer>
{
    public string TeamName;
    public int RunningTotal
}

这是正确的:

1
2
3
4
5
6
class FootballTeam
{
    public List<FootballPlayer> players
    public string TeamName;
    public int RunningTotal
}


我把你的问题重写一下。所以你可以从不同的角度来看待这个问题。

当我需要代表一支足球队时,我明白它基本上就是一个名字。如:"老鹰"

1
string team = new string();

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

为什么我不能扩展字符串类型,让它也包含一个播放器列表呢?

你对这个问题的看法是武断的。试着去思考一个团队拥有什么(属性),而不是它是什么。

这样做之后,您可以看到它是否与其他类共享属性。想想遗传。


它取决于上下文

当你把你的球队看作一个球员列表时,你是在把一个足球队的"想法"投射到一个方面:你把"球队"缩小到你在场上看到的人。这种推测只有在特定的背景下才是正确的。在另一种情况下,这可能是完全错误的。假设你想成为团队的赞助者。所以你必须和团队的经理谈谈。在这种情况下,团队被投射到它的经理列表中。这两个列表通常不会重叠太多。其他上下文是当前和以前的玩家,等等。

不清楚语义

因此,将一个团队视为其参与者列表的问题在于,它的语义依赖于上下文,并且不能在上下文更改时扩展它。此外,很难表达您使用的上下文。

类是扩展的

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

类更复杂

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

<

结论/考虑/ hh1 >

当你有一个非常具体的背景时,你可以把一个团队看作一个球员列表。例如,在一个方法中完全可以编写

1
IList<Player> footballTeam = ...

当使用f#时,甚至可以创建一个类型缩写

1
type FootballTeam = IList<Player>

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


仅仅因为我认为其他的答案与足球队是-a List还是-a (has-a) List有很大的出入,而这两个答案并没有像上面写的那样回答这个问题。

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

A guideline says that you shouldn't inherit from List. Why not?

因为List没有虚方法。在您自己的代码中,这不是什么问题,因为您通常可以相对轻松地切换实现—但是在公共API中,这可能是一个更大的问题。

What is a public API and why should I care?

公共API是您向第三方程序员公开的接口。认为框架代码。请记住,所引用的指南是"。NET框架设计指南"而非"。NET应用程序设计指南。这是有区别的,而且——一般来说——公共API设计要严格得多。

If my current project does not and is not likely to ever have this public API, can I safely ignore this guideline? If I do inherit from List and it turns out I need a public API, what difficulties will I have?

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

如果您将来添加一个公共API,您将需要从实现中抽象出您的API(通过不直接公开您的List),或者违反准则,这可能会导致将来的痛苦。

Why does it even matter? A list is a list. What could possibly change? What could I possibly want to change?

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

1
2
3
4
5
6
7
 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来修复任何编译错误:

1
2
3
4
5
6
7
8
9
10
 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 */
 }

但是,如果您公开地向第三方公开,那么您只是做了一个破坏性的更改,这会导致编译和/或运行时错误。

DR -本指南是为公共api编写的。对于私有api,做您想做的。


允许人们说

1
myTeam.subList(3, 5);

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


这取决于您的"团队"对象的行为。如果它的行为就像一个集合,那么可以先用一个普通列表表示它。然后,您可能开始注意到您一直在重复在列表中迭代的代码;此时,您可以选择创建一个封装球员列表的FootballTeam对象。FootballTeam类成为在球员列表中迭代的所有代码的home。

It makes my code needlessly verbose. I must now call my_team.Players.Count instead of just my_team.Count. Thankfully, with C# I can define indexers to make indexing transparent, and forward all the methods of the internal List... But that's a lot of code! What do I get for all that work?

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

It just plain doesn't make any sense. A football team doesn't"have" a list of players. It is the list of players. You don't say"John McFootballer has joined SomeTeam's players". You say"John has joined SomeTeam". You don't add a letter to"a string's characters", you add a letter to a string. You don't add a book to a library's books, you add a book to a library.

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


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

您希望将所有规则、附加工作和内部细节封装到适当的对象中。这样,与这个对象交互的其他对象就不必担心所有的问题。实际上,您希望更进一步,积极地防止其他对象绕过这些内部构件。

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

假设您想通过了解球员是退役、辞职还是被解雇来区分他们何时离开。您可以实现一个RemovePlayer方法,该方法接受一个适当的输入enum。但是,通过继承List,您将无法阻止对RemoveRemoveAll甚至Clear的直接访问。结果,您实际上取消了FootballTeam类的权限。

关于封装的其他想法…你提出了以下关切:

It makes my code needlessly verbose. I must now call my_team.Players.Count instead of just my_team.Count.

您是对的,这对于使用您的团队的所有客户来说是不必要的冗长。然而,与您将List Players暴露给各种各样的人以便他们可以在未经您同意的情况下篡改您的团队相比,这个问题是非常小的。

你接着说:

It just plain doesn't make any sense. A football team doesn't"have" a list of players. It is the list of players. You don't say"John McFootballer has joined SomeTeam's players". You say"John has joined SomeTeam".

你在第一点上就错了:去掉"list"这个词,很明显一个团队确实有球员。然而,你用第二颗击中了要害。您不希望客户端调用ateam.Players.Add(...)。您确实希望他们调用ateam.AddPlayer(...)。您的实现将(可能包括其他事情)在内部调用Players.Add(...)

希望您能看到封装对于增强对象的功能是多么重要。您希望允许每个类都能很好地完成自己的工作,而不用担心受到其他对象的干扰。


What is the correct C# way of representing a data structure...

记住,"所有的模型都是错误的,但有些是有用的。"-乔治·e·p·博克斯

没有"正确的方法",只有有用的方法。

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

——编辑

我最好的回答是……视情况而定。从列表继承会将该类的客户端暴露给可能不应该暴露的方法,主要是因为足球队看起来像一个业务实体。

——版2

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

另一方面,我相信从列表中继承有用的例子是有限的。正如我在前一版中所写的,这要视情况而定。每个案例的答案都深受知识、经验和个人偏好的影响。

感谢@kai帮助我更精确地思考这个问题的答案。


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

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


我的秘密:我不在乎别人怎么说,我就是这么做的。

所以什么阻止了我说:

1
team.Players.ByName("Nicolas")

当我发现它比

1
team.ByName("Nicolas")

此外,我的PlayerCollection可能被其他类使用,比如"Club",没有任何代码重复。

1
club.Players.ByName("Nicolas")

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

1
team.Players.ByName("Nicolas")

1
team.ByName("Nicolas")

真的。你有什么疑问吗?现在,您可能需要使用其他技术约束来阻止您在实际用例中使用List。但是不要添加不应该存在的约束。如果微软没有说明原因,那么毫无疑问,这是一种"最佳实践"。


指导方针说的是,公共API不应该显示您是否使用列表、集合、字典、树或其他内容的内部设计决策。一个"团队"不一定是一个列表。您可以将其实现为列表,但是您的公共API的用户应该根据需要使用您的类。这允许您更改您的决策,并在不影响公共接口的情况下使用不同的数据结构。


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

我经常看到很多人做大量的额外工作,仅仅是因为FxCop规则这么说,或者某人的博客说这是一种"坏"的做法。很多时候,这将代码转换成设计模式的怪异之处。与许多指导原则一样,将其视为可能存在异常的指导原则。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

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

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

    ...
}

class Player
{
    ...
}

如果您的类用户需要** List拥有的所有方法和属性,那么您应该从中派生类。如果他们不需要这些方法,请附上列表,并为类用户实际需要的方法创建包装器。

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

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


我只是想补充一点,埃菲尔铁塔和合同设计的发明者贝特朗·迈耶(Bertrand Meyer)将会毫不犹豫地继承List的遗产。

在他的著作《面向对象软件构造》中,他讨论了GUI系统的实现,其中矩形窗口可以有子窗口。他只是从RectangleTree继承Window来重用实现。

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


更喜欢接口而不是类

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

继承了封装

派生自类中断封装:

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

这使得重构代码变得更加困难。

类是一个实现细节

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

从概念上讲,将System.List数据类型称为"list"有点分散注意力。System.List是一个可变有序集合,支持用于添加、插入和删除元素的平摊O(1)操作,以及用于检索元素数量或通过索引获取和设置元素的O(1)操作。

接口越小,代码越灵活

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

如何选择接口

当你认为"列表"时,你应该先对自己说,"我需要代表一群棒球运动员"。假设你决定用一个类来建模。首先应该确定该类需要公开的接口的最小数量。

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

我需要点票吗?如果不考虑实现IEnumerable这个集合初始化后会改变吗?如果不考虑IReadonlyList。我可以通过索引访问项目,这很重要吗?考虑ICollection我向集合中添加项目的顺序重要吗?也许它是一个ISet?如果您确实想要这些东西,那么继续执行IList

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

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

关于避免样板文件的说明

在现代IDE中实现接口应该很容易。右键选择"实现界面"。然后,如果需要,将所有实现转发给一个成员类。

也就是说,如果您发现您正在编写大量的样板文件,这可能是因为您公开了比您应该公开的更多的函数。这也是不应该从类中继承的原因。

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


我不同意你的概括。一个团队不仅仅是一群球员。一个球队有太多关于它的信息——名字,徽章,管理/行政人员的收集,教练组的收集,然后是球员的收集。所以正确地说,你的足球队班级应该有3个集合而不是一个集合;如果要正确地模拟现实世界。

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

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

1
2
3
public class PlayerCollection : Collection<Player>
{
}

然后足球队可以是这样的:

1
2
3
4
5
6
7
8
9
10
11
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();
}