IEnumerable<T>
作为返回类型是否存在问题?
FxCop会警告返回 List<T>
(建议返回 Collection<T>
)。我一直遵循一个准则:“接收最少,但返回最多”。
从这个角度来看,返回
IEnumerable<T>
是不好的,但是当我想要使用“惰性检索”时该怎么办?此外,yield
关键字非常棒。IEnumerable<T>
作为返回类型是否存在问题?
FxCop会警告返回 List<T>
(建议返回 Collection<T>
)。IEnumerable<T>
是不好的,但是当我想要使用“惰性检索”时该怎么办?此外,yield
关键字非常棒。这实际上是一个由两个部分组成的问题。
1)返回一个 IEnumerable<T> 是否本质上存在任何问题?
没有任何问题。如果您正在使用 C# 迭代器,那么这就是预期行为。预先将其转换为 List<T> 或另一种集合类并不是一个好主意。这样做是对调用者的使用模式进行假设。我发现假设调用者的任何内容都不是个好主意。他们可能有想要 IEnumerable<T> 的充分理由。也许他们想将它转换为完全不同的集合层次结构(在这种情况下,转换为 List 就浪费了)。
2)是否有任何情况,其中返回其他东西可能更优?
是的。虽然假设调用者的很多内容都不是个好主意,但根据自己的行为来做决策是完全可以的。想象一下这样的情况:您有一个多线程对象,该对象将请求排队到一个不断更新的对象中。在这种情况下,返回原始的 IEnumerable<T> 是不负责任的。一旦修改了集合,枚举就无效了,并导致出现异常。相反,您可以快照该结构并返回该值。比如以 List<T> 的形式。在这种情况下,我只会返回对象作为直接结构(或接口)。
但这显然是更罕见的情况。
不,IEnumerable<T>
在这里是一种好的返回类型,因为你所承诺的只是“一系列(有类型)值”。非常适合LINQ等操作,并且完全可用。
调用方可以很容易地将这些数据放入列表(或其他类型的集合)中 - 特别是使用LINQ(如 ToList
,ToArray
等)。
这种方法允许您惰性地回溯值,而无需缓存所有数据。绝对是一个好主意。我最近也写了另一个有用的 IEnumerable<T>
窍门。
IEnumerable对我来说还可以,但它有一些缺点。客户端必须枚举才能获得结果。它没有办法检查Count等。 List很糟糕,因为你暴露了太多的控制权;客户端可以从中添加/删除等,这可能是一个坏事。 Collection似乎是最好的折衷方案,至少在FxCop的看法中是这样的。 我总是根据情况使用合适的方法(例如,如果我想返回只读集合,我将公开集合作为返回类型并返回List.AsReadOnly()或IEnumerable用于通过yield进行惰性评估等)。需要具体问题具体分析。
如果你真的只是返回枚举,并且调用者会按照此方式使用,那么返回IEnumerable<T>是可以的。
但正如其他人指出的那样,它的缺点是如果调用者需要任何其他信息(例如Count),他可能需要枚举。 .NET 3.5扩展方法IEnumerable<T>.Count将在后台枚举,如果返回值未实现ICollection<T>,这可能不可取。
当结果是一个集合时,我经常返回IList<T>或ICollection<T> - 在内部,您的方法可以使用List<T>,并将其原样返回,或者使用List<T>.AsReadOnly返回,以防止修改(例如,如果您在内部缓存列表)。据我所知,FxCop对这两种情况都很满意。
返回的数据是否可变或不可变
返回的数据是否可以在多个调用者之间共享
一个重要的方面是,当你返回一个 List<T>
时,实际上你返回的是一个引用。这使得调用者可以操纵你的列表。这是一个常见的问题,例如,业务层向GUI层返回一个List<T>
。
IEnumerable很酷,因为您可以使用yield迭代器,仅向消费者提供所需的数据,但构造中隐藏了成本。
让我用一个例子来解释一下。假设我正在使用此方法:
IEnumerable GetFilesFromFolder(string path)
那么我得到了什么?要获取文件夹中的所有文件,我必须遍历枚举,这很好,毕竟这就是枚举的工作方式,但是如果由于任何原因,我必须两次枚举它怎么办?
第二次,我应该期望刷新的结果还是结果是幂等的?我不知道。我必须检查库/方法的文档。
消费者对枚举的GetEnumerator方法的调用实际上可能会在幕后执行I/O操作、http调用或仅迭代内部数组,我无法确定。我必须检查文档,希望这种行为已经记录。
这个细节重要吗?我认为很重要。至少从性能的角度来看。
即使迭代的成本较慢且受 CPU 限制,这也不为零,并且在枚举链的情况下可能会变得更糟,这通常会使调试会话变得非常困难。
我喜欢让我的库的使用者没有疑虑,所以只要我知道我的 API 返回少量元素,我总是使用数组作为返回类型,只有当要返回的数据非常庞大时,才会使用 IEnumerable 或 IAsyncEnumerable。
无论如何,如果您想返回枚举,请记录您的 API,告诉使用者结果是否为快照。
我无法接受所选择的答案。虽然有处理所描述情况的方法,但使用列表或其他任何东西都不是其中之一。一旦返回IEnumerable,您必须假设调用者可能会进行foreach。在这种情况下,具体类型是List还是spaghetti并不重要。事实上,仅索引就是一个问题,特别是如果删除了项目。
任何返回的值都是快照。它可能是IEnumerable的当前内容,如果它被缓存,则应该是缓存副本的克隆;如果它应该更具动态性(例如SQL查询的结果),则使用yield return;但是,在多线程世界中,允许容器随意变异并提供Count和索引器等方法是灾难的配方。我甚至还没有涉及到调用者能够在您的代码控制的容器上调用Add或Delete的能力。
此外,返回具体类型将锁定您的实现。今天内部可能正在使用列表。明天,也许您会变得多线程,并希望使用线程安全容器、数组、队列或字典的Values集合或Linq查询的输出。如果您将自己锁定为具体的返回类型,那么您必须更改大量代码或在返回之前进行转换。
仅仅因为你说你返回IEnumerable并不意味着你不能返回List。这个想法是为了减少不必要的耦合。调用者所关心的只是获取一系列东西的列表,而不是用于包含该列表的集合的确切类型。如果你有一个由数组支持的东西,那么获取像Count这样的东西将会很快。
Collection<T>
而不是List<T>
。另请参见 Collection<T> versus List<T> what should you use on your interfaces?。 - DavidRR