Collection<T>与List<T>:在接口中应该使用哪个?

186

代码如下:

namespace Test
{
    public interface IMyClass
    {
        List<IMyClass> GetList();
    }

    public class MyClass : IMyClass
    {
        public List<IMyClass> GetList()
        {
            return new List<IMyClass>();
        }
    }
}

当我运行代码分析时,我得到了以下的建议。

警告 3 CA1002 : Microsoft.Design : 在 'IMyClass.GetList()' 中将 'List' 更改为 Collection、ReadOnlyCollection 或 KeyedCollection

我应该如何解决这个问题?这里有什么好的做法吗?

8个回答

268
为了回答为什么不使用 List<T> 的问题,原因是未来的可扩展性和 API 简单性。 未来的可扩展性 List<T> 不是设计成易于通过子类扩展的;它被设计为在内部实现中快速使用。你会注意到它上面的方法不是虚拟的,因此无法重写,而且也没有钩子进入其 Add/Insert/Remove 操作。
这意味着,如果您将来需要更改集合的行为(例如拒绝人们尝试添加的空对象,或在发生这种情况时执行其他工作,如更新类状态),则需要更改您返回的集合类型以使其可以进行子类化,这将导致破坏接口的变更(当然,更改不允许使用 null 等语义也可能是一种接口变更,但更新内部类状态等操作则不是)。通过返回一个易于子类化的类,比如 Collection<T> 或者像 IList<T>, ICollection<T>, IEnumerable<T> 这样的接口, 你可以更改内部实现以满足不同的集合类型需求,而不会破坏API使用者的代码,因为仍然可以作为期望的类型返回。 API的简洁性 List<T> 包含许多有用的操作,例如 BinarySearch, Sort等。但是如果这是你要公开的集合,那么你很可能控制列表的语义,而不是使用者。因此,虽然你的类在内部需要这些操作,但是使用者几乎不太可能(或甚至不应该)调用它们。
因此,通过提供一个更简单的集合类或接口,可以减少API用户看到的成员数量,并使其更易于使用。

1
这个回答非常准确。关于此主题的另一个好文章可以在这里找到:https://web.archive.org/web/20090608080454/http://blogs.msdn.com/fxcop/archive/2006/04/27/faq-why-does-donotexposegenericlists-recommend-that-i-expose-collection-lt-t-gt-instead-of-list-lt-t-gt-david-kean.aspx - senfo
7
我理解您的第一点,但我不确定我是否同意您关于API简化部分的观点。 - Boris Callens
1
这里有一篇非常好的帖子,讲述了集合(Collections)和列表(Lists)之间的区别。https://dev59.com/JHRC5IYBdhLWcg3wJNgN#398988 - El Mac
1
工作链接:https://blogs.msdn.microsoft.com/kcwalina/2005/09/26/why-we-dont-recommend-using-listt-in-public-apis/我们为什么不建议在公共API中使用List<T>? - ofthelit
我看到下面Skeet本人倡导接口(至少在写作时)。但是我见过很多强硬的支持者认为返回类型应尽可能具体和具体。你的立场是否也是只有在真正看到可能返回不同列表类型的具体情况时才使用接口?或者您是否认为有理由默认使用接口? - Base

60

10
@Jon:我知道这是旧的,但你能否评论一下Krzysztof在http://blogs.msdn.com/b/kcwalina/archive/2005/09/26/474010.aspx中所说的内容?具体来说,他的评论是:“我们建议对于输出和属性使用Collection<T>、ReadOnlyCollection<T>或KeyedCollection<TKey,TItem>,对于输入使用接口IEnumerable<T>、ICollection<T>、IList<T>。”CA1002似乎支持Krzysztof的意见。我无法想象为什么会推荐使用具体的集合而不是接口,并且为什么要区分输入/输出。 - Nelson Rothermel
3
@Nelson:要求调用者传入不可变列表是很少见的,但返回一个确实是不可变的列表让调用者知道它是不可变的是合理的。不确定其他集合是否也是如此。希望能有更多细节。 - Jon Skeet
2
这不是针对特定情况的。显然,通常情况下,ReadOnlyCollection<T> 对于输入来说没有意义。同样,将 IList<T> 作为输入表示,“我需要 Sort() 或其他 IList 具有的成员”,这对于输出来说是没有意义的。但我的意思是,为什么建议将 ICollection<T> 作为输入,而将 Collection<T> 作为输出呢?为什么不像你建议的那样,也将 ICollection<T> 用作输出呢? - Nelson Rothermel
1
@Nelson:说实话,我不知道。从Krzysztof那里获取更多信息会很有帮助。 - Jon Skeet
11
我认为这与"不含歧义"有关。Collection<T>ReadOnlyCollection<T> 都是从 ICollection<T> 派生的(即没有 IReadOnlyCollection<T>)。如果你返回接口,那么不清楚它是哪个集合类型以及是否可以被修改。无论如何,感谢你的意见,这对我来说是一个很好的确认。 - Nelson Rothermel
显示剩余2条评论

