返回类型为 IEnumerable<T>

30
使用 IEnumerable<T> 作为返回类型是否存在问题? FxCop会警告返回 List<T>(建议返回 Collection<T>)。
我一直遵循一个准则:“接收最少,但返回最多”。
从这个角度来看,返回 IEnumerable<T> 是不好的,但是当我想要使用“惰性检索”时该怎么办?此外,yield 关键字非常棒。
11个回答

33

这实际上是一个由两个部分组成的问题。

1)返回一个 IEnumerable<T> 是否本质上存在任何问题?

没有任何问题。如果您正在使用 C# 迭代器,那么这就是预期行为。预先将其转换为 List<T> 或另一种集合类并不是一个好主意。这样做是对调用者的使用模式进行假设。我发现假设调用者的任何内容都不是个好主意。他们可能有想要 IEnumerable<T> 的充分理由。也许他们想将它转换为完全不同的集合层次结构(在这种情况下,转换为 List 就浪费了)。

2)是否有任何情况,其中返回其他东西可能更优?

是的。虽然假设调用者的很多内容都不是个好主意,但根据自己的行为来做决策是完全可以的。想象一下这样的情况:您有一个多线程对象,该对象将请求排队到一个不断更新的对象中。在这种情况下,返回原始的 IEnumerable<T> 是不负责任的。一旦修改了集合,枚举就无效了,并导致出现异常。相反,您可以快照该结构并返回该值。比如以 List<T> 的形式。在这种情况下,我只会返回对象作为直接结构(或接口)。

但这显然是更罕见的情况。


正如 OP 所指出的那样,如果需要返回一个具体类,FxCop 建议返回 Collection<T> 而不是 List<T>。另请参见 Collection<T> versus List<T> what should you use on your interfaces? - DavidRR

29

不,IEnumerable<T>在这里是一种好的返回类型,因为你所承诺的只是“一系列(有类型)值”。非常适合LINQ等操作,并且完全可用。

调用方可以很容易地将这些数据放入列表(或其他类型的集合)中 - 特别是使用LINQ(如 ToListToArray等)。

这种方法允许您惰性地回溯值,而无需缓存所有数据。绝对是一个好主意。我最近也写了另一个有用的 IEnumerable<T>窍门


1
有没有关于 FxCop 抱怨的想法? - annakata
它是否抱怨返回IEnumerable<T>?如果是,那就不要全信... 它并不总是正确的。 - Marc Gravell
3
如果这促使你思考你到底想要返回什么,那么FxCop已经完成了它的工作;-p - Marc Gravell
1
我相当确定它只会抱怨List<T>,而不是IEnumerable<T>。除非我在某个地方将其排除在外,否则它不会抱怨返回IEnumerable<T>的代码。 - Beardo

5

IEnumerable对我来说还可以,但它有一些缺点。客户端必须枚举才能获得结果。它没有办法检查Count等。 List很糟糕,因为你暴露了太多的控制权;客户端可以从中添加/删除等,这可能是一个坏事。 Collection似乎是最好的折衷方案,至少在FxCop的看法中是这样的。 我总是根据情况使用合适的方法(例如,如果我想返回只读集合,我将公开集合作为返回类型并返回List.AsReadOnly()或IEnumerable用于通过yield进行惰性评估等)。需要具体问题具体分析。


1
客户端可以使用ToList(),ToArray(),Count()或任何它想要的方法。 - Mauricio Scheffer
1
但是这些方法创建了新的对象。任何修改都不会反映在原始列表对象上。 - Adrian Zanescu
好观点!逐个案例的方式似乎是最好的。虽然我更喜欢IEnumerable方法,但它更偏向于不可变性。 - Mauricio Scheffer

4

如果你真的只是返回枚举,并且调用者会按照此方式使用,那么返回IEnumerable<T>是可以的。

但正如其他人指出的那样,它的缺点是如果调用者需要任何其他信息(例如Count),他可能需要枚举。 .NET 3.5扩展方法IEnumerable<T>.Count将在后台枚举,如果返回值未实现ICollection<T>,这可能不可取。

当结果是一个集合时,我经常返回IList<T>或ICollection<T> - 在内部,您的方法可以使用List<T>,并将其原样返回,或者使用List<T>.AsReadOnly返回,以防止修改(例如,如果您在内部缓存列表)。据我所知,FxCop对这两种情况都很满意。


4
关于你的原则:“尽可能少地接受,但最大程度地回报”。
管理大型程序复杂性的关键是一种称为“信息隐藏”的技术。如果您的方法通过构建一个List来工作,通常不必通过返回该类型来揭示这个事实。如果您这样做,那么您的调用者可能会修改他们得到的列表。这将使您无法进行缓存或使用yield return进行惰性迭代。
因此,一个更好的原则是一个函数应该遵循的原则是:“尽可能少地透露有关您如何工作的信息”。

