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

  显示原文与译文双语对照的内容

在规划我的程序时,我经常以这样的思路开始:

足球队只是足球队员的List 。 因此,我应该用以下方法表示它:


var football_team = new List<FootballPlayer>();

这里 List的顺序代表列表中列出的玩家的顺序。

但我认识到以后,团队也有旁边的其他属性,如果只是 List,视听效果,它必须被记录下来。 例如本赛季的总分总和,当前预算,统一颜色,string 代表团队名称,等等 。

所以我认为:

好的,一个足球队就像一个玩家的List,但另外,它有一个名称( 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 是一个 List 。 可能是什么会改变我该怎么可能是要换什么?

最后,如果微软不希望我从 List 继承,为什么他们不让类成为 sealed

我还应该使用什么?

显然,对于自定义集合,微软提供了一个 Collection 类,它应该被扩展而不是 List 。 于instance,相关但此类是很裸,并不具有很多有用的事情,AddRange,. 回答jvitor83提供一个特定方法的性能原因,但是是一个缓慢的AddRange 不是比没有 AddRange

Collection 继承比从 List 继承更多的工作,我认为没有什么好处。 当然,微软不会让我做额外的工作,所以我无法帮你理解某事物,而继承 Collection 实际上不是解决问题的正确方法。

我已经看到了一些建议,比如实现 IList 。 不行,这是一堆boilerplate的代码代码,让我一无所获。

最后,有人建议将 List 打包为:


class FootballTeam 
{ 
 public List<FootballPlayer> Players; 
}

有两个问题:

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

  2. 只是没有任何意义。 足球队不是"有"的玩家。 它 List的球员。 你不说"john McFootballer加入了玩家"。someteam 你说"约翰加入了 someteam"。 不向字符串中添加字母,将字母添加到字符串中。 你没有在图书馆里添加图书,你给图书馆添加了图书。

我意识到"在引擎盖下"会被认为是"将X 添加到 List的内部",但这似乎是一个非常好的思考世界的方式。

我的问题( 总结)

什么是正确的C# 方式的代表一种数据结构,其中,"逻辑逻辑"(这是说,"到人脑")是只是 Listthings 与几个铃声和口哨声?

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

时间:

这里有一些不错的答案。 我将向他们添加以下点。

少数铃音,whistles,什么是正确的C# 表示数据结构的方法,其中,"逻辑逻辑"( 也就是说,"到人脑") 只是一个 List的幻象?

询问任何熟悉足球存在的十个non-computer-programmer人填写空白:


A football team is a particular kind of _____

任何人说"List的足球队员",或是都他们都说"运动队"或者"梅花"或者"单位"了? 你认为一个足球队某种特定的球员 List 在人类思维和人类思维。

List<T> 是一个机构。 足球队是一个业务对象 --即对象代表业务领域的一些概念,的计划。 不要把这些一个足球队是一种团队;它名单,名单 List的球员。! 名单不是特定种类的List的球员。 一个名单 List的球员。 所以让一个叫做 Roster的属性成为一个 List<Player> 。 让它成为 ReadOnlyList<Player>,除非你相信每个知道球队的人都会从名册上删除球员。

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

谁不能接受我没有。

什么时候可以接受?

当你正在构建一个机制,扩展 List<T> 机制 。

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

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

但这是很多代码 ! 我对这些工作有什么好处?

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

更新

我给了它一些更多的想法,还有另外一个理由不把足球队建模为玩家的List 。 事实上,它可能是一个坏主意模型一个足球队 List 球员太。 问题与一个团队/List的球员这是你必须是一个团队的快照时刻 。 我不知道你的业务案例是什么,但如果我有一个代表足球队的类,我想问它,比如"多少Seahawks玩家由于 2003和 2013之间受伤而错过了游戏"或者"以前为另一个球队玩过的丹佛球员在比赛中增加了最大的year-over-year",year did? "

即足球队似乎对我好建模为历史事实的集合等球员招募时,受伤,退休了, 等等 显然当前球员名单应该front-and-center是一个重要的事实,但可能还有其他有趣的事情你想做的这个对象需要更多的历史观点。

最后,有人建议将 List 打包为:

这是正确的方法。 "不必要的罗嗦"是一个不好的方法。 当你写 my_team.Players.Count 时它有一个明确的含义。 你想给玩家计数。

 
my_team.Count

 

means毫无意义。计数什么?

当你想从 List 继承时,你的设计被破坏。 你的团队不是 List 。 它是一个拥有其他实体的封闭实体。 一个团队拥有玩家,所以玩家应该是它的一部分。

如果你是正在"不必要的罗嗦"真的担心它,你总是可以公开 properties: team,处


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

。变成:

 
my_team.PlayerCount

 

这现在有了意义,并遵循了的Demeter 定律。

你还应该考虑遵循复合重用原则 。 旁边有一从 List<T> 继承,你的意思是一个团队 List的玩家和一些不必要的暴露方法移出它。 这不正确- 就像你所说的,一个团队不仅仅是玩家的List: 它有一个名称,经理,董事会成员,培训师,医疗人员,工资帽,类包含一个的播放器,你表示"一个球队的有一个的玩家"List 等等 通过拥有你的团队,但它也可以有其他的事情。

哇,你的文章有很多问题和要点。 你从微软获得的大部分推理都是精确的。 让我们从 List<T>的一切开始

  • List<T>is高度优化。 主要用法是用作对象的私有成员。
  • 因为有时你可能想要创建一个具有更友好名称的类,所以微软没有将它的封起来: class MyList<T, TX> : List<CustomObject<T, Something<TX>> {.. . } 现在,它和做的一样简单 var list = new MyList<int, string>();
  • CA1002: 不公开泛型列表: 基本上,即使你计划使用这里应用作为惟一的开发人员,所以值得开发了具有良好的编程实践,因此它们成为instilled成你和本能React了。 你仍然可以将 List 公开为 IList<T>,如果你需要任何使用者具有索引 List 。 这使我们可以稍后在类中更改实现。
  • 微软使 Collection<T> 非常通用,因为它是一个通用概念。。 名字是这么说的;它只是一个集合。 有更精确的版本,比如 SortedCollection<T>ObservableCollection<T>ReadOnlyCollection<T>,等等,它们每一个都实现 IList<T>,但代价不是 List<T>
  • Collection<T> 允许成员( 例如 。 添加,删除,等等,因为它们是虚拟的。 List<T> 不存在。
  • 你的问题的最后一部分是。 一个足球队不仅仅是一个玩家的List,所以它应该是一个包含玩家 List的类。 考虑组合 vs Inheritence 。 一个足球团队有 List的玩家一些( 花名册),它不是一个 List的球员。

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


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


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

以前的代码是:一群来自街头的人,他们碰巧有一个名字。 就像这样:

enter image description here

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

意思:这是一个拥有管理,玩家,管理员,等等 等的足球队:

enter image description here

这就是你在图片中展示的逻辑。

这是组合 vs 继承的经典示例。

在这里特定情况下:

团队是玩家的List,他们有附加行为

或者

团队是它自己的一个对象,它刚好包含一个玩家的List 。

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

  1. 你不能限制访问( 比如,阻止人们改变花名册) 。 无论你是否需要/想要它们,都可以得到所有的List 方法。

  2. 如果你想要列出其他事物的列表,会发生什么。 例如,团队有教练员、管理人员、球迷、设备,等等 其中一些很可能是在他们自己的权利的列表。

  3. 你限制了继承的选项。 例如你可能想要创建一个通用的团队对象,然后拥有 BaseballTeam,FootballTeam,等等,继承自。 然后,以继承 List 你需要做的的继承从团队,但这意味着所有的各种类型的团队被迫具有相同的实现,它的花名册

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

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

两者都有用途,但这是一个明确的例子,组合是可取的。

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

如果你想使用 LINQ,实现一个通用接口如 ICollection<T> 或者 IEnumerable<T> 会更有意义。

就像前面提到的,组合是正确的方法。 只需实现一个 List 作为私有变量。

如果 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 不仅仅是一个 CollectionTeam 可以有教练,教练等,因为其他的答案已经指出。

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

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

List<FootballPlayer> 是一个很好的处理类型。 如果你从方法返回它,可能 IList<FootballPlayer>

总之

问问自己

  1. XY? 或者 XY

  2. 我的类名称意味着它们是什么?

这取决于上下文

当你将你的团队看作是一个球员的List 时,你将把一个foot的"构思"投射到一个方面: 将"团队"缩小到你在字段中看到的人。 这里投影仅在特定上下文中正确。 在不同的上下文中,这可能是完全错误的。 想象你想成为团队的赞助商。 所以你必须与团队的经理联系。 在这个上下文中,团队被投影到它的经理的List 。 这两个列表通常并不重叠。 其他上下文是当前玩家和前玩家等。

不清楚的语义

因此,把团队看作是它的玩家的List的问题在于它的语义依赖于上下文,并且当上下文改变时它不能被扩展。 另外,很难表达,你正在使用哪个上下文。

类是可以扩展

当你使用只包含一个成员的类( 例如。 IList activePlayers ),可以使用成员( 另外它的评论)的名称使上下文清晰。 当有其他上下文时,你只需添加一个额外的成员。

类更复杂

在某些情况下,创建额外的类可能是多余的。 每个类定义都必须通过加载类加载,并被虚拟机缓存。 这会消耗你运行时性能和内存。 当你有一个非常特定的上下文时,你可以把一个足球队看作是玩家的List 。 但是在这种情况下,你真的应该使用 IList,而不是从它派生的类。

结论/注意事项

当你有一个非常特定的上下文时,你可以把一个团队看作是玩家的List 。 例如在一个方法中,写完全可以


IList<Player> footballTeam =.. .

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


type FootballTeam = IList<Player>

但当上下文更广泛或者更不清楚时,你不应该这样做。 尤其是当你创建一个新的类,它在未来可能使用的上下文不清晰时,尤其如此。 警告符号是当你开始向类( 团队名称,教练,等等 ) 添加其他属性时。 这是一个明确的标志,这个类将被使用的上下文是不固定的,将来会改变。 在这种情况下你不能考虑团队的List 球员,但是你应该模型的List ( 当前处于活动状态,没有受伤,等等 ) 球员作为球队的一个属性。

...