用于公开列表成员的 IEnumablevsIReadonlyCollection vsReadonlyCollection

我花了好几个小时思考曝光名单成员的问题。在一个与我类似的问题中,Jon Skeet 给出了一个非常好的答案。请随便看看。

用于公开成员集合的 ReadOnlyCollection 或 IEnumable?

我通常非常偏执于公开列表,特别是如果您正在开发 API 的话。

我一直使用 IEnumable 来公开列表,因为它非常安全,并且提供了很大的灵活性。让我举个例子:

public class Activity
{
private readonly IList<WorkItem> workItems = new List<WorkItem>();


public string Name { get; set; }


public IEnumerable<WorkItem> WorkItems
{
get
{
return this.workItems;
}
}


public void AddWorkItem(WorkItem workItem)
{
this.workItems.Add(workItem);
}
}

任何针对 IEnumable 进行编码的人在这里都是相当安全的。如果我以后决定使用一个有序列表或其他东西,他们的代码都没有中断,这仍然是很好的。这样做的缺点是 IEnumable 可以被强制转换回此类之外的列表。

出于这个原因,许多开发人员使用 ReadOnlyCollection 来公开成员。这是相当安全的,因为它永远不会被抛回到列表中。对我来说,我更喜欢 IEnumable,因为它提供了更多的灵活性,如果我想实现一些不同于列表的东西。

我想到了一个我更喜欢的新点子。使用 IReadOnlyCollection:

public class Activity
{
private readonly IList<WorkItem> workItems = new List<WorkItem>();


public string Name { get; set; }


public IReadOnlyCollection<WorkItem> WorkItems
{
get
{
return new ReadOnlyCollection<WorkItem>(this.workItems);
}
}


public void AddWorkItem(WorkItem workItem)
{
this.workItems.Add(workItem);
}
}

我觉得这保留了 IEnumable 的一些灵活性,并且封装得很好。

我发布这个问题是为了得到一些关于我的想法的意见。与 IEnumable 相比,您更喜欢此解决方案吗?您认为使用 ReadOnlyCollection 的具体返回值更好吗?这是一场激烈的辩论,我想试着看看我们都能想出哪些利弊。

提前谢谢你的意见。

剪辑

首先感谢大家对这里的讨论作出的贡献。我确实从每一个人身上学到了很多,我真诚地感谢你们。

我正在添加一些额外的情况和信息。

IReadOnlyCollection 和 IEnumable 有一些常见的缺陷。

考虑下面的例子:

public IReadOnlyCollection<WorkItem> WorkItems
{
get
{
return this.workItems;
}
}

即使接口是只读的,也可以将上面的示例强制转换回一个列表并进行修改。该接口,尽管与其同名,但并不保证不变性。由您来提供一个不可变的解决方案,因此您应该返回一个新的 ReadOnlyCollection。通过创建一个新列表(本质上是一个副本) ,对象的状态是安全可靠的。

里奇班在他的评论中说得最好: 一个界面只能保证某些东西能做什么,而不是它不能做什么。

下面是一个例子:

public IEnumerable<WorkItem> WorkItems
{
get
{
return new List<WorkItem>(this.workItems);
}
}

以上可以施放和变异,但你的对象仍然是不可变的。

另一个框外语句是集合类:

public class Bar : IEnumerable<string>
{
private List<string> foo;


public Bar()
{
this.foo = new List<string> { "123", "456" };
}


public IEnumerator<string> GetEnumerator()
{
return this.foo.GetEnumerator();
}


IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}

上面的类可以具有按照您希望的方式对 foo 进行变异的方法,但是您的对象永远不能被强制转换为任何排序的列表并进行变异。

Carsten Fuhrmann 在 IEnumerables 中提出了一个关于收益率回报表的奇妙观点。

再次感谢大家。

61876 次浏览

It seems that you can just return an appropriate interface:

...
private readonly List<WorkItem> workItems = new List<WorkItem>();


// Usually, there's no need the property to be virtual
public virtual IReadOnlyList<WorkItem> WorkItems {
get {
return workItems;
}
}
...

Since workItems field is in fact List<T> so the natural idea IMHO is to expose the most wide interface which is IReadOnlyList<T> in the case

