为什么不从List <t>继承?</t>

时间:2014-02-11 03:01:36

标签: c# .net list oop inheritance

在计划我的课程时,我经常从一连串的想法开始:

  

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

var football_team = new List<FootballPlayer>();
     

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

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

那么我想:

  

好吧,足球队就像一个球员名单,但另外,它有一个名字(一个string)和一个总得分(int)。 .NET没有提供用于存储足球队的类,所以我将创建自己的类。最相似和相关的现有结构是List<FootballPlayer>,所以我将继承它:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public 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,我会遇到什么困难?

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

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

我还应该使用什么?

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

继承Collection比从List继承更多的工作,我认为没有任何好处。当然微软不会告诉我无缘无故地做额外的工作,所以我不禁觉得我在某种程度上误解了某些东西,而继承Collection实际上并不是解决我问题的正确方法。

我见过实施IList等建议。就是不行。这是几十行样板代码,它们什么也没有给我带来任何好处。

最后,有些人建议将List包装成某些内容:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

这有两个问题:

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

  2. 它很简单没有任何意义。足球队没有“拥有”球员名单。 玩家列表。你没有说“John McFootballer加入了SomeTeam的球员”。你说“约翰加入了SomeTeam”。您不要在“字符串的字符”中添加字母,而是在字符串中添加字母。您不会将图书添加到图书馆的图书中,而是将图书添加到图书馆。

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

    我的问题(总结)

    表示数据结构的正确C#方式是什么,“逻辑上”(也就是说,“对人类的思维”)只是list things的一些数据结构钟声和口哨?

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

28 个答案:

答案 0 :(得分:1364)

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

  

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

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

A football team is a particular kind of _____

任何人是否说“有一些花里胡哨的足球运动员名单”,还是他们都说“运动队”或“俱乐部”或“组织”?你认为足球队是特定类型的球员名单的观念仅在你的人类思想和你的人类思维中。

List<T>机制。 Football team是业务对象 - 即表示程序的业务域中的某个概念的对象。不要混合那些!足球队是一种队伍; 有一个名单,名单是一个玩家列表。名单不是特定类型的球员名单。名单一个玩家列表。因此,请创建一个名为Roster的属性List<Player>。除非你相信每个了解足球队的人都会从名单中删除球员,否则你可以在ReadOnlyList<Player>进行比赛。

  

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

谁不能接受?我?否。

  

什么时候可以接受?

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

  

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

我是否正在构建机制业务对象

  

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

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

更新

我更多地考虑了这一点,并且还有另一个理由不将足球队的模型列为球员名单。事实上,将足球队建模为拥有球员名单可能是一个坏主意。团队作为/拥有一个玩家列表的问题在于,您所拥有的是团队快照 。我不知道你的商业案例是什么,但是如果我有一个代表足球队的课程,我想问一些问题,比如“有多少海鹰队员因2003年至2013年的伤病错过了比赛?”或者“之前为另一支球队效力的丹佛球员在场上的同比增幅最大?”或“Did the Piggers go all the way this year?

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

答案 1 :(得分:273)

  

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

这是正确的方法。 “不必要罗嗦”是一种看待这个问题的坏方法。当你写my_team.Players.Count时,它有明确的含义。你想要算上球员。

my_team.Count

..没什么意思。数数呢?

团队不是一个列表 - 不仅仅是一个玩家列表。一个团队拥有球员,因此球员应该参与其中(成员)。

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

public int PlayerCount {
    get {
        return Players.Count;
    }
}

..变为:

my_team.PlayerCount

这现在有意义并且遵守The Law Of Demeter

您还应该考虑遵守Composite reuse principle。继承自List<T>,你说团队是一个的玩家列表,并暴露出不必要的方法。这是不正确的 - 正如你所说,一个团队不仅仅是一个球员名单:它有一个名字,经理,董事会成员,培训师,医疗人员,工资帽等。通过让你的团队成员包含一个球员名单,你“说”团队有一个玩家列表“,但它也可以有其他东西。

答案 2 :(得分:131)

哇,你的帖子有很多问题和要点。从微软获得的大多数推理都是恰到好处的。让我们从List<T>

