当我计划我的计划时,我经常以这样的思路开始:
A football team is just a list of football players. Therefore, I should represent it with:
1
| var football_team = new List <FootballPlayer >(); |
The ordering of this list represent the order in which the players are listed in the roster.
但我后来意识到,除了球员名单,球队还有其他必须记录的属性。例如,本赛季的总比分、当前的预算、统一的颜色、代表球队名称的string等等。
所以我想:
Okay, a football team is just like a list of players, but additionally, it has a name (a string) and a running total of scores (an int). .NET does not provide a class for storing football teams, so I will make my own class. The most similar and relevant existing structure is List, so I will inherit from it:
1 2 3 4 5
| class FootballTeam : List<FootballPlayer>
{
public string TeamName;
public int RunningTotal
} |
但是事实证明,一个指导方针说您不应该继承List。我完全被这条指导方针搞糊涂了。
为什么不呢?
显然List在某种程度上优化了性能。所以如何?如果我扩展List会导致什么性能问题?到底什么会断裂?
我看到的另一个原因是,List是由Microsoft提供的,而我对它没有控制权,所以在公开一个"公共API"之后,我无法更改它。但我很难理解这一点。什么是公共API ?我为什么要关心它?如果我当前的项目没有也不太可能有这个公共API,我可以安全地忽略这个指南吗?如果我从List继承了一个公共API,那么我将会遇到什么困难呢?
为什么这很重要?列表就是列表。有什么可能改变?我可能想要改变什么?
最后,如果微软不想让我继承List,他们为什么不创建类sealed?
我还应该用什么?
显然,对于自定义集合,Microsoft提供了一个Collection类,它应该被扩展,而不是List。但是这个类非常简单,没有很多有用的东西,例如AddRange。jvitor83的答案为该特定方法提供了性能原理,但是为什么慢的AddRange不比没有AddRange好呢?
从Collection继承比从List继承要复杂得多,我没有看到任何好处。当然,微软不会无缘无故地让我做额外的工作,所以我不禁觉得自己在某种程度上误解了什么,继承Collection实际上并不是解决问题的正确方法。
我看到了一些建议,比如实现IList。只是没有。这是几十行样板代码,没有给我带来任何好处。
最后,有人建议将List包入以下内容:
1 2 3 4
| class FootballTeam
{
public List<FootballPlayer> Players;
} |
这有两个问题:
它使我的代码不必要地冗长。我现在必须调用my_team.Players.Count而不是仅仅调用my_team.Count。幸运的是,使用c#我可以定义索引器使索引透明,并转发内部List的所有方法…但是代码太多了!所有这些工作我能得到什么?
它就是没有任何意义。足球队没有球员名单。它是玩家列表。你不会说"John mcfootball已经加入了某个球队的球员"。你说"John加入了某个团队"。你不是在"字符串的字符"中添加一个字母,而是在字符串中添加一个字母。你不是把一本书加到图书馆的书里,而是把一本书加到图书馆里。
我意识到,"引线下"发生的事情可以说是"将X添加到Y的内部列表中",但这似乎是一种非常反直觉的世界观。
我的问题(总结)
正确的c#表示数据结构的方法是什么,"逻辑上"(也就是说,"对人类的大脑")只是list的things加上一些花哨的东西?
继承自List总是不可接受的吗?什么时候可以接受?为什么/为什么不?程序员在决定是否继承List时必须考虑什么?
- 所以,你的问题是什么时候使用继承(一个广场是一个矩形,因为它总是合理的提供一个平方通用矩形请求时)和何时使用成分(一个FootballTeam有FootballPlayers列表,除了其他属性一样"基本",像一个名字)?如果在其他程序员希望得到一个简单列表的时候,您在代码的其他地方向他们传递了一个足球队,他们会感到困惑吗?
- @FlightOdyssey似乎在实践中,我的问题变成了"当扩展List的功能时,为什么组合比继承更好?"我认为"球员名单"是一支足球队最基本的属性——最重要的是名字或预算。一个没有名字的球队是可以存在的(街坊邻居的孩子们组成两个球队在一个下午比赛),但是一个没有球员的球队对我来说没有意义。至于惊讶,我不知道。我很难想象有人会对这种遗传感到惊讶,但我觉得我可能忽略了一些东西。
- 也看到stackoverflow.com/questions/4353203/…
- 如果我告诉你一个足球队是一个拥有球员合同列表的商业公司,而不是拥有一些额外财产的球员列表,那会怎么样?
- 非常简单。足球队是球员名册吗?显然不是,因为就像你说的,还有其他相关的性质。足球队有球员名册吗?是的,所以它应该包含一个列表。除此之外,组合通常比继承更好,因为稍后更容易修改。如果同时发现继承和组合是合乎逻辑的,那么就选择组合。
- my_team.CountFootballs, my_team.CountSpareUniforms…
- @FlightOdyssey:小心"正方形是某种矩形"的类比;它只在正方形和矩形是不可变值时才有效。如果它们是可变的,那么就有问题了。
- @FlightOdyssey:假设您有一个方法SquashRectangle,它接受一个矩形,将其高度减半,并将其宽度加倍。现在传入一个矩形,它恰好是4x4,但类型是矩形,还有一个正方形是4x4。调用SquashRectangle后,对象的维数是多少?矩形显然是2x8。如果正方形是2x8,那么它就不再是正方形;如果它不是2x8,那么对相同形状的相同操作会产生不同的结果。结论是可变的正方形不是矩形。
- 显然,Squash()应该只是Rectangle类上的一个虚拟方法,当您试图调用伪装成Rectangle的Square上的方法时,您会得到一个有用的NotSupportedException。为了避免这个异常,我将在类中添加一个virtual IsSquashable属性,从而将这个烦人的异常转换为一个愚蠢的异常(因为他们应该检查这个标志!)然后,我……哦,不,我变成斗鸡眼了。OO设计很难。
- 因此,任何继承都不能工作,因为真正的子类总是关闭一些功能,因为它只是父类的一个子集。只是我们从来没有实现过所有的功能,这并不明显。所以,你的想法导致荒谬,是错误的。是的,必须重写不适合子类的函数。
- 你的陈述完全是错误的;一个真正的子类需要具有超类的超集的功能。string需要做object可以做的所有事情,甚至更多。
- @EricLippert关于Rectangle和Square的观点很好。我用这个例子的目的,并不是说,在每一个可能的类结构应实现为Square Rectangle的一个子类,给一个简单的例子来说明遗传的一般概念不太深陷扩展解释和/或模糊的术语。
- List有太多与业务无关的方法和属性。也许您可以将football team声明为一个接口。基于列表的实现是一种可能的实现。
- 这里的答案很有道理(尤其是参见Eric Lippert和Sam Leach的观点)。事实上,一个足球队并不是一份球员名单。这只是其中一个方面。一个可以从List继承的类,如果它的名称是FootballPlayerCollection,那么它就是有意义的(纯粹主义者会纠正我,Collection是更好的选择,好的,但是您明白了)。
- 稍微偏离主题(但仍然值得一提):List不太合适,因为它可能包含重复项。一个足球队真的能不止一次地拥有同一名球员吗?如果FootballTeam继承自或包含一个List,情况就是这样。我建议您查看具有更严格保证的集合类型,比如ISet。
- 我经常看到的一种方法(不管我喜欢与否,我仍然在观望)是使用class FootballTeam : IEnumerable,这是主题的变体。
- 我不同意你列出的两个"问题"。首先,如果我有一个团队和11名球员,我希望我的团队。Count = 11,玩家。Count(一个数组)= 11和my_team。Count = 1(实际上,my_team甚至不应该有Count方法)。其次,一个足球队不仅仅是球员——它有各种各样的属性,比如"建立在"、"状态"等等。因此,它被更恰当地建模为my_team,其中包含作为属性的播放器。职业足球队{公开名单<足球运动员>球员;是一种自然的建模方法,即使没有MS的指导。
- 看看这篇文章的答案。stackoverflow.com/questions/400135/c-sharp-listt-or-ilistt
- 这里的许多答案似乎忽略了一般的要点,即列表<..>的特殊之处在于它能够覆盖和提高性能,所以最好使用Collection<..>。如果分析师/程序员想要将一支足球队建模为一组球员,那么这就取决于他/她。只要他们的世界观是内在一致的,你就不能谴责一个人的世界观。
- 对我来说,一个足球队不是一个球员列表,它包含一个球员列表,以及其他属性和特征。因此,不从List<>继承似乎是合乎逻辑的,因为它不是List。
- public List Players; 不必是公共财产。封装/隐藏方面是程序员的职责。美化代码/类是他/她的工作,这样用户就不必知道如何实现。同样使用组合,您可以隐藏像FootballTeam.Clear();这样的东西,并对列表有更多的控制。
- 哇,答案是不是出了问题。建模总是很棘手,但真正的问题是什么时候、什么时候不从列表中继承,以及为什么MS指南建议不这样做。请回答问题。
- 没有开玩笑。特别是在代码分析警告CA1002(由于某种原因?)被引发的情况下,如果您有一个列表作为公共成员,并且根本没有从它继承任何东西。这与组合与继承无关,而且似乎没有任何注释或答案真正解决了这个问题。
- 哇,我很震惊。这么简单的问题,却有那么多炒作。很多人已经在评论中回答了这个问题,很明显,一个足球队不仅仅是一个球员名单。就是这样,你们都在这里发布什么??
这里有一些很好的答案。我还要补充以下几点。
What is the correct C# way of representing a data structure, which,"logically" (that is to say,"to the human mind") is just a list of things with a few bells and whistles?
让十个熟悉足球存在的非计算机程序员来填空:
1
| A football team is a particular kind of _____ |
有没有人说过"一些花哨的足球运动员名单",或者他们都说过"运动队"、"俱乐部"或"组织"?你认为一支足球队是一种特殊的球员名单,这种想法存在于你的人类思维中,而且只存在于你的人类思维中。
List是一种机制。足球队是一个业务对象——也就是说,一个对象表示程序业务领域中的某个概念。不要把这些!足球队是一种球队;它有一个花名册,花名册是球员的名单。花名册并不是一种特殊的球员名单。花名册是球员的名单。因此,创建一个名为Roster的属性,它是一个List。如果你不相信任何一个了解足球队的人都有权把球员从名册上删除,那就趁你还在做这件事的时候去做吧。
Is inheriting from List always unacceptable?
无法接受谁?我吗?不。
When is it acceptable?
当您构建扩展List机制的机制时。
What must a programmer consider, when deciding whether to inherit from List or not?
我是在构建一个机制还是一个业务对象?
But that's a lot of code! What do I get for all that work?
您花了更多的时间输入您的问题,这将需要您为List的相关成员编写50次转发方法。很明显,您并不害怕冗长,我们这里讨论的是非常少量的代码;这是几分钟的工作。
更新
我对此进行了更多的思考,还有一个理由不把足球队作为球员列表来建模。事实上,用球员名单来建立足球队的模型可能是个坏主意。一个球队拥有一份球员名单的问题在于,你所拥有的只是球队在某一时刻的快照。我不知道你的业务案例是对于这门课,但是如果我有一个类代表一个足球队我想问这样的问题"多少海鹰队球员因伤错过了游戏在2003年和2013年之间?"和"什么丹佛球员以前打了另一个团队的最大码了同比增长?"或"今年Piggers一路吗?"
也就是说,在我看来,一个足球队似乎是一个历史事实的集合,比如球员何时被招募、何时受伤、何时退役等等。很明显,当前的球员名单是一个重要的事实,应该放在最前面和最中间,但是你可能想对这个对象做一些其他有趣的事情,需要一个更历史的视角。
- 虽然其他的答案都很有帮助,但我认为这个答案最直接地表达了我的担忧。关于夸大代码的数量,您是对的,最终这并不是很多工作,但是当我在程序中包含一些代码而不理解为什么要包含这些代码时,我确实很容易感到困惑。
- @Superbest:很高兴我能帮上忙!你听到这些怀疑是对的;如果你不明白为什么要写一些代码,要么弄明白,要么写一些你能理解的不同代码。
- @EricLippert:"你花了更多的时间来输入你的问题,这需要你为List的相关成员写50遍转发方法。"——我已经有一段时间没有在Visual Studio工作了,但是当使用Java和Eclipse时,环境可以自动生成这些方法(Source/Generate delegate methods/select the list/finish: 4 mouse click)。VS没有这样的选项吗?
- 所谓的"机制"和"业务对象"之间的区别并不能解释为什么列表不应该扩展到"业务对象"。每个对象都是"机制",尽管并不总是像这样的泛型元素那样清晰和简单。
- "什么时候可以接受?"当您构建扩展List机制的机制时。或者当你必须关心额外的间接层的性能时,这一点似乎从来没有人提过。
- 但是,您似乎忘记了优化的黄金规则:1)不要这样做,2)还不要这样做,3)在没有首先执行性能概要以显示需要优化的内容之前不要这样做。
- 老实说,如果您的应用程序要求您关心虚拟方法等的性能负担,那么任何现代语言(c#、Java等)都不适合您。我有一台笔记本电脑,可以同时模拟我小时候玩的所有街机游戏。这让我不用担心这里和那里的间接性。
- @Jules: VS有这样一种实现接口的机制;我不知道它是否有一个转发方法。如果Roslyn不存在的话,用它来写会很容易。
- @Jules:我对我的VS技能没有足够的信心来坚持VS IDE缺少这个特性,但是我注意到一些VS扩展(比如r#)确实支持这个特性。
- 如果你不需要担心,那么你就不需要担心。如果你做了,你就做了。我想说的是,当它成为瓶颈时,继承而不是组合可以成为解决方案。没有理由仅仅因为在某个地方遇到瓶颈就必须转储整个语言……
- Obviously the current player roster is an important fact that should probably be front-and-center, but there may be other interesting things you want to do with this object that require a more historical perspective. -考虑YAGNI?当然,你可能在不久或很长一段时间内需要看看这些东西……但你现在需要吗?为未来需求(可能存在,也可能不存在……)的可能性而构建是在浪费精力。
- 我完全同意。最初的海报并没有告诉我们这个项目的业务是什么,只是告诉我们足球队参与其中。如果这是一个游戏的模拟,那是一回事。如果它的目的是模拟现实,那就是另一回事了。
- 关于你的更新:我实际上在尝试做一个Delta-V计算器,它消耗了火箭部件的列表(包括质量和燃料量等属性)。的确,火箭没有一个单一的部件列表,但它在每个阶段都有不同的部件集。我通过给火箭一个List来处理它,其中每个Stage都有一个List——尽管我对这个特殊程序之外的一般问题感到好奇。
- @Superbest:现在我们肯定处在"组合而不是继承"的光谱末端。火箭是由火箭零件组成的。火箭不是"一种特殊的零件清单"!我建议你再剪短一些。为什么是列表,而不是IEnumerable——序列?
- @EricLippert——如果你还没有看过福勒的时间模式,你应该看看他是怎么写的。),但是否需要这样做在很大程度上取决于业务用例和存储/检索需求。例如,您可能希望您的对象只表示快照,但是要让数据库基础设施了解时间方面。我在这个项目中就是这么做的。
- @MattJohnson:谢谢你的链接。OOP教学的一个讽刺之处在于,"银行账户"通常是学生们的早期练习。当然,他们学习的建模方法与银行软件中实际银行账户的建模方法是完全相反的。在真正的软件中,帐户是一个只能追加的借方和贷方列表,可以从中计算余额,而不是一个在更新发生时被销毁和替换的可变余额。
- @Mehrdad:重申Eric Lippert所说的,考虑到运行时已经在做的所有事情,某些事情总是比选择组合更糟糕的瓶颈。任何从继承中更快地移动的东西都不应该用c#编程,这肯定已经影响了性能。
- @Superbest, Kerbal太空计划的目的是什么?
- @SeanLynch:《燕麦片》的作者住在西雅图,我强烈怀疑小猪实际上是海鹰。所以,是的,小猪今年一路走来喔!
- 这个练习的目的不是提倡创建一个类型层次结构的特定方法,而是响应最初海报上的断言,即"任何人类的思维"都会从逻辑上把一个足球队看作一个球员列表。如果你不同意这个观点,那么我邀请你写一个你认为能更好地回答这个问题的答案。
- 我完全同意你最后的陈述。很明显,这里的每个人都同意这一点——足球队并不是一份球员名单。问题是它完全没有抓住问题的重点。这是一个很不幸的例子,因为每个人对这个问题的本能反应完全掩盖了真实有效的问题。更好的说法是,他想扩展List以获得Roster属性中的一些小功能。然后,我们可能已经能够讨论为什么MS建议不扩展List的原因,以及什么时候这样做是合适的。
- @gilly3根据OP在这里的评论,似乎选择的例子和问题的观点是正确的。
- @EricLippert扩展方法几乎消除了这里对组合(即但是,用它们实现接口的能力并没有达到预期的效果。不过Scala的特性在这里可以很好地工作。
- @EricLippert:我不同意你更新后的部分回答。创建业务对象以表示足球队的实例,并使用数据仓库技术获取业务智能/历史记录,这样不是更好吗?
- 什么指标更好?
- 今年小猪们并没有走完全程。哦。
- 对不起,埃里克,我不同意你的假设,一个球队不是一个球员的名单。我认为足球队是一个球员列表,而足球俱乐部是一个球员列表。一个俱乐部有经理和历史等等,但是一个球队可能只存在于一场比赛中。我认为答案和评论一直在使用术语"团队"和"俱乐部"互换,这导致混淆。团队这个词的定义是:"一群球员……",所以我认为一个球队就是一列球员。
- 我还想说,我感谢您对机制和业务对象之间差异的解释。这是我发现非常有用的东西,谢谢。
- 还有一句名言:"足球队是一群球员的集体名字……这样的队伍可以被选中参加比赛。代表一个足球俱乐部"。在体育语境中,team这个词是一组运动员的集合名词。因此,我认为说一个团队是一组球员(或者更准确地说,是一组球员)是合理的。
- 你在"团队"和"俱乐部"之间所做的区分,如果没有首先在特定的背景下定义它们,大多数人就会忽略。之前我看了你的评论,如果你问我一个足球队之间的区别和一个足球俱乐部,我说一个足球队玩的球在一个领域有很多线在北美,而足球俱乐部玩球球在英国球场上几行。换句话说,一个球队和一个俱乐部之间的区别,大致相当于一辆卡车和一辆卡车之间的区别。
- 有一点我还没有注意到,那就是一个足球队可能迟早会包含球员以外的东西。(设备?其他员工?)如果一个列表有更多的列表附加在它的旁边(而不是作为列表的子列表),那将会很奇怪。我以前做过。这是奇怪的。
- 无意冒犯,但在我看来,更新部分太过分了。当然,考虑到适当的用例,这是一个很好的理由不模型团队名单的球员,但如果用例,远远超出了被描述为手头的问题不是有用的寻找和理解一个适当的设计为当前的问题。毕竟,有人可能会指出,你实际上更需要一些东西,比如表示组成球员的原子的物体图,因为你可能在模拟足球比赛中涉及的物理(包括泥土……
- …随着时间的推移,从一个玩家的皮肤切换到另一个)。或者有人指出,"历史事实的集合"是不够的,因为它不包含地理上分布的公众信息(回答问题,比如"什么公众认为球员X仍然是球队的一员一年之后的另一部分公众认为X辞去球队吗?")。因此,我不认为你的更新中的观点是一个普遍适用的"不以球员列表作为足球队模型的理由"。
- "我为什么要这样做?"是程序员/开发人员应该问的最重要的问题!任何在每次编程时问这个问题的人,都是在他们自己的程序上,或者已经是一个优秀的程序员了。
- 我不会让播放器列表只读…选秀开始了,你的花名册上有勃起花,而你的花名册是只读的?祝你好运。
- 我确信其他答案已经有了这一点:即使你在哪里创建一个模型,你只对当前的玩家集感兴趣,列表也会是错误的。列表可能包含重复项,在第一个位置有列表表明顺序很重要。你不希望在花名册上有重复的名字在花名册上,顺序并不重要;如果你想将属性映射给玩家,你应该使用地图/字典,而不是像列表这样反复无常的东西。
- 关键是使公开列表的公共属性为只读。当然,列表的内部表示对于Team类仍然是可变的。
- 有人不明白这个问题与足球队无关吗?这是一个关于扩展列表的编程问题,足球只是一个例子,一个可怜的真,但那又怎样?你们都应该忘掉足球。
- @dannyhut:既然你对这个问题的态度很强烈,为什么不写自己更喜欢的答案呢?从你的角度向我们展示一个高质量的答案,我们都可以从中学习。
- @Eric Lippert: m-y发布的答案很好地解决了这个问题,我给出的任何答案都只是重复,尽管CA: 1002的Microsoft文档提供了一个例子,可以在其中扩展List。我的感觉不是关于这个问题,而是关于足球的空间,而不是解决这个问题。这种情况经常发生。
- @dannyhut:我明白了。那么,你处理这个感知问题的策略是对5年的问题发表批判性的评论,并给出公认的答案?祝你好运;你的工作很艰巨。正如你所注意到的,有很多5岁以上的孩子提出的问题都有公认的答案可以批评。
- @Eric Lippert:我的问题不在于给出的问题或答案,我很高兴有一个公认的答案。你的权利有很多5岁以上的孩子的问题,那是因为人们仍然对这些问题感兴趣,就像我来这里寻求答案,不得不花很多时间阅读关于什么是足球队的辩论。如果你不觉得这有什么问题,没关系,但我有。这样的事情应该被评论,这是评论的部分原因。
- @EricLippert……小猪们需要一个更好的B计划来对付牛仔
Lastly, some suggest wrapping the List in something:
这是正确的方法。用"不必要的啰嗦"来看待这个问题是不好的。当您编写my_team.Players.Count时,它有一个显式的含义。你要计算玩家的数量。
. .没有任何意义。计算什么?
一个团队不是一个列表——它不仅仅由一组球员组成。一个球队拥有球员,所以球员应该是其中的一员。
如果你真的担心它过于冗长,你可以从团队中公开属性:
1 2 3 4 5
| public int PlayerCount {
get {
return Players.Count;
}
} |
. .这就变成:
这句话现在有了意义,也符合得墨忒耳的律法。
您还应该考虑遵循复合重用原则。通过继承List,您可以说一个团队是一组玩家,并从中暴露出不必要的方法。这是不正确的——正如你所说,一支球队不仅仅是一份球员名单:它有名字、经理、董事会成员、教练、医务人员、工资上限等等。通过让您的team类包含一个球员列表,您是在说"一个球队有一个球员列表",但是它也可以有其他的东西。
- 我把措辞改为"啰嗦",以便更清楚。我不明白你为什么说my_team.Count"毫无意义"。对我来说,"数团队"是一个非常有意义的概念。我能理解为什么你会坚持认为一个人真的"计算一个团队的球员"——这是一个哲学上的区别吗?还是说包装更好是有实际原因的?我抱怨额外的工作,因为每次回到代码时,我都会想,"等等,我为什么要包装这个?"为什么不直接继承呢?"我也想不出一个解释它的评论。
- @Superbest:如果你在谈论管理列表的"额外工作",你总是可以有一个属性"TeamPlayers"暴露出来,并在它上面做AddRange,从列表继承是没有意义的
- @SimonWhitehead我理解"Has-A"vs。"Is-A"区别(至少我听说过,但我不太清楚它在实践中有什么作用)。然而,一般来说,"Is-A"关系是很常见的,并且在c#中并不气馁(据我所知)。为什么"List"是一个异常,而同时不是sealed?我会遇到什么问题?
- 如果我对你说:"我今天加入了X团队。"你会认为我是球员吗?或者要求澄清我在团队中的角色?
- 我不是故意要大谈特谈,但我会假设你就是那个球员。此外,如果你以另一种身份加入,我希望你能明确说明这一点。
- @pseudocoder实际上,如果它是密封的,事情就会简单得多(尽管我仍然想知道为什么它是密封的)。
- @SimonWhitehead"你(……)暴露了不必要的方法。"-你能举一个这样的方法的具体例子吗?
- 一般来说,您应该尽可能避免继承。部分原因是,在。net中,我们只能从一个类继承。因此,一旦从List继承,就不能继承其他东西。因此,如果稍后您想使用ObservableCollection(用于绑定),您将引入一个中断更改。
- @Superbest他做到了。Team.PlayerCount和Team.Players.Count。第一个是您的方法(名称比"RunningTotal"更好),而第二个是组合的自然结果。
- @SimonWhitehead我不同意你关于"软件架构POV"的建议。问题空间的语义分析结果应完全不受计算机空间约束的影响。对我来说,最初的问题是一个错误的语义分析:"Johny join Team X"并不意味着一个团队就是一组玩家。
- 如果您担心占用太多的代码行,它也可以是public int PlayerCount => return Players.Count;只有一行代码,并且在没有自动完成的情况下,只需要不到一秒钟的时间就可以输入
哇,你的帖子里有一大堆问题和观点。你从微软得到的大部分理由都是正确的。让我们从List开始
List是高度优化的。它的主要用途是作为对象的私有成员使用。微软没有封住它,因为有时您可能想创建一个名称更友好的类:
class MyList : List> { ... }。现在它就像做
var list = new MyList();一样简单。CA1002:不要公开通用列表:基本上,即使您计划使用这个应用程序作为唯一的开发人员,使用良好的编码实践来开发也是值得的,因此它们会逐渐灌输到您和第二天性中。如果您需要任何消费者拥有索引列表,您仍然可以将列表公开为
IList。这让我们稍后更改类中的实现。微软使
Collection非常通用,因为它是一个通用的概念…名字说明了一切;它只是一个集合。还有更精确的版本,如
SortedCollection、
ObservableCollection、
ReadOnlyCollection等,每个版本都实现了
IList,但没有实现
List。
Collection允许覆盖成员(即添加、删除等),因为它们是虚拟的。
List没有。你问题的最后一部分非常正确。足球队不仅仅是一个球员列表,所以它应该是一个包含该球员列表的类。想想组合和继承。一个足球队有一个球员名单(花名册),它不是一个球员名单。
如果我在写这段代码,类可能看起来像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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.
} |
- "你的帖子里有一大堆问题"——嗯,我确实说过我完全糊涂了。感谢您链接到@Readonly的问题,我现在看到我的问题的很大一部分是关于更一般的组合与继承问题的特殊情况。
- 足球团体:列表<足球运动员>,这似乎是绝对可以接受的,非常有效的我。没有任何真正的技术原因可以解释,为什么这个建筑不值得居住,只有哲学上的争论。
- 我对你的观点持怀疑态度,你认为List是开放的,允许使用更友好的名字。这就是using别名的作用。
- @Brian:除非您不能使用alias创建泛型,否则所有类型都必须是具体的。在我的示例中,List被扩展为一个私有内部类,它使用泛型来简化List的使用和声明。如果扩展名只是class FootballRoster : List { },那么是的,我可以使用别名。我想值得注意的是,您可以添加额外的成员/功能,但是它是有限的,因为您不能覆盖List的重要成员。
- 如果这是程序员的话。SE,我同意当前的答案投票范围,但是,由于这是一个SO帖子,这个答案至少尝试回答c# (. net)的特定问题,即使有明确的一般设计问题。
- @jberger:我总是喜欢使用Enumerable.Count(IEnumerable)的LINQ版本,这样,如果我更改了底层类型,只要它是IEnumerable,它就会一直工作。与只使用List. count相比,性能损失非常小。不过,有一天,我可能会决定要一个实现IEnumerable的新数据结构,但除此之外,谁知道呢。更多信息请参见stackoverflow.com/a/981283/347172。:)
- Collection allows for members (i.e. Add, Remove, etc.) to be overridden现在这是一个很好的理由不继承列表。其他的答案太有哲理了。
- @Navin:这不适用于List类。Collection类应该是继承的,以便根据需要扩展它的功能。
- @Groo:如果序列是ICollection或ICollection的类型,您就会意识到它不是从O(1)到O(n)。在这些情况下,LINQ函数只是返回到使用ICollection.Count/ICollection.Count方法。
- 是的,对不起,那部分是正确的,我没有想清楚。它还使用First、Last和其他可以从直接访问ICollection成员中获益的扩展方法来进行这种优化,因此这样做实际上没有什么害处。
- 我怀疑你的意思是一连串的问题,因为侦探就是侦探。
- 如果您编写public IList Roster,消费者将付出性能损失。在List上使用for-each比在IList上使用for-each要快得多,就像在我的机器上使用double以上一样。
- @JonathanAllen:这不是foreach关键字的工作方式(请参见stackoverflow.com/a/3679993/347172)。在本例中,底层类型仍然是List,这意味着它将对该类型调用GetEnumerator(),并使用该枚举器进行迭代。
- 如果调用List.GetEnumerator(),将得到一个名为Enumerator的结构。如果您调用IList.GetEnumerator(),您将得到类型为IEnumerable的变量,该变量恰好包含上述结构的装箱版本。在前一种情况下,foreach将直接调用这些方法。在后者中,所有调用实际上都必须通过接口进行分派,从而使每次调用都变慢。(在我的机器上,速度大约是它的两倍。)
- @JonathanAllen:对GetEnumerator()的任何调用都会返回一个实现IEnumerable(或IEnumerable)的类型,具体取决于使用情况。不过(在本例中)底层类型是相同的。它是Enumerator结构。参考资料来源:referencesource.microsoft.com/#mscorlib/system/collections/……它们都返回相同的类型。
- 你完全没有抓住重点。foreach并不关心Enumerator是否实现了IEnumerable接口。如果它能在Enumerator上找到它需要的方法,那么它就不会使用IEnumerable。如果您实际对代码进行反编译,您可以看到foreach在遍历List时如何避免使用虚拟分派调用,而在遍历IList时将使用虚拟分派调用。
- 事实上,GetEnumerator甚至不需要返回IEnumerable。您可以创建自己的枚举器,它具有正确的方法,但不实现该接口,并且foreach仍然可以工作。
1 2 3 4 5
| class FootballTeam : List<FootballPlayer>
{
public string TeamName;
public int RunningTotal;
} |
以前的代码的意思是:一群在街上踢足球的家伙,他们碰巧有个名字。喜欢的东西:
不管怎样,这个代码(来自m-y的答案)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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.
} |
意思是:这是一支有管理人员、球员、管理员等的足球队。喜欢的东西:
这就是你的逻辑是如何在图片中呈现的……
- 现在只需要一个SackManager()方法…
- 我希望你的第二个例子是足球俱乐部,而不是足球队。足球俱乐部有经理和行政人员等。由此可知:"A football team(足球队)是对一群球员的统称……这样的球队可以被选中参加与对手球队的比赛,代表一个足球俱乐部……"所以我的观点是,一个团队是一组球员(或者更准确地说,是一组球员)。
- 更优化的private readonly List _roster = new List(44);
- 需要导入System.Linq名称空间。
这是组合vs继承的一个经典例子。
在这种特殊情况下:
球队是一组有附加行为的球员吗
或
团队是它自己的对象,碰巧包含一个球员列表。
通过扩展列表,你可以在以下几个方面限制自己:
您不能限制访问(例如,阻止人们更改花名册)。无论是否需要,都可以获得所有列表方法。
如果你还想要其他东西的列表会发生什么。例如,球队有教练、经理、球迷、装备等。其中一些很可能是列表本身。
您限制了继承的选项。例如,您可能想要创建一个通用的Team对象,然后从该对象继承棒球队、足球队等。要从列表中继承,您需要从团队中继承,但这意味着所有不同类型的团队都必须具有相同的花名册实现。
组合——包括一个对象,它提供了您想要的对象内部行为。
继承——您的对象成为具有您想要的行为的对象的实例。
两者都有各自的用途,但这是一个明显的例子,其中组合是可取的。
- 在第二点的基础上展开,列出一个清单是没有意义的。
- 首先,这个问题与组合与继承无关。答案是OP不想实现更具体的列表,所以他不应该扩展list <>。我扭曲使你有很高的分数stackoverflow应该知道这显然和人们信任你说的话,所以现在upvoted分钟55人,谁的主意感到困惑相信一种方式或另一种是可以建立一个系统,但显然不是!# no-rage
- 他想知道列表的行为。他有两个选择,可以扩展list(继承),也可以在对象(组合)中包含list。也许你误解了问题或答案的一部分,而不是55个人错了,而你是对的?:)
- @TimB . .我们真的在说同一件事吗?en.wikipedia.org/wiki/Composition_over_inheritance
- 是的。这是一个简并的例子只有一个上下标但我对他的具体问题给出了更一般的解释。他可能只有一个团队,并且这个团队可能总是有一个列表,但是仍然可以选择继承这个列表,或者将它包含为一个内部对象。一旦你开始包括多种类型的球队(足球)。板球。等等),他们开始不仅仅是一个玩家列表,你看到你如何有完整的组合和继承的问题。在这种情况下,着眼于全局是很重要的,这样可以避免早期的错误选择,而这些错误选择意味着以后要进行大量重构。
- 这是一个复合与继承的问题。OP提出的话题最终表明,在他的思维中,他并没有真正理解"is-a"和"has-a"关系之间的区别。OP还误解了行为和实现。
- OP没有要求组合,但是用例是一个明确的X:Y问题的例子,其中4个面向对象编程原则之一被破坏、误用或没有完全理解。更明确的答案是,要么编写一个像堆栈或队列这样的专门集合(这似乎不适合用例),要么理解组合。足球队不是足球运动员的名单。OP是否显式地请求它是无关紧要的,如果不理解抽象、封装、继承和多态性,他们将无法理解答案,因此,X:Y会失控
- 2. 可能是以上所有帖子中最好的理由。如果你决定要把其他清单列在上面,你会把自己搞砸的。
正如每个人都指出的那样,一个球员团队并不是一个球员列表。这个错误是由世界各地的许多人所犯的,也许在不同的专业水平。通常问题是微妙的,有时非常严重,就像这个例子。这样的设计是不好的,因为它们违反了Liskov替换原则。互联网上有很多很好的文章来解释这个概念,比如http://en.wikipedia.org/wiki/Liskov_substitution_principle
总而言之,在类之间的父/子关系中,有两条规则需要保留:
一个孩子所需要的特征,不应少于完全定义了父母的特征。除了完全定义子元素之外,父元素不应该要求任何特征。
换句话说,父节点是子节点的必要定义,子节点是父节点的充分定义。
这里有一种方法来思考一个人的解决方案,并应用上面的原则,应该有助于避免这样的错误。应该通过验证父类的所有操作在结构和语义上是否对派生类都有效来测试假设。
足球队是足球运动员的名单吗?(列表的所有属性是否都适用于具有相同含义的团队)团队是同质实体的集合吗?是的,团队是一个球员的集合包含球员的顺序是否描述了球队的状态?球队是否确保保留顺序,除非明确更改?没有,没有球员是否会根据他们在球队中的顺序位置而被列入/淘汰?没有
如您所见,只有列表的第一个特性适用于团队。因此,团队不是一个列表。列表将是管理团队的实现细节,因此它应该只用于存储玩家对象,并使用team类的方法进行操作。
在这一点上,我想指出,在我看来,团队类甚至不应该使用列表来实现;在大多数情况下,应该使用Set数据结构(例如HashSet)来实现。
- 与Set相比,List上的catch不错。这似乎是一个非常常见的错误。当集合中只能有一个元素实例时,某种集合或字典应该是实现的首选对象。拥有一个由两名同名球员组成的球队是可以的。同一时间包含两名球员是不允许的。
- +1表示一些优点,不过更重要的问题是"数据结构是否能够合理地支持所有有用的东西(理想情况下是短期和长期)",而不是"数据结构做的事情比有用的多吗"。
- 我想说的是,人们不应该检查"数据结构是否做了比有用的更多的事情"。它是为了检查"父数据结构是否做了一些与子类可能暗示的行为无关、无意义或违反直觉的事情"。
- @SatyanRaina:其中,能够做一些无关紧要或毫无意义的额外事情并没有错,只要它们不会影响到那些有意义且将被使用的操作。例如,如果一个平衡的二叉树恰好以键排序的顺序迭代元素,而这是不必要的,这实际上也不是一个问题。如。"包含球员的顺序[是]描述球队状态的":如果不需要顺序,或者可能需要的多个顺序仍然可以有效地方便地形成,那么特定的缺省顺序没有害处。
- @TonyD实际上存在一个问题,那就是从父母那里获得的无关特征在很多情况下都无法通过阴性测试。程序员可以扩展Human{eat();run ();写();}从大猩猩{吃();run ();swing();}认为人类具有额外的swing特性并没有什么问题。然后在游戏世界中,你的人类突然开始通过在树上摇摆来绕过所有的陆地障碍。除非明确规定,一个实际的人不应该能够摆动。这样的设计使得api很容易被滥用和混淆
- @SatyanRaina:请不要将主题从理想API中的偶然行为更改为根本错误的派生。具体来说,你的"is the order of inclusion…"和"……测试不是很好地测试一个列表是否合适,它们只是确定了一个列表有一些不需要的属性。您建议使用一个HashSet,这是一个合理的建议,但是使用您的逻辑,我可以说"是否需要按键进行O(1)查找?""也许性能测试通过了,尽管算法效率不高"。测试是一场军备竞赛。
- 我也不建议Player类应该派生自HashSet。我建议Player类"在大多数情况下"应该通过组合使用HashSet来实现,这完全是实现级别的细节,而不是设计级别的细节(这就是为什么我在回答中提到它作为旁注)。如果有一个有效的理由来实现这样的实现,那么可以使用列表来实现它。因此,要回答您的问题,是否需要按键查找O(1) ?不。因此,也不应该从HashSet扩展播放器。
- 集合比数组或列表慢得多,开销也更大。您需要为每个用例决定是否需要这种开销。
- 您混淆了亲子关系和继承。完全不同。
如果FootballTeam有一个预备队和主队呢?
1 2 3 4 5
| class FootballTeam
{
List<FootballPlayer> Players { get; set; }
List<FootballPlayer> ReservePlayers { get; set; }
} |
你会怎么建模呢?
1 2 3 4 5
| class FootballTeam : List<FootballPlayer>
{
public string TeamName;
public int RunningTotal
} |
关系显然是有a而不是a。
或RetiredPlayers ?
1 2 3 4 5 6
| class FootballTeam
{
List<FootballPlayer> Players { get; set; }
List<FootballPlayer> ReservePlayers { get; set; }
List<FootballPlayer> RetiredPlayers { get; set; }
} |
根据经验,如果希望从集合继承,请将类命名为SomethingCollection。
您的SomethingCollection在语义上有意义吗?只有当您的类型是Something的集合时才这样做。
在FootballTeam的情况下,它听起来并不正确。一个Team不仅仅是一个Collection。正如其他答案所指出的,一个亚南人可以有教练、教练员等。
FootballCollection听起来像是一堆足球,或者可能是一堆足球用具。TeamCollection,团队集合。
FootballPlayerCollection听起来像一个播放器集合,如果您真的想要这样做,它将是继承自List的类的有效名称。
真的List是一个非常好的处理类型。如果你从一个方法返回它,可能是IList。
总之
问问你自己
X是Y吗?或者X a Y有吗?
我的类名是什么意思?
- 如果每个玩家的类型都将其分类为属于DefensivePlayers、OffensivePlayers或OtherPlayers,那么拥有一个类型可能是合法有用的,该类型可以由期望有List的代码使用,但也包括IList、IList和IList类型的成员OffsensivePlayers、SpecialPlayers和IList。可以使用一个单独的对象来缓存单独的列表,但是将它们封装在与主列表相同的对象中似乎更简洁[使用列表枚举器的失效…
- …提示主列表已经更改,子列表需要在下次访问时重新生成]。
- 虽然我同意这一点,但看到有人给出设计建议,并建议在业务对象中使用公共getter和setter公开一个具体的列表时,我真的很伤心。
设计>
实施
您公开的方法和属性是一个设计决策。继承的基类是实现细节。我觉得有必要回到前者。
对象是数据和行为的集合。
所以你的第一个问题应该是:
这个对象在我创建的模型中包含哪些数据?这个对象在那个模型中表现出什么行为?未来这种情况会发生怎样的变化?
请记住,继承意味着"isa"(is)关系,而组合意味着"has"(hasa)关系。根据您的视图中的情况选择正确的视图,要记住随着应用程序的发展,事情可能会发展到什么程度。
在考虑具体类型之前,先考虑接口的思考,因为有些人发现这样更容易让他们的大脑进入"设计模式"。
这并不是每个人在日常编码中都会有意识地做的事情。但是如果你正在考虑这类话题,你就在设计的水域中涉猎。意识到这一点是一种解放。
考虑设计细节
查看MSDN或Visual Studio上的List和IList。查看它们公开了哪些方法和属性。在你看来,这些方法都像是有人想对一支足球队做的事情吗?
reverse()对您有意义吗?足球队. convertall ()看起来像您想要的东西吗?
这不是一个刁钻的问题;答案可能是肯定的。如果你实现/继承List或IList,你就被它们卡住了;如果这对你的模型来说是理想的,那就去做。
如果您决定yes,那么这是有意义的,并且您希望您的对象可以作为一个集合/玩家列表(行为)来处理,因此您希望实现ICollection或IList,无论如何都要这样做。名义上的:
1 2 3 4
| class FootballTeam : ... ICollection<Player>
{
...
} |
如果您想要您的对象包含一个集合/玩家列表(数据),并且因此您想要集合或列表成为一个属性或成员,那么无论如何都要这样做。名义上的:
1 2 3 4
| class FootballTeam ...
{
public ICollection<Player> Players { get { ... } }
} |
您可能希望人们能够只枚举一组玩家,而不是计数、添加或删除它们。IEnumerable是一个非常有效的选项。
您可能觉得这些接口在您的模型中根本没有任何用处。这是不太可能的(IEnumerable在很多情况下都很有用),但仍然是可能的。
任何试图告诉你其中之一在任何情况下都是绝对错误的人都是被误导的。任何试图在任何情况下都明确无误地告诉你它是正确的人都是被误导的。
转到实现
一旦确定了数据和行为,就可以对实现做出决策。这包括通过继承或组合所依赖的具体类。
这可能不是一个大的步骤,而且人们经常把设计和实现混为一谈,因为很有可能在一两秒钟内在您的脑海中浏览完所有内容并开始输入。
一个思维实验
一个人为的例子:正如其他人所提到的,一个团队并不总是"仅仅"由一群球员组成。你是否保存球队的比赛成绩?在你的模型中,球队和俱乐部是可以互换的吗?如果是这样,如果你的球队是一个球员的集合,也许它也是一个工作人员和/或分数的集合。最后得到:
1 2 3 4 5 6
| class FootballTeam : ... ICollection<Player>,
ICollection<StaffMember>,
ICollection<Score>
{
....
} |
尽管进行了设计,但是在c#中,无论如何都不能通过从List继承来实现所有这些,因为c#"只"支持单继承。(如果您在c++中尝试过这种胡言乱语,您可能会认为这是一件好事。)通过继承实现一个集合和通过组合实现一个集合可能会让人感觉很脏。除非实现ILIst,否则Count等属性会让用户感到困惑。计数和IList <职员>。明确地数数等等,然后他们只是痛苦而不是困惑。你可以看到它的走向;当你沿着这条路思考时,你的直觉很可能会告诉你,朝这个方向走是不对的(无论对错,如果你这样做,你的同事也可能会这么想)。
简短的答案(太晚了)
关于不从集合类继承的指导原则并不特定于c#,您会在许多编程语言中发现它。它是公认的智慧,而不是法律。一个原因是,在实践中,在可理解性、可实现性和可维护性方面,组合常常被认为胜过了继承。在现实世界/域对象中,找到有用且一致的"hasa"关系比找到有用且一致的"isa"关系更常见,除非您非常抽象,尤其是随着时间的推移,以及对象在代码更改中的精确数据和行为。这不应该导致您总是排除从集合类继承;但这可能是一种暗示。
首先,它与可用性有关。如果使用继承,Team类将公开纯粹为对象操作而设计的行为(方法)。例如,AsReadOnly()或CopyTo(obj)方法对团队对象没有任何意义。您可能想要一个更具描述性的AddPlayers(players)方法,而不是AddRange(items)方法。
如果您想使用LINQ,那么实现一个通用接口(如ICollection或IEnumerable)将更有意义。
如上所述,写作是正确的方法。只需将播放器列表实现为私有变量。
足球队不是足球运动员的名单。足球队是由一列足球运动员组成的!
这在逻辑上是错误的:
1 2 3 4 5
| class FootballTeam : List<FootballPlayer>
{
public string TeamName;
public int RunningTotal
} |
这是正确的:
1 2 3 4 5 6
| class FootballTeam
{
public List<FootballPlayer> players
public string TeamName;
public int RunningTotal
} |
- 这并不能解释为什么。OP知道大多数其他程序员都有这种感觉,但他就是不明白为什么这很重要。这只是提供了问题中已经存在的信息。
- 这也是我想到的第一件事,他的数据是如何被错误地建模的。原因是,你的数据模型是不正确的。
- 为什么不从列表中继承?因为他不想要一个更具体的清单。
- 我不认为第二个例子是正确的。您正在公开状态,因此正在破坏封装。状态应该隐藏在对象的内部,而改变该状态的方法应该是通过公共方法。确切地说,哪些方法是由业务需求决定的,但是它应该基于"现在就需要这个",而不是"将来某个时候我不知道是否方便"。保持您的api紧凑,您将节省自己的时间和精力。
- 封装不是问题的重点。如果您不希望类型以更特定的方式运行,我将重点放在不扩展类型上。这些字段在c#中应该是autoproperties,在其他语言中应该是get/set方法,但是在这个上下文中,这完全是多余的
我把你的问题重写一下。所以你可以从不同的角度来看待这个问题。
当我需要代表一支足球队时,我明白它基本上就是一个名字。如:"老鹰"
1
| string team = new string(); |
后来我意识到球队也有球员。
为什么我不能扩展字符串类型,让它也包含一个播放器列表呢?
你对这个问题的看法是武断的。试着去思考一个团队拥有什么(属性),而不是它是什么。
这样做之后,您可以看到它是否与其他类共享属性。想想遗传。
- 这是一个很好的观点——你可以把球队仅仅看作一个名字。然而,如果我的应用程序旨在处理玩家的行为,那么这种想法就不那么明显了。无论如何,最终的问题似乎归结为组合与继承。
- 这是一种值得称道的看待它的方式。附注:一个富人拥有几支足球队,他送给朋友一支作为礼物,朋友改变了球队的名字,解雇了教练,替换了所有的球员。朋友队和男子队在果岭相遇,当男子队输球时,他说:"我不敢相信你用我给你的球队打败了我!"你怎么检查这个?
它取决于上下文
当你把你的球队看作一个球员列表时,你是在把一个足球队的"想法"投射到一个方面:你把"球队"缩小到你在场上看到的人。这种推测只有在特定的背景下才是正确的。在另一种情况下,这可能是完全错误的。假设你想成为团队的赞助者。所以你必须和团队的经理谈谈。在这种情况下,团队被投射到它的经理列表中。这两个列表通常不会重叠太多。其他上下文是当前和以前的玩家,等等。
不清楚语义
因此,将一个团队视为其参与者列表的问题在于,它的语义依赖于上下文,并且不能在上下文更改时扩展它。此外,很难表达您使用的上下文。
类是扩展的
当您使用只有一个成员的类(例如IList activePlayers)时,您可以使用成员的名称(以及它的注释)来明确上下文。当有其他上下文时,只需添加一个附加成员。
类更复杂
在某些情况下,创建一个额外的类可能是多余的。每个类定义必须通过类加载器加载,并由虚拟机缓存。这将消耗运行时性能和内存。当你有一个非常具体的背景时,可以把一个足球队看作一个球员列表。但是在这种情况下,您应该只使用 IList ,而不是从它派生的类。
<
结论/考虑/ hh1 >
当你有一个非常具体的背景时,你可以把一个团队看作一个球员列表。例如,在一个方法中完全可以编写
1
| IList<Player> footballTeam = ... |
当使用f#时,甚至可以创建一个类型缩写
1
| type FootballTeam = IList<Player> |
但是,当上下文更广泛甚至不清楚时,您不应该这样做。当您创建一个新类时,尤其在这种情况下,不清楚将来将在什么上下文中使用它。警告标志是当您开始向类添加附加属性时(团队名称、教练等)。这是一个明显的迹象,表明将使用该类的上下文不是固定的,并且将来会发生变化。在这种情况下,您不能将团队视为一个球员列表,但您应该将(当前活动的、未受伤的等)球员列表建模为团队的属性。
仅仅因为我认为其他的答案与足球队是-a List还是-a (has-a) List有很大的出入,而这两个答案并没有像上面写的那样回答这个问题。
OP主要要求澄清从List继承的准则:
A guideline says that you shouldn't inherit from List. Why not?
因为List没有虚方法。在您自己的代码中,这不是什么问题,因为您通常可以相对轻松地切换实现—但是在公共API中,这可能是一个更大的问题。
What is a public API and why should I care?
公共API是您向第三方程序员公开的接口。认为框架代码。请记住,所引用的指南是"。NET框架设计指南"而非"。NET应用程序设计指南。这是有区别的,而且——一般来说——公共API设计要严格得多。
If my current project does not and is not likely to ever have this public API, can I safely ignore this guideline? If I do inherit from List and it turns out I need a public API, what difficulties will I have?
差不多,是的。您可能想要考虑它背后的基本原理,看看它是否适用于您的情况,但是如果您不构建一个公共API,那么您就不需要特别担心诸如版本控制之类的API问题(其中,这是一个子集)。
如果您将来添加一个公共API,您将需要从实现中抽象出您的API(通过不直接公开您的List),或者违反准则,这可能会导致将来的痛苦。
Why does it even matter? A list is a list. What could possibly change? What could I possibly want to change?
这取决于上下文,但是由于我们使用FootballTeam作为一个例子——假设您不能添加一个FootballPlayer,如果它会导致团队超过工资上限。添加它的一种可能方法是:
1 2 3 4 5 6 7
| 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来修复任何编译错误:
1 2 3 4 5 6 7 8 9 10
| 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 */
} |
但是,如果您公开地向第三方公开,那么您只是做了一个破坏性的更改,这会导致编译和/或运行时错误。
DR -本指南是为公共api编写的。对于私有api,做您想做的。
允许人们说
有意义吗?如果不是,那么它就不应该是一个列表。
- 如果你叫它myTeam.subTeam(3, 5);
- @SamLeach假设这是真的,那么您仍然需要组合而不是继承。因为subList甚至不再返回Team。
这取决于您的"团队"对象的行为。如果它的行为就像一个集合,那么可以先用一个普通列表表示它。然后,您可能开始注意到您一直在重复在列表中迭代的代码;此时,您可以选择创建一个封装球员列表的FootballTeam对象。FootballTeam类成为在球员列表中迭代的所有代码的home。
It makes my code needlessly verbose. I must now call my_team.Players.Count instead of just my_team.Count. Thankfully, with C# I can define indexers to make indexing transparent, and forward all the methods of the internal List... But that's a lot of code! What do I get for all that work?
封装。你的客户不需要知道球队内部发生了什么。您的所有客户都知道,它可以通过在数据库中查找播放器列表来实现。他们不需要知道,这可以改进您的设计。
It just plain doesn't make any sense. A football team doesn't"have" a list of players. It is the list of players. You don't say"John McFootballer has joined SomeTeam's players". You say"John has joined SomeTeam". You don't add a letter to"a string's characters", you add a letter to a string. You don't add a book to a library's books, you add a book to a library.
你会说footballTeam.Add(john),而不是footballTeam.List.Add(john)。内部列表将不可见。
- OP希望了解如何对现实建模,但随后将足球队定义为足球运动员的列表(这在概念上是错误的)。这就是问题所在。在我看来,任何其他的论点对他都是错误的。
- 我不同意球员名单在概念上是错误的。不一定。正如其他人在本页上所写,"所有的模型都是错误的,但有些模型是有用的"
- 正确的模型很难得到,一旦完成,它们就是有用的。如果一个模型可以被误用,那么它在概念上就是错误的。stackoverflow.com/a/21706411/711061
这里有很多很好的答案,但是我想谈谈一些我没有提到的东西:面向对象设计是关于增强对象的能力。
您希望将所有规则、附加工作和内部细节封装到适当的对象中。这样,与这个对象交互的其他对象就不必担心所有的问题。实际上,您希望更进一步,积极地防止其他对象绕过这些内部构件。
当您从List继承时,所有其他对象都可以将您视为列表。他们可以直接访问添加和删除玩家的方法。你会失去控制;例如:
假设您想通过了解球员是退役、辞职还是被解雇来区分他们何时离开。您可以实现一个RemovePlayer方法,该方法接受一个适当的输入enum。但是,通过继承List,您将无法阻止对Remove、RemoveAll甚至Clear的直接访问。结果,您实际上取消了FootballTeam类的权限。
关于封装的其他想法…你提出了以下关切:
It makes my code needlessly verbose. I must now call my_team.Players.Count instead of just my_team.Count.
您是对的,这对于使用您的团队的所有客户来说是不必要的冗长。然而,与您将List Players暴露给各种各样的人以便他们可以在未经您同意的情况下篡改您的团队相比,这个问题是非常小的。
你接着说:
It just plain doesn't make any sense. A football team doesn't"have" a list of players. It is the list of players. You don't say"John McFootballer has joined SomeTeam's players". You say"John has joined SomeTeam".
你在第一点上就错了:去掉"list"这个词,很明显一个团队确实有球员。然而,你用第二颗击中了要害。您不希望客户端调用ateam.Players.Add(...)。您确实希望他们调用ateam.AddPlayer(...)。您的实现将(可能包括其他事情)在内部调用Players.Add(...)。
希望您能看到封装对于增强对象的功能是多么重要。您希望允许每个类都能很好地完成自己的工作,而不用担心受到其他对象的干扰。
What is the correct C# way of representing a data structure...
记住,"所有的模型都是错误的,但有些是有用的。"-乔治·e·p·博克斯
没有"正确的方法",只有有用的方法。
选择一个对你和你的用户有用的。就是这样。罢工>发展经济,不要过度工程。编写的代码越少,需要调试的代码就越少。(请阅读以下版本)。
——编辑
我最好的回答是……视情况而定。从列表继承会将该类的客户端暴露给可能不应该暴露的方法,主要是因为足球队看起来像一个业务实体。
——版2
我真的不记得我说的"不要过度设计"是什么意思了。虽然我相信KISS思想是一个很好的指南,但我想强调的是,从List继承一个业务类会产生比它解决的问题更多的问题,因为抽象泄漏。
另一方面,我相信从列表中继承有用的例子是有限的。正如我在前一版中所写的,这要视情况而定。每个案例的答案都深受知识、经验和个人偏好的影响。
感谢@kai帮助我更精确地思考这个问题的答案。
- 我最好的回答是……视情况而定。从列表继承会将该类的客户端暴露给可能不应该暴露的方法,主要是因为足球队看起来像一个业务实体。我不知道我应该编辑这个答案还是发布另一个,有什么想法吗?
- "从List继承会将该类的客户端暴露给可能不应该暴露的方法"这一点正是使从List继承成为绝对禁忌的原因。一旦暴露出来,随着时间的推移,它们会逐渐被滥用。现在通过"经济地开发"节省时间很容易导致将来损失十倍的时间:调试滥用并最终重构以修复继承。YAGNI原则也可以理解为:您不需要来自List的所有方法,所以不要公开它们。
- "不要过度工程化。你写的代码越少,你需要调试的代码就越少。"<——我认为这是误导。封装和组合而不是继承不是过度工程,也不会导致更多的调试需求。通过封装,可以限制客户机使用(和滥用)类的方式,从而减少需要测试和输入验证的入口点。继承自List,因为它快速、简单,因此会导致更少的bug,这是完全错误的,它只是糟糕的设计,而糟糕的设计会导致比"过度工程"更多的bug。
- @kai我完全同意你的观点。我真的不记得我说的"不要过度设计"是什么意思了。OTOH,我相信只有少数情况下从列表中继承是有用的。正如我在后来的版本中所写的,这要视情况而定。每个案例的答案都深受知识、经验和个人偏好的影响。就像生活中的一切。:-)
这让我想起了"Is a"和"has a权衡"。有时直接从超类继承更容易,也更有意义。其他时候,创建一个独立的类并将继承的类包含为成员变量更有意义。您仍然可以访问类的功能,但不绑定到接口或从类继承而来的任何其他约束。
你是做什么的?和很多事情一样……这取决于上下文。我将使用的指导是,为了从另一个类继承,确实应该有一个"is a"关系。如果你写一个叫BMW的类,它可以继承Car因为BMW确实是一辆车。马类可以继承自哺乳动物类,因为在现实生活中,马实际上是哺乳动物,任何哺乳动物的功能都应该与马相关。但是你能说一个团队是一个列表吗?据我所知,一个团队似乎并不是真正的"a"列表。在这种情况下,我有一个列表作为成员变量。
我的秘密:我不在乎别人怎么说,我就是这么做的。
所以什么阻止了我说:
1
| team.Players.ByName("Nicolas") |
当我发现它比
此外,我的PlayerCollection可能被其他类使用,比如"Club",没有任何代码重复。
1
| club.Players.ByName("Nicolas") |
昨天的最佳实践可能不会成为明天的最佳实践。大多数最佳实践背后没有任何理由,大多数只是社区之间的广泛共识。不要问社区在您这样做时是否会责备您,而是问您自己,什么更易于阅读和维护?
1
| team.Players.ByName("Nicolas") |
或
真的。你有什么疑问吗?现在,您可能需要使用其他技术约束来阻止您在实际用例中使用List。但是不要添加不应该存在的约束。如果微软没有说明原因,那么毫无疑问,这是一种"最佳实践"。
- 虽然在适当的时候有勇气和主动性去挑战公认的智慧是很重要的,但我认为在开始挑战之前,先了解为什么公认的智慧开始被接受是明智的。-1因为"忽略该准则!"并不是"为什么存在该准则?"顺便说一句,在这个案例中,社区不仅"责怪我",而且提供了一个令人满意的解释,成功地说服了我。
- 事实上,当没有任何解释时,你唯一的方法就是突破界限,测试自己不害怕被侵犯。现在。问问你自己,即使List不是一个好主意。将继承从列表更改为集合会带来多大的痛苦?一个猜测:无论您使用重构工具的代码有多长,都比在论坛上发帖的时间短。这是个好问题,但不实用。
- 澄清:我当然不是在等待我的祝福继承任何想要但是我想要它,但是我的问题是理解应该进入这个决定的因素,和为什么许多经验丰富的程序员似乎已经决定我所做的相反。Collection和List是不一样的,所以在一个大型的ish项目中验证它可能需要相当多的工作。
- 非常抱歉,我并没有暗示您的问题是无用的,也没有暗示您过于依赖社区。这是一个好问题,否则就不会费心去回答了。我的观点是,如果有人说:"不要那样做",但没有解释原因,你应该忽略这些建议,同时考虑如果你错了会对结果产生什么影响。将List更改为Collection对于当今的重构工具和模式来说不是问题。(即使对于大型项目)正如您所说,List在Collection上有一些优势,目前接受的回答是哲学上的,而不是实际的。
- team.ByName("Nicolas")表示"Nicolas"是团队的名称。
- 好的评论,山姆,它强调了这个方法更难读和理解。:)
- "不要添加不应该存在的约束",相反,不要暴露任何你没有充分理由暴露的东西。这不是纯粹主义或盲目的狂热,也不是最新的设计时尚潮流。基本面向对象设计101,初级水平。这条"规则"的存在并不是什么秘密,它是几十年经验的结果。
指导方针说的是,公共API不应该显示您是否使用列表、集合、字典、树或其他内容的内部设计决策。一个"团队"不一定是一个列表。您可以将其实现为列表,但是您的公共API的用户应该根据需要使用您的类。这允许您更改您的决策,并在不影响公共接口的情况下使用不同的数据结构。
- 回想起来,@EricLippert和其他人的解释后,你已经得到一个伟大的API的一部分,我的问题回答——除了你说的,如果我做class FootballTeam : List,我班的用户将能够告诉我继承自List通过使用反射,看到List方法没有意义的足球队,并能够使用FootballTeam List,所以我将揭示实现细节到客户端(不必要的)。
当他们说List是"优化的"时,我想他们的意思是它没有像虚拟方法那样的功能,而虚拟方法要稍微贵一点。所以问题是,一旦您在您的公共API中公开了List,您就失去了执行业务规则或稍后定制其功能的能力。但是,如果您将这个继承的类作为项目内部使用(而不是作为API潜在地公开给成千上万的客户/合作伙伴/其他团队),那么如果它节省了您的时间,并且它是您想要复制的功能,那么它可能没有问题。从List继承的好处是,您消除了许多愚蠢的包装器代码,这些代码在可预见的将来永远不会被定制。此外,如果您希望您的类在api的生命周期中显式地具有与List完全相同的语义,那么也可以这样做。
我经常看到很多人做大量的额外工作,仅仅是因为FxCop规则这么说,或者某人的博客说这是一种"坏"的做法。很多时候,这将代码转换成设计模式的怪异之处。与许多指导原则一样,将其视为可能存在异常的指导原则。
虽然我没有像这些答案一样进行复杂的比较,但是我想分享一下我处理这种情况的方法。通过扩展IEnumerable,您可以允许您的Team类支持Linq查询扩展,而无需公开公开List的所有方法和属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Team : IEnumerable <Player >
{
private readonly List <Player > playerList ;
public Team ()
{
playerList = new List <Player >();
}
public Enumerator GetEnumerator ()
{
return playerList .GetEnumerator();
}
...
}
class Player
{
...
} |
如果您的类用户需要** List拥有的所有方法和属性,那么您应该从中派生类。如果他们不需要这些方法,请附上列表,并为类用户实际需要的方法创建包装器。
这是一个严格的规则,如果您编写一个公共API,或者任何其他将被许多人使用的代码。如果你有一个很小的应用程序,并且开发人员不超过2人,你可以忽略这个规则。这会节省你一些时间。
对于小型应用程序,您也可以考虑选择另一种不太严格的语言。Ruby, JavaScript——任何可以让你写更少代码的东西。
我只是想补充一点,埃菲尔铁塔和合同设计的发明者贝特朗·迈耶(Bertrand Meyer)将会毫不犹豫地继承List的遗产。
在他的著作《面向对象软件构造》中,他讨论了GUI系统的实现,其中矩形窗口可以有子窗口。他只是从Rectangle和Tree继承Window来重用实现。
然而,c#不是埃菲尔。后者支持特性的多重继承和重命名。在c#中,当你子类化时,你继承了接口和实现。您可以覆盖实现,但是调用约定是直接从超类复制的。但是,在Eiffel中,您可以修改公共方法的名称,因此您可以在您的Team中将Add和Remove重命名为Hire和Fire。如果Team的一个实例向上转换回List,调用者将使用Add和Remove修改它,但是您的虚拟方法Hire和Fire将被调用。
- 这里的问题不是多重继承、混合(等等)或者如何处理它们的缺失。在任何语言中,甚至在人类语言中,团队都不是由一组人组成的,而是由一组人组成的。这是一个抽象的概念。如果Bertrand Meyer或任何人通过子类化List管理团队是错误的。如果你想要一个更具体的列表,你应该子类化一个列表。希望你同意。
- 这取决于你继承了什么。就其本身而言,它是一个抽象的操作,并不意味着两个类处于"是一种特殊的"关系中,即使它是主流的解释。然而,如果语言设计支持继承,则可以将其视为实现重用的机制,即使组合是当前的错误选择。
更喜欢接口而不是类
类应该避免从类派生,而是实现必要的最小接口。
继承了封装
派生自类中断封装:
公开有关如何实现集合的内部详细信息声明一个可能不合适的接口(一组公共函数和属性)
这使得重构代码变得更加困难。
类是一个实现细节
类是应该对代码的其他部分隐藏的实现细节。简而言之,System.List是抽象数据类型的特定实现,现在和将来可能不合适,也可能不合适。
从概念上讲,将System.List数据类型称为"list"有点分散注意力。System.List是一个可变有序集合,支持用于添加、插入和删除元素的平摊O(1)操作,以及用于检索元素数量或通过索引获取和设置元素的O(1)操作。
接口越小,代码越灵活
在设计数据结构时,接口越简单,代码就越灵活。看看LINQ是如何强大地演示这一点的。
如何选择接口
当你认为"列表"时,你应该先对自己说,"我需要代表一群棒球运动员"。假设你决定用一个类来建模。首先应该确定该类需要公开的接口的最小数量。
一些问题可以帮助指导这个过程:
我需要点票吗?如果不考虑实现
IEnumerable这个集合初始化后会改变吗?如果不考虑
IReadonlyList。我可以通过索引访问项目,这很重要吗?考虑
ICollection我向集合中添加项目的顺序重要吗?也许它是一个
ISet?如果您确实想要这些东西,那么继续执行
IList。
这样,您就不会将代码的其他部分耦合到棒球运动员集合的实现细节,并且只要您尊重接口,就可以自由地更改实现方式。
通过采用这种方法,您将发现代码变得更容易阅读、重构和重用。
关于避免样板文件的说明
在现代IDE中实现接口应该很容易。右键选择"实现界面"。然后,如果需要,将所有实现转发给一个成员类。
也就是说,如果您发现您正在编写大量的样板文件,这可能是因为您公开了比您应该公开的更多的函数。这也是不应该从类中继承的原因。
您还可以设计对您的应用程序有意义的更小的接口,也许只需要几个helper扩展函数就可以将这些接口映射到您需要的任何其他接口。这是我在自己的LinqArray库的IArray接口中采用的方法。
我不同意你的概括。一个团队不仅仅是一群球员。一个球队有太多关于它的信息——名字,徽章,管理/行政人员的收集,教练组的收集,然后是球员的收集。所以正确地说,你的足球队班级应该有3个集合而不是一个集合;如果要正确地模拟现实世界。
您可以考虑一个PlayerCollection类,它像专门化的StringCollection一样提供一些其他功能——比如在对象添加或从内部存储中删除之前进行验证和检查。
也许,PlayerCollection better的概念适合您的首选方法?
1 2 3
| public class PlayerCollection : Collection<Player>
{
} |
然后足球队可以是这样的:
1 2 3 4 5 6 7 8 9 10 11
| 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 ();
} |