首页 文章

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

提问于
浏览
1077

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

足球队只是一个足球运动员名单 . 因此,我应该用:var football_team = new List <FootballPlayer>();
此列表的顺序表示球员在名单中列出的顺序 .

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

那么我认为:

好的,足球队就像一个球员名单,但另外,它有一个名字(一个字符串)和一个运行的总分(一个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 AddRange . jvitor83's answer提供了该特定方法的性能原理,但是如何慢 AddRange 不比没有 AddRange 好?

继承 Collection 比继承 List 更有意义,我认为没有任何好处 . 当然,微软不会觉得我在某种程度上误解了某些东西,继承 Collection 实际上并不是解决我问题的正确方法 .

我已经看到了诸如实施 IList 之类的建议 . 就是不行 . 这是几十行样板代码,没有任何帮助 .

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

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

这有两个问题:

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

  • 它只是简单的没有't make any sense. A football team doesn' t "have"一个球员名单 . 这是球员名单 . 你不要't say 113067 . You say 113068 . You don' t添加一个字母到"a string's characters",你给字符串添加一个字母 . 你没有_113071的书籍,你在图书馆里添加一本书 .

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

我的问题(总结)

表示数据结构的正确C#方式是什么,"logically"(也就是说,"to the human mind")只是一个 listthings ,只有几个铃铛和口哨声?

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

26 回答

  • 41

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

    In summary

    问你自己

    • X a Y ?或 X Y

    • 我的 class 名字是什么意思吗?

  • 122

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

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

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

    • CA1002: Do not expose generic lists:基本上,即使您打算将此应用程序作为唯一的开发人员使用,也值得用良好的编码实践进行开发,这样它们就会被灌输到您和第二天性 . 如果您需要任何消费者拥有索引列表,您仍然可以将列表公开为 IList<T> . 这让你以后改变一个类中的实现 .

    • 微软使 Collection<T> 非常通用,因为它是一个通用的概念...这个名字说明了一切;它只是一个集合 . 还有更精确的版本,如 SortedCollection<T> ,_ ObservableCollection<T>ReadOnlyCollection<T> 等,每个版本都实现 IList<T> 但是 not 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.
    }
    
  • 49

    这取决于具体情况

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

    语义不清楚

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

    类是可扩展的

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

    类更复杂

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

    结论/考虑因素

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

    IList<Player> footballTeam = ...
    

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

    type FootballTeam = IList<Player>
    

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

  • 252

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

    这是正确的方法 . "Needlessly wordy"是一个看待这个的坏方法 . 当你写 my_team.Players.Count 时,它有明确的含义 . 你想要算上球员 .

    my_team.Count
    

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

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

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

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

    ..which变成:

    my_team.PlayerCount
    

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

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

  • 15

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

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

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

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

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

  • 14
    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

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

  • 0

    我的肮脏秘密:我不在乎别人说什么,而且我这样做 . .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> . 但是不要添加不应存在的约束 . 如果微软没有记录原因,那肯定是一个“最佳实践” .

  • 21

    这是composition vs inheritance的经典示例 .

    在这个特定情况下:

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

    要么

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

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

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

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

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

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

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

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

  • 5

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

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

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

  • 4

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

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

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

    A football team is a particular kind of _____
    

    有人说"list of football players with a few bells and whistles",还是他们都说"sports team"或"club"或"organization"?你认为足球队是一种特殊的球员名单的想法就在于你的人类思想和你的人类思维 .

    List<T> 是一种机制 . Football team是一个业务对象 - 也就是说,一个对象代表程序业务领域中的一些概念 . 不要混合那些!足球队是一种团队;它有一个名单,名单是一个球员名单 . 名单不是一种特殊的球员名单 . 名单是一个球员名单 . 因此,创建一个名为 Roster 的属性 List<Player> . 除非你相信每个了解足球队的人都会从球队名单中删除球员,否则在你进入球队的时候,你可以做到 ReadOnlyList<Player> .

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

    不能接受谁?我?没有 .

    什么时候可以接受?

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

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

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

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

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

    更新

    我更多地考虑了一下,还有另一个原因就是不要将足球队的模型列为球员名单 . 事实上,将足球队建模为拥有球员名单可能是一个坏主意 . 团队作为/拥有球员名单的问题在于,你所得到的是球队在某个时刻的快照 . 我不知道你的商业案例是什么,但是如果我有一个代表足球队的课程,我会问它像"how many Seahawks players missed games due to injury between 2003 and 2013?"或"What Denver player who previously played for another team had the largest year-over-year increase in yards ran?"或“Did the Piggers go all the way this year?”这样的问题 .

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

  • 16

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

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

    • a儿童应该不要求完全定义父母的特征 .

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

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

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

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

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

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

    • 球员是否应该根据他们在球队中的排名位置被列入/弃牌?没有

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

    在这一点上,我想说一下,在我看来,团队类甚至不应该使用L