3
“尽可能少地接受,但最大程度地返回”是我的主张。当一个方法返回一个对象时,我们有什么理由不返回实际类型并通过返回基本类型来限制对象的功能。然而,这引发了一个问题:当我们设计接口时,如何知道“最大”(实际类型)将是什么。答案非常简单。只有在接口设计人员设计一个开放接口,该接口将在应用程序/组件之外实现时,他们才不会知道实际返回类型可能是什么。聪明的设计师应该始终考虑方法应该做什么以及最佳/通用返回类型应该是什么。
例如,如果我正在设计一个接口来检索对象的向量,并且我知道返回的对象数量将是可变的,我总是假设聪明的开发人员将始终使用List。如果有人计划返回一个数组,我会质疑他的能力,除非他/她只是从他/她不拥有的另一层返回数据。这可能就是为什么FxCop倡导ICollection(List和Array的共同基础)。
上述内容说完了,还有几件事要考虑:
  • 返回的数据是否可变或不可变

  • 返回的数据是否可以在多个调用者之间共享

关于LINQ的惰性评估,我相信95%以上的C#用户都不理解其中的细节。这非常不符合面向对象的思想。面向对象推崇方法调用上的具体状态变化。LINQ的惰性评估则促进了运行时状态的变化,这是基于表达式评估模式的(并不是所有非高级用户都会遵循)。

2

一个重要的方面是,当你返回一个 List<T> 时,实际上你返回的是一个引用。这使得调用者可以操纵你的列表。这是一个常见的问题,例如,业务层向GUI层返回一个List<T>


但是,如果你返回的不是值类型,那么你难道不会每次都返回一个引用吗? - David Klempfner

1

IEnumerable很酷,因为您可以使用yield迭代器,仅向消费者提供所需的数据,但构造中隐藏了成本。

让我用一个例子来解释一下。假设我正在使用此方法:

IEnumerable GetFilesFromFolder(string path)

那么我得到了什么?要获取文件夹中的所有文件,我必须遍历枚举,这很好,毕竟这就是枚举的工作方式,但是如果由于任何原因,我必须两次枚举它怎么办?

第二次,我应该期望刷新的结果还是结果是幂等的?我不知道。我必须检查库/方法的文档。

消费者对枚举的GetEnumerator方法的调用实际上可能会在幕后执行I/O操作、http调用或仅迭代内部数组,我无法确定。我必须检查文档,希望这种行为已经记录。

这个细节重要吗?我认为很重要。至少从性能的角度来看。

即使迭代的成本较慢且受 CPU 限制,这也不为零,并且在枚举链的情况下可能会变得更糟,这通常会使调试会话变得非常困难。

我喜欢让我的库的使用者没有疑虑,所以只要我知道我的 API 返回少量元素,我总是使用数组作为返回类型,只有当要返回的数据非常庞大时,才会使用 IEnumerable 或 IAsyncEnumerable。

无论如何,如果您想返回枚举,请记录您的 API,告诉使用者结果是否为快照。


0

我无法接受所选择的答案。虽然有处理所描述情况的方法,但使用列表或其他任何东西都不是其中之一。一旦返回IEnumerable,您必须假设调用者可能会进行foreach。在这种情况下,具体类型是List还是spaghetti并不重要。事实上,仅索引就是一个问题,特别是如果删除了项目。

任何返回的值都是快照。它可能是IEnumerable的当前内容,如果它被缓存,则应该是缓存副本的克隆;如果它应该更具动态性(例如SQL查询的结果),则使用yield return;但是,在多线程世界中,允许容器随意变异并提供Count和索引器等方法是灾难的配方。我甚至还没有涉及到调用者能够在您的代码控制的容器上调用Add或Delete的能力。

此外,返回具体类型将锁定您的实现。今天内部可能正在使用列表。明天,也许您会变得多线程,并希望使用线程安全容器、数组、队列或字典的Values集合或Linq查询的输出。如果您将自己锁定为具体的返回类型,那么您必须更改大量代码或在返回之前进行转换。


0

仅仅因为你说你返回IEnumerable并不意味着你不能返回List。这个想法是为了减少不必要的耦合。调用者所关心的只是获取一系列东西的列表,而不是用于包含该列表的集合的确切类型。如果你有一个由数组支持的东西,那么获取像Count这样的东西将会很快。


4
如果调用者希望得到一个列表,那么IList[<T>]是一个不错的选择。 - Marc Gravell

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