29

IEnumerable<T>用作返回类型有问题吗?FxCop 抱怨返回List<T>(它建议返回Collection<T>)。

好吧,我一直遵循“接受最少,但返回最多”的规则。

从这个角度来看,返回IEnumerable<T>是一件坏事,但是当我想使用“懒惰检索”时应该怎么做呢?此外,yield关键字是如此的好。

4

11 回答 11

30

这真的是一个两部分的问题。

1) 返回 IEnumerable<T> 本质上是否有任何问题

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

2) 在任何情况下,返回 IEnumerable<T> 以外的东西可能更可取?

是的。虽然对您的来电者做出太多假设并不是一个好主意,但根据您自己的行为做出决定是完全可以的。想象一个场景,你有一个多线程对象,它将请求排队到一个不断更新的对象中。在这种情况下,返回原始 IEnumerable<T> 是不负责任的。一旦集合被修改,可枚举就会失效,并会导致执行。相反,您可以拍摄结构的快照并返回该值。以 List<T> 形式说。在这种情况下,我只会将对象作为直接结构(或接口)返回。

不过,这当然是少见的情况。

于 2008-12-19T15:39:27.977 回答
28

不,回到这里IEnumerable<T>是件好事,因为您所承诺的只是“一系列(类型化的)值”。LINQ 等的理想选择,并且完全可用。

调用者可以轻松地将这些数据放入列表(或其他任何内容)中 - 尤其是使用 LINQ(ToListToArray等)。

这种方法允许您延迟后台处理值,而不必缓冲所有数据。绝对是个好东西。前几天我也写了另一个有用的IEnumerable<T>技巧

于 2008-12-19T15:06:29.070 回答
5

IEnumerable 对我来说很好,但它有一些缺点。客户端必须枚举才能得到结果。它没有办法检查 Count 等。 List 不好,因为你暴露了太多的控制;客户可以从中添加/删除等,这可能是一件坏事。收藏似乎是最好的妥协,至少在 FxCop 看来。我总是使用在我的上下文中看起来合适的东西(例如,如果我想返回一个只读集合,我将集合公开为返回类型并返回 List.AsReadOnly() 或 IEnumerable 以通过 yield 等进行惰性评估)。视情况而定

于 2008-12-19T15:09:50.593 回答
4

关于您的原则:“接受最少,但返回最大”。

管理大型程序复杂性的关键是一种称为信息隐藏的技术。如果您的方法通过构建 a来工作List<T>,则通常不需要通过返回该类型来揭示这一事实。如果你这样做了,那么你的调用者可能会修改他们返回的列表。这消除了您使用yield return.

所以一个更好的原则是一个函数要遵循的是:“尽可能少地透露你是如何工作的”。

于 2008-12-19T19:42:10.763 回答
3

如果您真的只返回一个枚举,则返回 IEnumerable<T> 是可以的,并且它将被您的调用者这样使用。

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

当结果是集合时,我经常返回 IList<T> 或 ICollection<T> - 在内部,您的方法可以使用 List<T> 并按原样返回它,或者如果要保护,则返回 List<T>.AsReadOnly反对修改(例如,如果您在内部缓存列表)。AFAIK FxCop 对其中任何一个都非常满意。

于 2008-12-19T16:00:49.197 回答
3

“接受最少,但回报最多”是我所倡导的。当一个方法返回一个对象时,我们有什么理由不返回实际类型并通过返回基类型来限制对象的功能。然而,这提出了一个问题,当我们设计一个界面时,我们如何知道“最大”(实际类型)是什么。答案很简单。只有在接口设计者正在设计一个开放接口(将在应用程序/组件之外实现)的极端情况下,他们才会知道实际的返回类型可能是什么。聪明的设计师应该始终考虑该方法应该做什么以及最佳/通用返回类型应该是什么。

例如,如果我正在设计一个接口来检索对象向量,并且我知道返回对象的数量是可变的,那么我将始终假设聪明的开发人员将始终使用列表。如果有人计划返回一个数组,我会质疑他的能力,除非他/她只是从他/她不拥有的另一层返回数据。这可能就是 FxCop 提倡 ICollection(List 和 Array 的共同基础)的原因。

综上所述,还有一些其他的事情需要考虑

  • 如果返回的数据应该是可变的还是不可变的

  • 如果返回的数据在多个调用者之间共享

关于 LINQ 惰性评估,我敢肯定 95% 以上的 C# 用户不了解其中的内容。它是如此的非 oo-ish。OO 促进方法调用的具体状态更改。LINQ 惰性求值促进表达式求值模式的运行时状态更改(非高级用户总是遵循的)。

于 2011-12-06T15:36:49.670 回答
2

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

于 2010-11-19T12:48:05.097 回答
1

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

于 2008-12-19T15:21:21.133 回答
0

我认为您自己的指导很棒——如果您能够更具体地说明您返回的内容而不会影响性能(例如,您不必根据您的结果构建一个列表),那么就这样做。但是,如果您的函数合法地不知道它将找到什么类型,例如在某些情况下您将使用 List 并且在某些情况下使用 Array 等,那么返回 IEnumerable 是您可以做的“最好的” . 将其视为您可能想要返回的所有内容的“最大公倍数”。

于 2008-12-19T15:45:38.587 回答
0

我不能接受选择的答案。有一些方法可以处理所描述的场景,但使用 List 或您使用的任何其他方法都不是其中之一。返回 IEnumerable 的那一刻,您必须假设调用者可能会执行 foreach。在这种情况下,具体类型是 List 还是 spaghetti 并不重要。实际上,仅索引是一个问题,尤其是在删除项目时。

任何返回值都是快照。它可能是 IEnumerable 的当前内容,在这种情况下,如果它被缓存,它应该是缓存副本的克隆;如果它应该更动态(如 sql 查询的结果),则使用 yield return;然而,允许容器随意变异并提供诸如 Count 和 indexer 之类的方法是多线程世界中灾难的根源。我什至还没有了解调用者在你的代码应该控制的容器上调用 Add 或 Delete 的能力。

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

于 2014-01-15T08:05:03.160 回答
0

IEnumerable很酷,因为您可以使用 yield 迭代器,它只为消费者提供他们需要的数据,但结构中隐藏着成本。

让我用一个例子来解释它。假设我正在使用这种方法:

IEnumerable GetFilesFromFolder(字符串路径)

那么,我得到了什么?要获取我文件夹的所有文件,我必须迭代枚举,这很好,毕竟枚举就是这样工作的,但是如果出于任何原因我必须枚举它两次呢?

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

消费者对枚举的GetEnumerator方法的调用,其实可以在后台执行一个I/O操作,或者是一个http调用,或者它可以简单地迭代一个内部数组,我不能确定. 我必须检查文档以希望记录此行为。

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

即使迭代的成本很慢且 CPU 受限,这也不是零,而且在枚举链的情况下可能会变得更糟,这通常会使调试会话变成一场噩梦。

我不想让我的库的使用者怀疑,所以每当我知道我的 API 返回的元素很少时,我总是使用数组作为返回类型,并且只有当要返回的数据很大时,我才使用 IEnumerable 或 IAsyncEnumerable。

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

于 2022-02-08T10:02:04.150 回答