在计划我的计划时,我通常会从这样的一系列想法开始:

足球队只是足球运动员的名单。因此,我应以以下方式表示:var football_team=新列表<FootballPlayer>();此列表的顺序表示球员在名册中的排列顺序。

但我后来意识到,除了球员名单之外,球队还有其他必须记录的财产。例如,本赛季总得分、当前预算、制服颜色、代表球队名称的字符串等。。

所以我想:

好吧,足球队就像一个球员列表,但除此之外,它还有一个名字(一个字符串)和一个连续的总得分(一个整数)。NET没有提供存储足球队的类,所以我将创建自己的类。最相似和最相关的现有结构是List<FootballPlayer>,因此我将从中继承:class FootballTeam:列表<FootballPlayer>{ 公共字符串TeamName;公共int RunningTotal}

但事实证明,一条准则说你不应该从List<t>继承。我在两个方面完全被这条准则搞糊涂了。

为什么不呢?

显然,List在某种程度上优化了性能。为什么呢如果我扩展列表,会导致什么性能问题?到底会发生什么?

我看到的另一个原因是List是由Microsoft提供的,我无法控制它,所以在暴露了一个“公共API”之后,我无法稍后更改它。但我很难理解这一点。什么是公共API?我为什么要关心?如果我当前的项目没有也不可能有这个公共API,我可以放心地忽略这个准则吗?如果我确实继承了List,结果发现我需要一个公共API,我会遇到什么困难?

为什么这很重要?列表是一个列表。什么可能会改变?我可能想要改变什么?

最后,如果微软不想让我继承List,他们为什么不把这个类封起来呢?

我还能用什么?

显然,对于自定义集合,Microsoft提供了一个Collection类,该类应该扩展而不是List。但是这个类非常简单,没有很多有用的东西,例如AddRange。jvitor83的答案为该特定方法提供了性能基础,但如何使缓慢的AddRange不比没有AddRange更好?

从收藏中继承比从列表中继承要多得多,我看不出有什么好处。当然,微软不会无缘无故地让我做额外的工作,所以我忍不住觉得我在某种程度上误解了什么,而继承Collection实际上并不是解决我问题的正确方法。

我看到了一些建议,比如实施IList。只是没有。这是几十行样板代码,我什么也没得到。

最后,一些人建议将列表包装为:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

这有两个问题:

这使我的代码变得不必要地冗长。我现在必须调用my_team.Players.Count而不是my_team.Count。幸运的是,使用C#,我可以定义索引器以使索引透明,并转发内部List的所有方法。。。但这是很多代码!我做这些工作能得到什么?这显然没有任何意义。足球队没有球员名单。这是球员名单。你不会说“约翰·麦克足球先生加入了SomeTeam的球员”。你说“John加入了SomeTeam”。您不向“字符串的字符”添加字母,而是向字符串添加字母。您不向图书馆的图书添加图书,而是向图书馆添加图书。

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

我的问题(总结)

正确的C#表示数据结构的方式是什么?数据结构“逻辑上”(也就是说,“对人类的大脑”)只是一个带有一些铃铛和口哨的列表?

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


当前回答

这取决于“团队”对象的行为。如果它的行为就像一个集合,那么可以先用一个普通的List来表示它。然后,您可能会注意到,您一直在复制在列表上迭代的代码;此时,您可以选择创建一个FootballTeam对象来包装球员列表。FootballTeam类成为在球员列表上迭代的所有代码的主场。

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

封装。你的客户不需要知道足球队内部发生了什么。你的客户都知道,它可以通过在数据库中查找玩家列表来实现。他们不需要知道,这会改进您的设计。

这显然没有任何意义。足球队没有球员名单。这是球员名单。你不会说“约翰·麦克足球先生加入了SomeTeam的球员”。你说“John加入了SomeTeam”。您不向“字符串的字符”添加字母,而是向字符串添加字母。您不向图书馆的图书添加图书,而是向图书馆添加图书。

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

其他回答

我肮脏的秘密:我不在乎别人说什么,我也这么做。.NET Framework以“XxxxCollection”(我头上的例子是UIElementCollection)传播。

那么,是什么阻止了我说:

team.Players.ByName("Nicolas")

当我发现它比

team.ByName("Nicolas")

此外,我的PlayerCollection可能会被其他类使用,如“俱乐部”,而不需要任何代码重复。

club.Players.ByName("Nicolas")

昨天的最佳实践可能不是明天的最佳实践。大多数最佳做法背后没有任何原因,大多数只是社区之间的广泛共识。与其问社区,当你这样做时,它是否会责怪你,不如问问自己,什么更容易阅读和维护?

team.Players.ByName("Nicolas") 

or

team.ByName("Nicolas")

真正地你有什么疑问吗?现在,您可能需要处理其他技术限制,这些限制会阻止您在实际用例中使用List<T>。但不要添加不应该存在的约束。如果微软没有记录原因,那么这无疑是一个“最佳实践”。

哇,你的帖子有很多问题和观点。你从微软那里得到的大部分理由都是正确的。让我们从List<T>开始

列表<T>高度优化。它的主要用途是用作对象的私有成员。Microsoft没有密封它,因为有时您可能希望创建一个具有更友好名称的类:class MyList<T,TX>:List<CustomObject<T,Something<TX>{…}。现在,这就像做var-list=newMyList<int,string>();一样简单;。CA1002:不要暴露通用列表:基本上,即使你计划将此应用程序作为唯一的开发人员,也值得用良好的编码实践进行开发,因此它们会逐渐灌输给你和第二天性。如果您需要任何消费者拥有索引列表,您仍然可以将列表公开为IList<T>。这允许您稍后在类中更改实现。Microsoft使集合<T>非常通用,因为它是一个通用概念。。。名字说明了一切;这只是一个集合。还有更精确的版本,如SortedCollection<T>、ObservableCollection<T’、ReadOnlyCollection<T〕等,它们都实现了IList<T>,但不实现List<T>。集合<T>允许重写成员(即Add、Remove等),因为它们是虚拟的。列表<T>没有。你问题的最后一部分是正确的。足球队不仅仅是一个球员名单,所以它应该是一个包含球员名单的班级。思考组成与继承。足球队有一个球员名单(名册),而不是球员名单。


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

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

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

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

当我需要代表一支足球队时,我明白这基本上是一个名字。比如:《老鹰》

string team = new string();

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

为什么我不能扩展字符串类型,以便它也包含一个玩家列表?

你的切入点是武断的。试着想想一个团队有什么(财产),而不是它是什么。

完成后,您可以查看它是否与其他类共享财产。想想遗产。

正如所有人都指出的那样,一队球员并不是一个球员名单。世界各地的许多人都犯了这个错误,可能是不同专业水平的人。问题往往很微妙,偶尔也很严重,就像本例中的情况一样。这样的设计是糟糕的,因为它们违反了利斯科夫替代原则。互联网上有许多很好的文章解释这个概念,例如。,http://en.wikipedia.org/wiki/Liskov_substitution_principle

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

子项不应要求低于完全定义父项的特征。除了完全定义孩子之外,父母不应要求任何特征。

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

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

足球队是足球运动员的名单吗?(列表的所有财产是否以相同的含义应用于团队)团队是同质实体的集合吗?是的,球队是球员的集合球员的出场顺序是否描述了球队的状态?球队是否确保除非明确更改,否则顺序将被保留?没有,也没有球员是否会根据他们在球队中的顺序被纳入/淘汰?不

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

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