的所有内容开始
  • List<T> 高度优化。它的主要用途是用作对象的私有成员。
  • Microsoft没有对其进行封锁,因为有时您可能希望创建一个具有更友好名称的类:class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }。现在它就像做var list = new MyList<int, string>();一样简单。
  • CA1002: Do not expose generic lists:基本上,即使您打算将此应用程序作为唯一的开发人员使用,也值得用良好的编码实践进行开发,因此它们会被灌输到您和第二天性。如果您需要任何消费者拥有索引列表,您仍然可以将列表公开为IList<T>。这让你可以在以后更改一个类中的实现。
  • Microsoft使Collection<T>非常通用,因为它是一个通用的概念......这个名字说明了一切;它只是一个集合。还有更精确的版本,例如SortedCollection<T>ObservableCollection<T>ReadOnlyCollection<T>等,每个版本都实现IList<T> List<T>
  • Collection<T>允许覆盖成员(即添加,删除等),因为它们是虚拟的。 List<T>没有。
  • 你问题的最后一部分是现货。足球队不仅仅是一个球员名单,所以它应该是一个包含球员名单的球员。想想Composition vs Inheritance。足球队球员名单(名单),不是球员名单。

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

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

答案 3 :(得分:124)

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

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

People playing football

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

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

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

答案 4 :(得分:100)

这是compositioninheritance的典型示例。

在这个具体案例中:

该团队是否具有添加行为的玩家列表

团队是否属于自己的对象,恰好包含了一系列玩家。

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

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

  2. 如果您想要列出其他内容,会发生什么。例如,团队有教练,经理,粉丝,设备等。其中一些可能就是他们自己的名单。

  3. 您限制了继承选项。例如,您可能想要创建一个通用的Team对象,然后使用继承自该对象的BaseballTeam,FootballTeam等。要从List继承,您需要从Team继承,但这意味着所有不同类型的团队都被迫拥有该名单的相同实现。

  4. 组合 - 包括在对象中提供所需行为的对象。

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

    两者都有它们的用途,但这是一个明显的情况,其中组成是可取的。

答案 5 :(得分:59)

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

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

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

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

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

  • 足球队是足球运动员名单吗? (列表的所有属性是否适用于具有相同含义的团队)
    • 团队是一个同质实体的集合吗?是的,团队是球员的集合
    • 是否包含了描述团队状态的玩家的顺序,团队是否确保序列被保留,除非明确更改?不,不,
    • 球员是否应根据球队的排名位置被列入/弃牌?否

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

此时我想说的是,在我看来,团队类甚至不应该使用List来实现;它应该在大多数情况下使用Set数据结构(例如HashSet)来实现。

答案 6 :(得分:46)

如果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在语义上有意义吗?只有当您的类型是<{1}}的集合时,才能执行此操作。

Something的情况下听起来不对。 FootballTeam超过Team。其他答案指出,Collection可以有教练,培训师等。

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

TeamCollection听起来像是一个玩家集合,如果你真的想这样做,它将是继承自FootballPlayerCollection的类的有效名称。

真的List<FootballPlayer>是一个非常好的类型。如果您从某个方法返回它,可能会List<FootballPlayer>

总结

问问自己

  1. IList<FootballPlayer>一个X?或 Y一个X

  2. 我的班级名称是什么意思吗?

答案 7 :(得分:33)

设计&gt;实施

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

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

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

  • 此对象在模型I&m;创建中包含哪些数据?
  • 此对象在该模型中表现出什么行为?
  • 将来如何改变?

请记住,继承意味着&#34; isa&#34; (是)一种关系,而作文暗示一个&#34;有一个&#34; (哈)关系。在您的视图中根据您的情况选择合适的一个,同时考虑到应用程序发展过程中可能出现的情况。

在考虑具体类型之前,请考虑在界面中进行思考,因为有些人发现更容易将他们的大脑放入&#34;设计模式&#34;那样。

这并不是每个人在日常编码中有意识地在这个级别上做的事情。但如果你正在考虑这类话题,你就会在设计领域踩踏。意识到这一点可以解放。

考虑设计细节

看一下List&lt; T&gt;和IList&lt; T&gt;在MSDN或Visual Studio上。了解他们公开的方法和属性。这些方法看起来都像是你想在视图中对FootballTeam做些什么吗?

