提问



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



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


var football_team = new List<FootballPlayer>();

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



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


那么我认为:



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


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



但事实证明,指南说你不应该继承List<T>。我在两个方面完全被本指南所困惑。


为什么不呢?



显然List以某种方式针对性能进行了优化。怎么会这样?如果我延长List,我将会遇到什么性能问题?究竟会打破什么?


我见过的另一个原因是List是由微软提供的,而我无法控制它,所以在暴露出公共API后我无法在以后更改它。但我很难理解这一点。一个公共API,我为什么要关心?如果我当前的项目没有,也不可能有这个公共API,我可以安全地忽略这个指南吗?如果我继承了List 和事实证明我需要一个公共API,我会遇到什么困难?


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


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


我还应该使用什么?



显然,对于自定义集合,Microsoft提供了Collection类,应该扩展而不是List。但是这个类非常简单,没有很多有用的东西,比如AddRange。 jvitor83的回答提供了该特定方法的性能原理,但是如何缓慢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。你不要在字符串的字符中添加一个字母,你可以在字符串中添加一个字母。你不会在图书馆的书中添加一本书,而是将图书添加到图书馆。



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


我的问题(摘要)



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


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

最佳参考


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



  表示数据结构的正确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年的伤病错过了比赛?或者之前为另一支球队效力的丹佛球员在场上的同比增幅最大?或者今年小猪队一路走了吗? [162]


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

其它参考1



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



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


my_team.Count


..什么也没有。算什么?


一个团队不是一个列表 - 不仅包括一个球员列表。一个团队拥有球员,所以球员应该是球员的一部分(一个成员)。


如果你真的担心它过于冗长,你可以随时暴露团队中的属性:


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


..which变成:


my_team.PlayerCount


这有意义,并遵守得墨忒耳法。[163]


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

其它参考2


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



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

  • Microsoft没有对其进行封锁,因为有时您可能想要创建一个具有更友好名称的类: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>没有。

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






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


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


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


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





无论如何,这段代码(来自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.
}


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





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

其它参考4


这是构成与继承的典型例子。[167] [168]


在这个特定情况下:


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


要么


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


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



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

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

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



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


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


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

其它参考5


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



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



  • 一个子不应该要求完全定义父的特征。

  • 除了完全定义Child之外,Parent不应该要求任何特征。



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


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



  • 足球队是足球运动员名单吗? (列表的所有属性是否适用于具有相同含义的团队)


    • 团队是一个同质实体的集合吗?是的,团队是球员的集合

    • 是否包含了描述团队状态的玩家的顺序,团队是否确保序列被保留,除非明确更改?不,不,

    • 球员是否应根据球队的排名位置被列入/弃牌?否




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


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

其它参考6


如果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的情况下,它听起来不对.A Team不仅仅是CollectionTeam可以有教练,培训师等,因为其他答案有指出。


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


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


真的List<FootballPlayer>是一个非常好的类型来处理。如果你从一个方法返回它,也许IList<FootballPlayer>


总结


问你自己



  1. X a Y?或有 X a Y?

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


其它参考7


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


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


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

其它参考8


设计>实施



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


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


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



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

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

  • 将来这会怎样改变?



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


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


在日常编码中,这并不是每个人都有意识地在这个层面上做的事情。但如果你正在考虑这类话题,你就会在设计水域中徘徊。了解它可以解放。


考虑设计细节



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


footballTeam.Reverse()对你有意义吗? footballTeam.ConvertAll< TOutput>()看起来像你想要的东西?


这不是一个棘手的问题;答案可能真的是是。如果你实现/继承List< Player>或IList< Player>,你就会坚持下去;如果那是你模特的理想选择,那就去做吧。


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


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


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


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


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


您可能会觉得这些接口根本不适用于您的模型。这不太可能(IEnumerable< T>在许多情况下是有用的)但它仍然是可能的。


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


转到实施



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


这可能不是一个很大的进步,人们经常将设计和实施混为一谈,因为它很可能在一两秒内完成它并开始打字。


思想实验



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


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


设计尽管如此,在C#中你不能通过继承List< T>来实现所有这些,因为C#仅支持单继承。(如果你在C ++中试过这个malarky,你可能会认为这是一件好事。)通过继承和一个通过组合实现一个集合很可能感觉脏。除非你实现了ILIst< Player> .Count和IList< StaffMember> ;.Coun等属性会让用户感到困惑。明确地说,然后他们只是痛苦而不是混淆。你可以看到这是怎么回事;直觉思考这条大道可能会告诉你走向这个方向是错误的(无论是对还是错,你的如果你以这种方式实现它,也可能是同事!)


简答(太迟)



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

其它参考9


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


这在逻辑上是错误的:


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


这是正确的:


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

其它参考10


取决于上下文



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


语义不清楚



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


类是可扩展的



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


类更复杂



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


结论/注意事项



当您拥有非常具体的上下文时,可以将团队视为玩家列表。例如,在一个方法中,写入是完全可以的


IList<Player> footballTeam = ...


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


type FootballTeam = IList<Player>


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

其它参考11


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


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


string team = new string();


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


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


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


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

其它参考12


允许人们说


myTeam.subList(3, 5);


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

其它参考13


仅仅因为我认为其他答案几乎取决于足球队是a-aList<FootballPlayer>还是has-aList<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 */
 }


但是,如果你公开地暴露给第三方,你只是做了一个重大改变,将导致编译和/或运行时错误。


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

其它参考14


这取决于团队对象的行为。如果它的行为就像一个集合,那么首先使用普通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



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



记住,所有模型都错了,但有些模型很有用。 -George E. P. Box [170]


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


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


- 编辑


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


- 第2版


我真的不记得我所说的不要过度工程的评论。虽然我认为KISS的心态是一个很好的指导,但我想强调的是,从List继承业务类会产生比解决更多的问题,因为抽象泄漏。[171] [172]


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


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

其它参考16


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


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


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


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





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



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



你纠正了,所有客户使用你的团队都是不必要的冗长。然而,与你暴露List Players所有人的事实相比,这个问题非常小,所以他们可以摆弄你的未经您同意的团队。


你继续说:



  它显然没有任何意义。足球队没有一个球员名单。这是球员名单。你不能说John McFootballer已加入SomeTeam的球员。你说约翰加入了SomeTeam。



你对第一点感到错误:删掉列表这个词,显然团队确实有玩家。

然而,你用第二个击中头部的钉子。你不希望客户打电话ateam.Players.Add(...)。你确实希望他们打电话ateam.AddPlayer(...)。你的实施将(可能在其他方面)在内部调用Players.Add(...)





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

其它参考17


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


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

其它参考18


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

其它参考19


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

其它参考20


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


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

其它参考21


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


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


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

其它参考22


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


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


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

其它参考23


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

其它参考24


我想我不同意你的概括。团队不仅仅是一个球员的集合。一个团队有更多关于它的信息 - 名称,标志,管理/管理人员的集合,教练组的集合,然后是球员的集合。如此正确,你的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();
}