Talking about class libraries, I think IReadOnly* is really useful, and I think you're doing it right :)

It's all about immutable collection... Before there were just immutables and to enlarge arrays was a huge task, so .net decided to include in the framework something different, mutable collection, that implement the ugly stuff for you, but IMHO they didn't give you a proper direction for immutable that are extremely useful, especially in a high concurrency scenario where sharing mutable stuff is always a PITA.

If you check other today languages, such as objective-c, you will see that in fact the rules are completely inverted! They quite always exchange immutable collection between different classes, in other words the interface expose just immutable, and internally they use mutable collection (yes, they have it of course), instead they expose proper methods if they want let the outsiders change the collection (if the class is a stateful class).

So this little experience that I've got with other languages pushes me to think that .net list are so powerful, but the immutable collection were there for some reason :)

In this case is not a matter of helping the caller of an interface, to avoid him to change all the code if you're changing internal implementation, like it is with IList vs List, but with IReadOnly* you're protecting yourself, your class, to being used in not a proper way, to avoid useless protection code, code that sometimes you couldn't also write (in the past in some piece of code I had to return a clone of the complete list to avoid this problem).

One important aspect seems to be missing from the answers so far:

When an IEnumerable<T> is returned to the caller, they must consider the possibility that the returned object is a "lazy stream", e.g. a collection built with "yield return". That is, the performance penalty for producing the elements of the IEnumerable<T> may have to be paid by the caller, for each use of the IEnumerable. (The productivity tool "Resharper" actually points this out as a code smell.)

By contrast, an IReadOnlyCollection<T> signals to the caller that there will be no lazy evaluation. (The Count property, as opposed to the Count extension method of IEnumerable<T> (which is inherited by IReadOnlyCollection<T> so it has the method as well), signals non-lazyness. And so does the fact that there seem to be no lazy implementations of IReadOnlyCollection.)

This is also valid for input parameters, as requesting an IReadOnlyCollection<T> instead of IEnumerable<T> signals that the method needs to iterate several times over the collection. Sure the method could create its own list from the IEnumerable<T> and iterate over that, but as the caller may already have a loaded collection at hand it would make sense to take advantage of it whenever possible. If the caller only has an IEnumerable<T> at hand, he only needs to add .ToArray() or .ToList() to the parameter.

What IReadOnlyCollection does not do is prevent the caller to cast to some other collection type. For such protection, one would have to use the class ReadOnlyCollection<T>.

In summary, the only thing IReadOnlyCollection<T> does relative to IEnumerable<T> is add a Count property and thus signal that no lazyness is involved.

!! IEnumerable vs IReadOnlyList !!

IEnumerable has been with us from the beginning of time. For many years, it was a de facto standard way to represent a read-only collection. Since .NET 4.5, however, there is another way to do that: IReadOnlyList.

Both collection interfaces are useful.

<>

My take on concerns of casting and IReadOnly* contracts, and 'proper' usages of such.

If some code is being “clever” enough to perform an explicit cast and break the interface contract, then it is also “clever” enough to use reflection or otherwise do nefarious things such as access the underlying List of a ReadOnlyCollection wrapper object. I don’t program against such “clever” programmers.

The only thing that I guarantee is that after said IReadOnly*-interface objects are exposed, then my code will not violate that contract and will not modified the returned collection object.

This means that I write code that returns List-as-IReadOnly*, eg., and rarely opt for an actual read-only concrete type or wrapper. Using IEnumerable.ToList is sufficient to return an IReadOnly[List|Collection] - calling List.AsReadOnly adds little value against “clever” programmers who can still access the underlying list that the ReadOnlyCollection wraps.

In all cases, I guarantee that the concrete types of IReadOnly* return values are eager. If I ever write a method that returns an IEnumerable, it is specifically because the contract of the method is that which “supports streaming” fsvo.

As far as IReadOnlyList and IReadOnlyCollection, I use the former when there is 'an' implied stable ordering established that is meaningful to index, regardless of purposeful sorting. For example, arrays and Lists can be returned as an IReadOnlyList while a HashSet would better be returned as an IReadOnlyCollection. The caller can always assign the I[ReadOnly]List to an I[ReadOnly]Collection as desired: this choice is about the contract exposed and not what a programmer, “clever” or otherwise, will do.