8
我认为还没有人回答“为什么”的问题......所以我来解释一下。使用Collection<T>而不是List<T>的原因是,如果您公开了一个List<T>,那么任何获得对您对象访问权限的人都可以修改列表中的项目。而Collection<T>应表示您正在创建自己的“Add”、“Remove”等方法。

您可能不需要担心它,因为您可能仅为自己(或几个同事)编写界面。这里有另一个可能有意义的示例。

如果您有一个公共数组,例如:

public int[] MyIntegers { get; }

你可能会认为因为只有“get”访问器,所以没有人能够干扰值,但这并不是真的。任何人都可以像这样更改其中的值:
someObject.MyIngegers[3] = 12345;

就我个人而言,大多数情况下我都会使用List<T>。但是如果你正在设计一个类库并要将其提供给随机开发人员,并且需要依赖于对象的状态……那么你就需要创建自己的集合,并从那里进行锁定 :)


如果你将List<T>返回给客户端代码,那么当客户端代码修改集合时,你将永远无法接收到通知。- FX COP...另请参阅"http://msdn.microsoft.com/en-us/library/0fss9skc.aspx"...哇,看来我并没有离题 :) - Timothy Khouri
哇 - 多年后我发现我在上面的评论中粘贴的链接末尾有一个引号,所以它没有起作用... http://msdn.microsoft.com/en-us/library/0fss9skc.aspx - Timothy Khouri

3

这主要是关于将自己的实现抽象化,而不是直接暴露List对象进行操作。

让其他对象(或人)直接修改你的对象状态不是好的做法。考虑属性的getter/setter。

Collection -> 用于普通集合
ReadOnlyCollection -> 用于不应被修改的集合
KeyedCollection -> 当你想使用字典时。

如何解决取决于你想让你的类做什么以及GetList()方法的目的。你能详细说明吗?


1
但是Collection<T>和KeyedCollection<,>也不是只读的。 - nawfal
1
你说过不要让其他对象(或人)直接修改你的对象状态,并列出了3种集合类型。除了ReadOnlyCollection,其他两种都没有遵守这个规则。 - nawfal
这是指“良好的实践”。请提供适当的上下文。下面的列表仅说明此类类型的基本要求,因为OP想要了解警告。然后我继续询问他关于GetList()的目的,以便更正确地帮助他。 - chakrit
我绝不是说使用这3种类型会立即使你达到“良好实践”的位置。 - chakrit
1
好的,我理解了那部分。但是对我来说,推理部分仍然不合理。你说Collection<T>有助于抽象内部实现并防止直接操作内部列表。怎么做到的?Collection<T>只是一个包装器,操作传递的同一实例。它是用于继承的动态集合,仅此而已(Greg的答案在这里更相关)。 - nawfal
显示剩余2条评论

1

虽然这个问题已经问了很久,但我还是有一些补充。

当你的列表类型派生自List<T>而不是Collection<T>时,你无法实现Collection<T>实现的受保护虚拟方法。 这意味着你的派生类型不能响应于任何修改。这是因为List<T>假定你在添加或删除项目时已经知道。能够响应通知是一种额外负担,因此List<T>不提供它。

在外部代码可以访问您的集合的情况下,您可能无法控制项目何时被添加或删除。因此,Collection<T>提供了一种了解何时修改列表的方法。


1
在这种情况下,我通常尽量暴露最少量的实现。如果消费者不需要知道你实际上正在使用一个列表,那么你就不需要返回一个列表。通过像Microsoft建议的返回一个Collection,你可以隐藏你正在使用列表的事实,使你的类的消费者与内部变化隔离开来。

0

我认为像这样返回一些东西没有任何问题

this.InternalData.Filter(crteria).ToList();

如果我返回了一个已断开连接的内部数据的副本,或者是数据查询的分离结果 - 我可以安全地返回List<TItem>,而不暴露任何实现细节,并允许以方便的方式使用返回的数据。
但这取决于我期望的消费者类型 - 如果这是类似数据网格的东西,我更喜欢返回IEnumerable<TItem>,在大多数情况下,这将是复制的项目列表 :)

-1

Collection类实际上只是一个包装器类,用于隐藏其他集合的实现细节和其他特性。我认为这与面向对象语言中的属性隐藏编码模式有关。

我认为你不应该担心它,但如果你真的想让代码分析工具满意,只需按照以下步骤操作:

//using System.Collections.ObjectModel;

Collection<MyClass> myCollection = new Collection<MyClass>(myList);

1
抱歉,那是个打错字。我想说的是 Collection<MyClass>。顺便说一下,我真的很期待使用C# 4和泛型协变! - Tamas Czinege
据我所知,将Collection<T>包装起来既不能保护它本身,也不能保护底层集合。 - nawfal
那段代码是为了“取悦代码分析工具”。我不认为@TamasCzinege在任何地方说过使用Collection<T>会立即保护您的底层集合。 - chakrit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接