footballTeam.Reverse()对你有意义吗? footballTeam.ConvertAll&lt; TOutput&gt;()看起来像你想要的东西?

这不是一个棘手的问题;答案可能真的是&#34;是&#34;。如果你实现/继承List&lt; Player&gt;或者IList&lt; Player&gt;,你会被他们困住;如果这对你的模特来说是理想的,那就去做吧。

如果您决定是,那么这是有道理的,并且您希望您的对象可以作为玩家的集合/列表(行为)进行处理,因此您希望通过各种方式实现ICollection或IList。名义上:

class FootballTeam : ... ICollection<Player>
{
    ...
}

如果您希望对象包含玩家(数据)的集合/列表,并且您希望集合或列表成为属性或成员,请务必执行此操作。名义上:

class FootballTeam ...
{
    public ICollection<Player> Players { get { ... } }
}

您可能会觉得您希望人们只能枚举一组玩家,而不是计算它们,添加它们或删除它们。 IEnumerable的&LT;播放&GT;是一个非常有效的选择。

您可能会觉得这些界面根本不适用于您的模型。这不太可能(IEnumerable&lt; T&gt;在许多情况下都很有用)但它仍然可能。

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

转到实施

一旦您决定了数据和行为,您就可以做出有关实施的决定。这包括您通过继承或组合依赖的具体类。

这可能不是一个很大的进步,人们经常会将设计和实施混为一谈,因为很可能会在一两秒内完成所有这些操作并开始输入。

思考实验

一个人为的例子:正如其他人所提到的,一个团队并不总是&#34;只是&#34;一系列球员。你是否为球队保留了一系列比赛分数?在你的模特中,球队是否可以与俱乐部互换?如果是这样,如果你的团队是一个球员集合,也许它也是一组员工和/或一组分数。然后你最终得到:

class FootballTeam : ... ICollection<Player>, 
                         ICollection<StaffMember>,
                         ICollection<Score>
{
    ....
}

尽管设计,但在C#中,您无法通过继承List&lt; T&gt;来实现所有这些功能。无论如何,因为C#&#34;只有&#34;支持单继承。 (如果您已经在C ++中尝试过这个malarky,您可能会认为这是一件好事。)通过继承实现一个集合,一个通过组合实现可能感觉脏。除非你明确地实现ILIst&lt; Player&gt; .Count和IList&lt; StaffMember&gt; .Count等,否则像Count这样的属性会让用户感到困惑,然后他们只是痛苦而不是混淆。你可以看到它的发展方向;在思考这条道路时的直觉可能会告诉你朝这个方向前进是错误的(无论是对还是错,你的同事也可能以这种方式实施!)

简答(太迟)

关于不从集合类继承的指南不是C#特定的,您可以在许多编程语言中找到它。它是智慧而不是法律。一个原因是,在实践中,组合被认为通常在可理解性,可实现性和可维护性方面胜过继承。现实世界/域名对象更常见的是找到有用且一致的&#34; hasa&#34;关系比有用和一致&#34; isa&#34;关系,除非你深入到摘要中,尤其是随着时间的推移,代码中对象的精确数据和行为发生变化。这不应该导致您总是排除从集合类继承;但它可能具有启发性。

答案 8 :(得分:31)

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

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

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

答案 9 :(得分:26)

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

这在逻辑上是错误的:

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

这是正确的:

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

答案 10 :(得分:23)

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

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

string team = new string();

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

为什么我不能只扩展字符串类型以便它还包含一个播放器列表?

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

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

答案 11 :(得分:22)

取决于上下文

当你将你的球队视为球员名单时,你就会将足球队的“想法”投射到一个方面:你将“球队”减少到你在场上看到的人。此投影仅在某些上下文中是正确的。在不同的背景下,这可能是完全错误的。想象一下,你想成为团队的赞助商。所以你必须和团队的经理谈谈。在此背景下,团队将被列入其经理名单。而这两个列表通常不会重叠很多。其他情况是当前与前任球员等等。

语义不清楚

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

类是可扩展的

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

类更复杂

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

结论/注意事项

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

