在计划我的程序时,我经常会从这样一个思路开始:
足球队只是足球运动员的名单。因此,我应该用以下方式表示它: var football_team = new List
但我后来意识到,除了球员名单之外,球队还有其他属性必须记录下来。例如本赛季的总成绩、当前预算、球衣颜色、代表球队名称的string
等。
那么我想:
好吧,一个足球队就像一个球员名单,但另外,它有一个名字(一个字符串)和一个总得分(一个 int)。 .NET 没有提供用于存储足球队的类,所以我将创建自己的类。最相似和相关的现有结构是 List
但事实证明,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,我会有什么困难?
为什么它甚至重要?一个列表就是一个列表。有什么可能改变?我可能想要改变什么?
最后,如果 Microsoft 不希望我从 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 的所有方法……但那是很多代码!所有这些工作我能得到什么?简单明了没有任何意义。足球队没有“拥有”球员名单。这是球员名单。您不会说“John McFootballer 已加入 SomeTeam 的球员”。您说“John 已加入 SomeTeam”。您不会将字母添加到“字符串的字符”,而是将字母添加到字符串。您不会将一本书添加到图书馆的书籍中,而是将一本书添加到图书馆。
我意识到“幕后”发生的事情可以说是“将 X 添加到 Y 的内部列表”,但这似乎是一种非常违反直觉的思考世界的方式。
我的问题(总结)
表示数据结构的正确 C# 方式是什么,“逻辑上”(也就是说,“对人类思维”)只是 things
的 list
和一些花里胡哨的东西?
从 List<T>
继承总是不可接受的吗?什么时候可以接受?为什么/为什么不?在决定是否从 List<T>
继承时,程序员必须考虑什么?
string
才能完成 object
可以做的所有事情以及更多。
这里有一些很好的答案。我会向他们补充以下几点。
什么是表示数据结构的正确 C# 方式,“逻辑上”(也就是说,“对于人类思维”)只是一个带有一些花里胡哨的东西的列表?
请任何十个熟悉足球存在的非计算机程序员来填空:
足球队是一种特殊的____
有人说“花里胡哨的足球运动员名单”,还是都说“运动队”或“俱乐部”或“组织”?您认为足球队是特定类型的球员名单的想法存在于您的人类思想中,并且仅存在于您的人类思想中。
List<T>
是一个机制。足球队是一个业务对象——也就是说,一个代表程序业务领域中的某个概念的对象。不要混合这些!足球队是一种球队;它有一个名册,一个名册是一个球员名单。名册不是特定类型的球员名单。名册是球员名单。因此,创建一个名为 Roster
的属性,它是一个 List<Player>
。并且在你参与的时候让它ReadOnlyList<Player>
,除非你相信每个了解足球队的人都会从名单中删除球员。
从 List
谁不能接受?我?不。
什么时候可以接受?
当您构建一种扩展List<T>
机制的机制时。
在决定是否从 List
我是在构建机制还是业务对象?
但这是很多代码!所有这些工作我能得到什么?
您花了更多时间输入您的问题,因为您需要为 List<T>
的相关成员编写转发方法五十次。您显然不怕冗长,我们在这里讨论的是非常少量的代码;这是几分钟的工作。
更新
我对此进行了更多思考,还有另一个理由不将足球队建模为球员名单。事实上,将足球队建模为拥有球员名单可能是个坏主意。球队作为/拥有球员名单的问题在于,你所拥有的是球队在某一时刻的快照。我不知道你对这门课的商业案例是什么,但如果我有一个代表足球队的课,我想问它这样的问题:“2003 年至 2013 年间有多少海鹰队球员因伤缺席比赛?”或“之前为另一支球队效力的哪位丹佛球员的码数同比增幅最大?”或“Did the Piggers go all the way this year?”
也就是说,在我看来,一支足球队被很好地建模为历史事实的集合,例如球员何时被招募、受伤、退役等。显然,目前的球员名单是一个重要的事实,可能应该是前面和 -中心,但您可能还想对这个对象做一些其他有趣的事情,这些事情需要更多的历史视角。
哇,你的帖子有一大堆问题和要点。你从微软那里得到的大部分推理都是正确的。让我们从有关 List<T>
的一切开始
List
Microsoft 没有密封它,因为有时您可能想要创建一个具有更友好名称的类:class MyList
CA1002:不要公开通用列表:基本上,即使您打算将此应用程序用作唯一的开发人员,使用良好的编码实践进行开发也是值得的,因此它们会成为您的第二天性。如果您需要任何消费者拥有索引列表,您仍然可以将列表公开为 IList
Microsoft 使 Collection
Collection
你问题的最后一部分是正确的。足球队不仅仅是一个球员名单,所以它应该是一个包含球员名单的类。想想组合与继承。足球队有一个球员名单(名单),它不是球员名单。
如果我正在编写这段代码,该类可能看起来像这样:
public class FootballTeam<T>//generic class
{
// 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.
}
List<T>
被扩展为一个私有内部类,它使用泛型来简化 List<T>
的使用和声明。如果扩展名只是 class FootballRoster : List<FootballPlayer> { }
,那么是的,我可以使用别名。我想值得注意的是,您可以添加其他成员/功能,但它是有限的,因为您不能覆盖 List<T>
的重要成员。
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden
现在 that 是不从 List 继承的一个很好的理由。其他答案的哲学太多了。
List<T>.GetEnumerator()
,则会得到一个名为 Enumerator
的结构。如果你调用 IList<T>.GetEnumerator()
,你会得到一个类型为 IEnumerable<T>
的变量,它恰好包含所述结构的装箱版本。在前一种情况下,foreach 将直接调用方法。在后者中,所有调用都必须通过接口虚拟分派,从而使每个调用变慢。 (在我的机器上大约慢两倍。)
foreach
不关心 Enumerator
是否实现 IEnumerable<T>
接口。如果它可以在 Enumerator
上找到它需要的方法,那么它就不会使用 IEnumerable<T>
。如果您实际反编译代码,您可以看到 foreach 在迭代 List<T>
时如何避免使用虚拟调度调用,但会使用 IList<T>
。
class FootballTeam : List<FootballPlayer>
{
public string TeamName;
public int RunningTotal;
}
以前的代码意思是:一群来自街头踢足球的人,他们碰巧有一个名字。就像是:
https://i.stack.imgur.com/PqKUd.jpg
无论如何,这段代码(来自我的回答)
public class FootballTeam
{
// A team's name
public string TeamName;
// 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.
}
意思是:这是一支拥有管理层、球员、管理员等的足球队。比如:
https://i.stack.imgur.com/4QQ31.jpg
这就是您的逻辑在图片中的呈现方式……
private readonly List<T> _roster = new List<T>(44);
System.Linq
命名空间。
这是 composition 与 inheritance 的经典示例。
在这种特定情况下:
团队是具有附加行为的玩家列表吗
或者
球队是否是它自己的对象,恰好包含一个球员名单。
通过扩展 List,您可以通过多种方式限制自己:
您不能限制访问(例如,阻止人们更改名册)。无论您是否需要/想要它们,您都可以获得所有 List 方法。如果您还想拥有其他事物的列表,会发生什么。例如,球队有教练、经理、球迷、设备等。其中一些很可能本身就是列表。您限制了继承的选择。例如,您可能想要创建一个通用的 Team 对象,然后让 BaseballTeam、FootballTeam 等继承自该对象。要从 List 继承,您需要从 Team 继承,但这意味着所有不同类型的团队都被迫具有该名册的相同实现。
组合 - 包括一个对象,它可以在对象内部提供您想要的行为。
继承 - 您的对象成为具有您想要的行为的对象的实例。
两者都有其用途,但这是一个明显的案例,其中组合更可取。
正如大家所指出的,一队球员不是球员名单。这个错误是由世界各地的许多人所犯的,也许在不同的专业水平上。通常问题是微妙的,有时非常严重,就像在这种情况下一样。这样的设计很糟糕,因为它们违反了 Liskov 替换原则。互联网上有很多很好的文章解释了这个概念,例如,http://en.wikipedia.org/wiki/Liskov_substitution_principle
总之,在类之间的父/子关系中需要保留两条规则:
一个孩子不应该要求比完全定义父母的特征少。
除了完全定义孩子的特征之外,父母不应该要求任何特征。
换句话说,父母是孩子的必要定义,孩子是父母的充分定义。
这是一种思考解决方案并应用上述原则的方法,可以帮助人们避免此类错误。应该通过验证父类的所有操作在结构上和语义上是否对派生类都有效来检验假设。
足球队是足球运动员的名单吗? (列表的所有属性是否以相同的含义应用于团队)团队是同质实体的集合吗?是的,团队是玩家的集合 包含玩家的顺序是否描述了团队的状态,团队是否确保保留序列,除非明确更改?否,否 是否预计球员会根据他们在球队中的顺序位置而被纳入/放弃?不
团队是同质实体的集合吗?是的,团队是玩家的集合
包含球员的顺序是否描述了球队的状态,球队是否确保保留顺序,除非明确改变?不,不
球员是否会根据他们在球队中的顺序位置被包括/放弃?不
如您所见,只有列表的第一个特征适用于团队。因此,团队不是列表。列表将是您如何管理团队的实现细节,因此它只能用于存储玩家对象并使用 Team 类的方法进行操作。
在这一点上,我想指出,在我看来,Team 类甚至不应该使用 List 来实现。在大多数情况下,它应该使用 Set 数据结构(例如 HashSet)来实现。
如果 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>
。
总之
问你自己
是 Y 吗?还是有 X 和 Y?我的班级名称是什么意思吗?
DefensivePlayers
、OffensivePlayers
或 OtherPlayers
,那么拥有一个可以由需要 List<Player>
但也包含在内的代码使用的类型可能是合法有用的IList<DefensivePlayer>
、IList<OffensivePlayer>
和 IList<Player>
类型的成员 DefensivePlayers
、OffsensivePlayers
或 SpecialPlayers
。可以使用单独的对象来缓存单独的列表,但是将它们封装在与主列表相同的对象中似乎更干净[使用列表枚举器的失效...
设计 > 实施
您公开的方法和属性是设计决策。您继承的基类是实现细节。我觉得回到前者是值得的。
对象是数据和行为的集合。
所以你的第一个问题应该是:
这个对象在我创建的模型中包含哪些数据?
该对象在该模型中表现出什么行为?
这在未来会如何变化?
请记住,继承意味着“isa”(is a)关系,而组合意味着“has a”(hasa)关系。在您认为适合您的情况的情况下选择正确的一种,请记住随着应用程序的发展,事情可能会走向何方。
在考虑具体类型之前考虑考虑接口,因为有些人发现这样更容易将他们的大脑置于“设计模式”。
这不是每个人在日常编码中在这个级别上都有意识地做的事情。但是,如果您正在考虑此类主题,那么您就是在涉足设计领域。意识到它可以解放。
考虑设计细节
查看 MSDN 或 Visual Studio 上的 List<T>
和 IList<T>
。查看它们公开了哪些方法和属性。在您看来,这些方法是否看起来都像是某人想要对 FootballTeam
执行的操作?
footballTeam.Reverse()
对您有意义吗? footballTeam.ConvertAll<TOutput>()
看起来像您想要的吗?
这不是一个技巧问题。答案可能真的是“是”。如果您实现/继承 List<Player>
或 IList<Player>
,您就会被它们困住;如果这对您的模型来说是理想的,那就去做吧。
如果您决定是,那是有道理的,并且您希望您的对象可被视为玩家(行为)的集合/列表,因此您希望实现 ICollection<Player>
或 IList<Player>
,请务必这样做。名义上:
class FootballTeam : ... ICollection<Player>
{
...
}
如果您希望您的对象包含玩家(数据)的集合/列表,并且因此希望集合或列表成为属性或成员,请务必这样做。名义上:
class FootballTeam ...
{
public ICollection<Player> Players { get { ... } }
}
您可能会觉得您希望人们只能列举玩家集合,而不是计算、添加或删除他们。 IEnumerable<Player>
是一个值得考虑的完全有效的选项。
您可能会觉得这些接口在您的模型中根本没有用处。这不太可能(IEnumerable<T>
在许多情况下很有用),但仍有可能。
任何试图告诉您其中一个在每种情况下都是绝对和明确错误的人都是被误导的。任何试图告诉你它在每种情况下都绝对正确的人都是被误导的。
继续实施
一旦你决定了数据和行为,你就可以做出关于实施的决定。这包括您通过继承或组合依赖的具体类。
这可能不是一个很大的步骤,人们经常将设计和实现混为一谈,因为很可能在一两秒钟内将所有内容都在你的脑海中运行并开始打字。
思想实验
一个人为的例子:正如其他人所提到的,一支球队并不总是“只是”一群球员。您是否为球队保留了比赛比分的集合?在您的模型中,球队是否可以与俱乐部互换?如果是这样,并且如果您的团队是球员的集合,也许它也是员工的集合和/或分数的集合。然后你最终得到:
class FootballTeam : ... ICollection<Player>,
ICollection<StaffMember>,
ICollection<Score>
{
....
}
尽管进行了设计,但此时在 C# 中,您将无法通过从 List<T>
继承来实现所有这些,因为 C#“仅”支持单继承。 (如果您在 C++ 中尝试过这个问题,您可能会认为这是一件好事。)通过继承实现一个集合并通过组合实现一个集合可能感觉很脏。除非您明确地实现 ILIst<Player>.Count
和 IList<StaffMember>.Count
等,否则 Count
之类的属性会让用户感到困惑,然后它们只会让人感到痛苦而不是令人困惑。你可以看到这是怎么回事;沿着这条道路思考时的直觉很可能会告诉您,朝这个方向前进是错误的(无论对错,如果您以这种方式实施,您的同事也可能会这样做!)
简短的回答(为时已晚)
关于不从集合类继承的准则不是 C# 特定的,您会在许多编程语言中找到它。这是公认的智慧而不是法律。一个原因是,在实践中,组合通常被认为在可理解性、可实施性和可维护性方面胜过继承。在现实世界/领域对象中找到有用且一致的“hasa”关系比找到有用且一致的“isa”关系更常见,除非您深入抽象,尤其是随着时间的推移以及代码中对象的精确数据和行为变化。这不应该导致您总是排除从集合类继承;但这可能是暗示性的。
首先,它与可用性有关。如果您使用继承,Team
类将公开纯粹为对象操作而设计的行为(方法)。例如,AsReadOnly()
或 CopyTo(obj)
方法对团队对象没有意义。您可能需要一个更具描述性的 AddPlayers(players)
方法,而不是 AddRange(items)
方法。
如果您想使用 LINQ,实现诸如 ICollection<T>
或 IEnumerable<T>
之类的通用接口会更有意义。
如前所述,组合是正确的方法。只需将玩家列表实现为私有变量。
让我重写你的问题。所以你可能会从不同的角度看待这个主题。
当我需要代表一支足球队时,我明白这基本上是一个名字。比如:《老鹰》
string team = new string();
后来我意识到球队也有球员。
为什么我不能只扩展字符串类型以便它也包含玩家列表?
您进入问题的点是任意的。试着想想一个团队有什么(属性),而不是它是什么。
完成此操作后,您可以查看它是否与其他类共享属性。并考虑继承。
这取决于上下文
当您将您的球队视为球员名单时,您将足球队的“理念”投射到一个方面:您将“球队”简化为您在球场上看到的人。这种预测仅在特定情况下是正确的。在不同的情况下,这可能是完全错误的。想象一下,您想成为团队的赞助商。因此,您必须与团队的经理交谈。在这种情况下,团队被投射到其经理列表中。这两个列表通常不会重叠太多。其他上下文是当前与以前的参与者等。
语义不明
因此,将团队视为其球员列表的问题在于,它的语义取决于上下文,并且当上下文发生变化时它无法扩展。此外,很难表达您使用的是哪种上下文。
类是可扩展的
当您使用只有一个成员的类时(例如 IList activePlayers
),您可以使用成员的名称(以及它的注释)来明确上下文。当有其他上下文时,您只需添加一个额外的成员。
类更复杂
在某些情况下,创建一个额外的类可能是多余的。每个类定义都必须通过类加载器加载,并会被虚拟机缓存。这会消耗您的运行时性能和内存。当您有一个非常具体的背景时,可以将一支足球队视为球员名单。但在这种情况下,您实际上应该只使用 IList
,而不是从它派生的类。
结论/注意事项
当您有非常具体的背景时,可以将团队视为球员列表。例如,在一个方法中完全可以这样写:
IList<Player> footballTeam = ...
使用 F# 时,甚至可以创建类型缩写:
type FootballTeam = IList<Player>
但是当上下文更广泛甚至不清楚时,你不应该这样做。当您创建一个新类时,尤其是在未来可能使用它的上下文不清楚的情况下,情况尤其如此。一个警告信号是当您开始向您的班级添加其他属性(团队名称、教练等)时。这清楚地表明,将使用该类的上下文不是固定的,并且将来会发生变化。在这种情况下,您不能将球队视为球员列表,但您应该将(当前活跃、未受伤等)球员列表建模为球队的属性。
足球队不是足球运动员的名单。一个足球队是由一系列足球运动员组成的!
这在逻辑上是错误的:
class FootballTeam : List<FootballPlayer>
{
public string TeamName;
public int RunningTotal
}
这是正确的:
class FootballTeam
{
public List<FootballPlayer> players
public string TeamName;
public int RunningTotal
}
仅仅因为我认为其他答案几乎与足球队是“是”List<FootballPlayer>
还是“有”List<FootballPlayer>
的切线相吻合,这实际上并没有按照书面形式回答这个问题。
OP 主要要求澄清从 List<T>
继承的准则:
一条准则说您不应该从 List
因为 List<T>
没有虚拟方法。这在您自己的代码中不是什么问题,因为您通常可以以相对较小的痛苦切换实现 - 但在公共 API 中可能会大得多。
什么是公共 API,我为什么要关心?
公共 API 是您向 3rd 方程序员公开的接口。想想框架代码。请记住,所引用的指南是“.NET Framework 设计指南”而不是“.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 */
}
但是,如果您已经公开暴露给第 3 方,那么您只是做了一个会导致编译和/或运行时错误的重大更改。
TL;DR - 该指南适用于公共 API。对于私有 API,随心所欲。
是否允许人们说
myTeam.subList(3, 5);
有任何意义吗?如果不是,那么它不应该是一个列表。
myTeam.subTeam(3, 5);
,它可能会
subList
甚至不再返回 Team
。
这里有很多很好的答案,但我想谈谈我没有提到的东西:面向对象的设计是关于赋予对象权力。
您希望将所有规则、附加工作和内部细节封装在适当的对象中。这样,与此交互的其他对象就不必担心这一切。事实上,你想更进一步,主动阻止其他对象绕过这些内部结构。
当您从 List
继承时,所有其他对象都可以将您视为一个列表。他们可以直接访问添加和删除玩家的方法。你会失去控制;例如:
假设您想通过了解球员是否退休、辞职或被解雇来区分球员何时离开。您可以实现一个采用适当输入枚举的 RemovePlayer
方法。但是,通过从 List
继承,您将无法阻止对 Remove
、RemoveAll
甚至 Clear
的直接访问。结果,您实际上取消了您的 FootballTeam
类。
关于封装的其他想法...您提出了以下问题:
它使我的代码不必要地冗长。我现在必须调用 my_team.Players.Count 而不仅仅是 my_team.Count。
你是对的,这对于所有客户使用你的团队来说都是不必要的冗长。但是,与您将 List Players
暴露给所有人和杂项的事实相比,这个问题非常小,因此他们可以在未经您同意的情况下摆弄您的团队。
你接着说:
简单明了没有任何意义。足球队没有“拥有”球员名单。这是球员名单。您不会说“John McFootballer 已加入 SomeTeam 的球员”。您说“John 已加入 SomeTeam”。
第一点你错了:去掉“名单”这个词,很明显一支球队确实有球员。
但是,你用第二个字打中了头。您不希望客户调用 ateam.Players.Add(...)
。您确实希望他们呼叫 ateam.AddPlayer(...)
。您的实现将(可能除其他外)在内部调用 Players.Add(...)
。
希望您能看到封装对于赋予对象权力的目标是多么重要。你想让每个类都能很好地完成它的工作,而不用担心来自其他对象的干扰。
这取决于您的“团队”对象的行为。如果它的行为就像一个集合,那么首先用一个普通的列表来表示它可能是可以的。然后您可能会开始注意到您不断重复在列表上迭代的代码;此时,您可以选择创建一个包含球员列表的 FootballTeam 对象。 FootballTeam 类成为在球员列表上迭代的所有代码的家。
它使我的代码不必要地冗长。我现在必须调用 my_team.Players.Count 而不仅仅是 my_team.Count。值得庆幸的是,使用 C# 我可以定义索引器以使索引透明化,并转发内部 List 的所有方法……但那是很多代码!所有这些工作我能得到什么?
封装。您的客户不需要知道 FootballTeam 内部发生了什么。对于您所有的客户都知道,它可以通过在数据库中查找玩家列表来实现。他们不需要知道,这可以改善您的设计。
简单明了没有任何意义。足球队没有“拥有”球员名单。这是球员名单。您不会说“John McFootballer 已加入 SomeTeam 的球员”。您说“John 已加入 SomeTeam”。您不会将字母添加到“字符串的字符”,而是将字母添加到字符串。您不会将一本书添加到图书馆的书籍中,而是将一本书添加到图书馆。
完全正确 :) 你会说 footballTeam.Add(john),而不是 footballTeam.List.Add(john)。内部列表将不可见。
表示数据结构的正确 C# 方式是什么...
请记住,“所有模型都是错误的,但有些模型是有用的。” -George E. P. Box
没有“正确的方法”,只有有用的方法。
选择一个对您和/您的用户有用的。而已。经济发展,不要过度设计。您编写的代码越少,您需要调试的代码就越少。 (阅读以下版本)。
-- 编辑
我最好的答案是......这取决于。从 List 继承会将此类的客户端暴露给可能不应该暴露的方法,主要是因为 FootballTeam 看起来像一个业务实体。
-- 第 2 版
我真的不记得我在“不要过度设计”评论中指的是什么。虽然我认为 KISS 思维方式是一个很好的指导,但我想强调的是,从 List 继承业务类会产生比它解决的问题更多的问题,因为 abstraction leakage。
另一方面,我相信在少数情况下,简单地从 List 继承是有用的。正如我在上一版中所写,这取决于。每个案例的答案都深受知识、经验和个人偏好的影响。
感谢@kai 帮助我更准确地思考答案。
List
继承会将此类的客户端暴露给可能不应该暴露的方法”这一点正是使从 List
继承成为绝对禁忌的原因。一旦暴露出来,随着时间的推移,它们会逐渐被滥用和误用。现在通过“经济发展”节省时间很容易导致未来十倍的节省损失:调试滥用并最终重构以修复继承。 YAGNI 原则也可以理解为:你不需要 List
中的所有方法,所以不要暴露它们。
List
继承,因为它快速简单,因此会导致更少的错误,这是完全错误的,它只是糟糕的设计,而糟糕的设计会导致比“过度工程”更多的错误。
这让我想起了“是”与“有”的权衡。有时直接从超类继承更容易也更有意义。其他时候,创建一个独立的类并将您继承的类作为成员变量包含在内更有意义。您仍然可以访问类的功能,但不受接口或任何其他可能来自类继承的约束的约束。
你做什么?与很多事情一样……这取决于上下文。我将使用的指南是,为了从另一个类继承,确实应该存在“是”关系。因此,如果您编写一个名为 BMW 的类,它可以从 Car 继承,因为 BMW 确实是一辆汽车。 Horse 类可以从 Mammal 类继承,因为马实际上是现实生活中的哺乳动物,并且任何 Mammal 功能都应该与 Horse 相关。但是你能说一个团队就是一个列表吗?据我所知,团队似乎并不真正“是”列表。所以在这种情况下,我会有一个 List 作为成员变量。
指导方针说的是,公共 API 不应该透露您是否使用列表、集合、字典、树或其他任何东西的内部设计决策。 “团队”不一定是列表。您可以将它实现为一个列表,但您的公共 API 的用户应该在需要知道的基础上使用您的类。这允许您在不影响公共接口的情况下更改您的决定并使用不同的数据结构。
class FootballTeam : List<FootballPlayers>
,我班的用户将能够告诉我通过使用反射从 List<T>
继承,看到对足球队没有意义的 List<T>
方法,并且能够在 List<T>
中使用 FootballTeam
,所以我会向客户透露实施细节(不必要地)。
序列化问题
public class DemoList : List<Demo>
{
// using XmlSerializer this properties won't be seralized
// There is no error, the data is simply not there.
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
改用 IList
就我个人而言,我不会从 List 继承,而是实现 IList。 Visual Studio 将为您完成这项工作并创建一个完整的工作实施。看这里:How to get a full working implementation of IList
当他们说 List<T>
是“优化的”时,我认为他们的意思是它没有像虚拟方法这样更昂贵的功能。所以问题在于,一旦您在 公共 API 中公开 List<T>
,您就失去了执行业务规则或稍后自定义其功能的能力。但是,如果您在项目中使用这个继承的类作为内部类(而不是作为 API 潜在地暴露给您的成千上万的客户/合作伙伴/其他团队),那么如果它可以节省您的时间并且它是您想要的功能,那么它可能是可以的复制。从 List<T>
继承的优点是您消除了许多在可预见的将来永远不会定制的愚蠢包装器代码。此外,如果您希望您的类在 API 的生命周期内明确具有与 List<T>
完全相同的语义,那么它也可能没问题。
我经常看到很多人只是因为 FxCop 规则这样说或者某人的博客说这是一种“坏”的做法而做了大量的额外工作。很多时候,这会将代码变成设计模式 palooza 的怪异之处。与许多指南一样,将其视为可能有例外的指南。
我的肮脏秘密:我不在乎人们说什么,我做到了。 .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
Collection<T>
与 List<T>
不同,因此在大型项目中可能需要进行大量工作才能进行验证。
team.ByName("Nicolas")
表示 "Nicolas"
是团队的名称。
虽然我没有像大多数这些答案那样进行复杂的比较,但我想分享我处理这种情况的方法。通过扩展 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
{
...
}
我只想补充一点,埃菲尔的发明者和合同设计的伯特兰·迈耶 (Bertrand Meyer) 会从 List<Player>
继承 Team
,而无需眨眼。
在他的面向对象的软件构建一书中,他讨论了矩形窗口可以有子窗口的 GUI 系统的实现。他只是让 Window
继承自 Rectangle
和 Tree<Window>
以重用实现。
但是,C# 不是埃菲尔。后者支持多重继承和功能重命名。在 C# 中,当您创建子类时,您继承了接口和实现。您可以覆盖实现,但调用约定直接从超类复制。但是,在 Eiffel 中,您可以修改公共方法的名称,因此您可以在 Team
中将 Add
和 Remove
重命名为 Hire
和 Fire
。如果 Team
的实例向上转换回 List<Player>
,调用者将使用 Add
和 Remove
来修改它,但您的虚拟方法 Hire
和 Fire
将被调用。
如果你的类用户需要所有的方法和属性** List 有,你应该从它派生你的类。如果他们不需要它们,请附上 List 并为您的类用户实际需要的方法制作包装器。
如果您编写公共 API 或任何其他将被许多人使用的代码,这是一个严格的规则。如果您的应用程序很小且开发人员不超过 2 个,您可以忽略此规则。这将为您节省一些时间。
对于小型应用程序,您还可以考虑选择另一种不太严格的语言。 Ruby、JavaScript——任何可以让你编写更少代码的东西。
我想我不同意你的概括。团队不仅仅是球员的集合。一支球队有更多关于它的信息——名字、徽章、管理/行政人员的集合、教练组的集合,然后是球员的集合。所以正确地,你的 FootballTeam 类应该有 3 个集合,而不是本身就是一个集合;如果要正确建模现实世界。
您可以考虑一个 PlayerCollection 类,它像 Specialized StringCollection 一样提供一些其他功能 - 例如在将对象添加到内部存储或从内部存储中删除之前进行验证和检查。
也许,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();
}
优先使用接口而不是类
类应该避免从类派生,而是实现必要的最小接口。
继承打破封装
从类 breaks encapsulation 派生:
公开有关如何实现您的集合的内部细节
声明一个可能不合适的接口(一组公共函数和属性)
除其他外,这使得重构代码变得更加困难。
类是实现细节
类是一个实现细节,应该对代码的其他部分隐藏。简而言之,System.List
是抽象数据类型的特定实现,现在和将来可能合适,也可能不合适。
从概念上讲,System.List
数据类型称为“列表”这一事实有点牵强附会。 System.List<T>
是一个可变有序集合,支持用于添加、插入和删除元素的分期 O(1) 操作,以及用于检索元素数量或按索引获取和设置元素的 O(1) 操作。
接口越小代码越灵活
在设计数据结构时,接口越简单,代码就越灵活。只需看看 LINQ 的强大功能就可以证明这一点。
如何选择接口
当你想到“列表”时,你应该先对自己说,“我需要代表棒球运动员的集合”。所以假设你决定用一个类来建模。您首先应该做的是确定该类需要公开的最少接口数量。
一些可以帮助指导此过程的问题:
我需要计数吗?如果不考虑实现 IEnumerable
这个集合在初始化后会改变吗?如果不考虑 IReadonlyList
我可以按索引访问项目重要吗?考虑 ICollection
我将项目添加到集合中的顺序重要吗?也许它是一个 ISet
如果您确实想要这些东西,那么请继续实施 IList
这样,您就不会将代码的其他部分与棒球运动员集合的实现细节耦合,并且只要您尊重接口,就可以自由地更改它的实现方式。
通过采用这种方法,您会发现代码变得更易于阅读、重构和重用。
关于避免样板的注意事项
在现代 IDE 中实现接口应该很容易。右键单击并选择“实现接口”。如果需要,然后将所有实现转发给成员类。
也就是说,如果你发现你正在编写大量样板文件,那可能是因为你暴露了比你应该更多的功能。这与您不应该从类继承的原因相同。
您还可以设计对您的应用程序有意义的更小的接口,也许只需几个辅助扩展函数来将这些接口映射到您需要的任何其他接口。这是我在自己的 IArray
界面中为 LinqArray library 采用的方法。
什么时候可以接受?
引用 Eric Lippert 的话:
当您构建扩展 List
例如,您厌倦了 IList<T>
中缺少 AddRange
方法:
public interface IMoreConvenientListInterface<T> : IList<T>
{
void AddRange(IEnumerable<T> collection);
}
public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
IEnumerable<T>
- 序列?