是完全可以的
IList<Player> footballTeam = ...

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

type FootballTeam = IList<Player>

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

答案 12 :(得分:18)

仅仅因为我认为其他答案几乎与足球队是否是一个很好的相关。 List<FootballPlayer>或&#34;有一个&#34; List<FootballPlayer>,它真的没有按照书面回答这个问题。

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

  

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

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

  

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

公共API是您向第三方程序员公开的界面。想想框架代码。并且回想一下,所引用的指南是&#34; .NET 框架设计指南&#34;而不是&#34; .NET 应用程序设计指南&#34;。有一点不同,而且 - 一般来说 - 公共API设计要严格得多。

  

如果我当前的项目没有并且不太可能拥有此公共API,我可以放心地忽略此指南吗?如果我继承List,结果我需要一个公共API,我会遇到什么困难?

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

如果您以后添加公共API,则需要从实施中抽象出您的API(不要直接暴露您的List<T>)或违反指南,以及将来可能带来的痛苦。< / p>

  

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

取决于上下文,但由于我们使用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 */
 }

但是,如果您公开地向第三方公开,那么您只是进行了一次重大更改,这将导致编译和/或运行时错误。

TL; DR - 指南适用于公共API。对于私有API,请执行您想要的操作。

答案 13 :(得分:16)

允许人们说

myTeam.subList(3, 5);

有意义吗?如果没有,则它不应该是列表。

答案 14 :(得分:15)

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

  

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

封装。您的客户无需知道FootballTeam内部的情况。对于所有客户都知道,可以通过查看数据库中的播放器列表来实现。他们不需要知道,这可以改善您的设计。

  

它只是简单没有任何意义。足球队没有“拥有”球员名单。这是球员名单。你没有说“John McFootballer加入了SomeTeam的球员”。你说“约翰加入了SomeTeam”。您不要在“字符串的字符”中添加字母,而是在字符串中添加字母。您不会将图书添加到图书馆的图书中,而是将图书添加到图书馆。

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

答案 15 :(得分:13)

这里有很多优秀的答案,但我想触及一些我没有看到过的东西:面向对象的设计是关于 授权对象

您希望将所有规则,其他工作和内部详细信息封装在适当的对象中。通过这种方式,与此对象交互的其他对象不必担心这一切。实际上,您希望更进一步,主动阻止其他对象绕过这些内部。

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

假设你想通过知道退役,辞职或被解雇来区分玩家离开的时间。您可以实现一个RemovePlayer方法,该方法采用适当的输入枚举。但是,通过继承List,您将无法阻止对RemoveRemoveAll甚至Clear的直接访问。因此,您实际上已经 取消了 您的FootballTeam课程。


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

  

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

你是对的,对于所有使用你团队的客户来说,这将是不必要的冗长。但是,与您向所有人展示List Players这一事实相比,这个问题非常小,因此他们可能会在未经您同意的情况下与您的团队进行斗争。

你继续说:

  

它只是简单没有任何意义。足球队没有“拥有”球员名单。这是球员名单。你没有说“John McFootballer加入了SomeTeam的球员”。你说“约翰加入了SomeTeam”。

第一位你错了:放下'list'这个词,显然团队确实有玩家。 然而,你用第二个击中头部的钉子。您不希望客户呼叫ateam.Players.Add(...)。你确实希望他们打电话给ateam.AddPlayer(...)。您的实施(可能还有其他内容)会在内部调用Players.Add(...)


希望你能看到封装对赋予对象权力的重要性。您希望允许每个班级完成其工作,而不必担心其他对象的干扰。

答案 16 :(得分:12)

  

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

记住,“所有型号都错了,但有些型号很有用。” - George E. P. Box

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

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

- 已编辑

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

- 第2版

我真诚地不记得我所说的“不要过度工程”评论。虽然我认为KISS心态是一个很好的指导,但我想强调的是,从List继承业务类会产生比解决更多的问题,因为abstraction leakage

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

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

答案 17 :(得分:10)

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

你做了什么?与许多事情一样......这取决于具体情况。我将使用的指南是,为了从另一个类继承,真正应该是“是一种”关系。所以如果你写一个名为宝马的课程,它可以继承汽车,因为宝马真的是一辆汽车。马类可以继承哺乳动物类,因为马实际上是现实生活中的哺乳动物,任何哺乳动物的功能都应该与马相关。但是你能说一个团队是一个名单吗?从我所知道的情况来看,这似乎不是一个团队真的“是一个”列表。所以在这种情况下,我会将List作为成员变量。

答案 18 :(得分:9)

我的肮脏秘密:我不在乎别人说什么,我也这么做。 .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&lt; T&gt;的其他技术限制。在你的真实用例中。但是不要添加不应存在的约束。如果微软没有记录原因,那么肯定是一个“最佳实践”来自无处。

答案 19 :(得分:8)

指南所说的是公共API不应该公开内部设计决定,无论是使用列表,集合,字典,树还是其他什么。 “团队”不一定是列表。您可以将其实现为列表,但您的公共API的用户应该根据需要了解您的类。这允许您更改决策并使用不同的数据结构,而不会影响公共接口。

答案 20 :(得分:6)

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

我经常看到很多人只是因为FxCop规则这么说或者有人的博客说这是一个“坏”的做法而做了很多额外的工作。很多时候,这会将代码转换为设计模式palooza怪异。与许多指南一样,将其视为可以有例外的指导原则。

答案 21 :(得分:4)

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

答案 22 :(得分:3)

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

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

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

答案 23 :(得分:3)

我只是想补充一点,埃菲尔的发明者Bertrand Meyer和合同设计,Team继承List<Player>,而不是打击眼睑。

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

然而,C#不是埃菲尔。后者支持多重继承和重命名功能。在C#中,当您继承子类时,您继承了接口和实现。您可以覆盖实现,但直接从超类复制调用约定。但是,在Eiffel中,您可以修改公共方法的名称,这样您就可以在Add中将RemoveHire重命名为FireTeam。如果将Team的实例向上转发回List<Player>,则调用者将使用AddRemove对其进行修改,但您的虚拟方法Hire和{{ 1}}将被调用。

答案 24 :(得分:0)

我想我不同意你的概括。团队不仅仅是一个球员集合。一个团队有更多关于它的信息 - 名称,标志,管理/管理人员的集合,教练组的收集,然后收集球员。如此正确,你的FootballTeam类应该有3个集合,而不是它自己的集合;如果要正确地模拟现实世界。

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

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

答案 25 :(得分:0)

首选接口优先于类

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

继承破坏了封装

源自类breaks encapsulation

  • 公开有关如何实现您的收藏的内部细节
  • 声明了不合适的接口(一组公共功能和属性)

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

类是实现细节

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

从概念上讲,System.List数据类型被称为“列表”的事实有点让人讨厌。 System.List<T>是一种可变的有序集合,它支持用于添加,插入和删除元素的摊销O(1)操作,以及用于检索元素数量或按索引获取和设置元素的O(1)操作。

界面越小,代码越灵活

在设计数据结构时,接口越简单,代码越灵活。只要看看LINQ对此功能的演示有多强大。

如何选择接口

当您认为“列出”时,您应该对自己说:“我需要代表一群棒球运动员”。因此,假设您决定使用一个类对此进行建模。您首先应该确定此类需要暴露的最小接口数量。

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

  • 我需要计数吗?如果不考虑实施IEnumerable<T>
  • 此集合在初始化后是否会更改?如果不考虑,请使用IReadonlyList<T>
  • 按索引访问项目是否重要?考虑ICollection<T>
  • 将项目添加到集合中的顺序重要吗?也许是ISet<T>
  • 如果您确实想要这些东西,请继续执行IList<T>

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

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

关于避免样板的注意事项

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

也就是说,如果您发现要编写大量样板文件,则可能是因为您要暴露的功能要多得多。这是您不应从类继承的相同原因。

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

答案 26 :(得分:0)

缺少一个方面。使用XmlSerializer无法正确继承从List <> 继承的类。在这种情况下,必须使用DataContractSerializer或自己的序列化实现。

public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized:
    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

答案 27 :(得分:0)

什么时候可以接受?

引用埃里克·利珀特的话:

在构建扩展<{>的List<T>机制的机制时。

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

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

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