13

背景:我给我的同事发了一封电子邮件,告诉他们Enumerable.Empty<T>()作为一种返回空集合的方法,而不做类似return new List<T>();我收到回复说缺点是它不公开特定类型的事情:

这确实提出了一个小问题。尽可能具体地说明您的返回类型总是好的*(因此,如果您要返回 List<>,请将其设为您的返回类型);那是因为像列表和数组这样的东西有额外的有用的方法。此外,在使用来自调用方法的集合时,它还有助于考虑性能。

不幸的是,下面的技巧迫使您返回一个 IEnumerable,它尽可能地不具体,对吧?:(

* 这实际上来自 .NET 设计指南。我相信,指南中陈述的原因与我在这里提到的相同。

这似乎与我所学到的完全相反,并且尽我所能尝试,我在设计指南中找不到这个确切的建议。我确实找到了这样的一小块:

一定要返回一个非常常用的方法和属性的子Collection<T>类。ReadOnlyConnection<T>

后面有一个代码片段,但根本没有更多理由。


话虽如此,这是一个真实且被接受的指导方针(第一个块引用中描述的方式)吗?还是被误解了?我能找到的所有其他 SO 问题都有首选IEnumerable<T>作为返回类型的答案。也许最初的 .NET 指南已经过时了?

或者,也许它不是那么清楚?是否有一些权衡需要考虑?什么时候返回更具体的类型是个好主意?是否曾建议返回一个具体的泛型类型,或者只返回一个更具体的接口,如IList<T>and ReadOnlyCollection<T>

4

4 回答 4

12

两种风格都有原因:

  1. 具体一点:您向调用者保证您将始终返回此类型。即使您的实施发生变化。你能信守诺言吗?如果是,请具体说明让来电者受益。更多派生类型具有更多特性。
  2. 通用:如果您可能更改相关方法或属性的底层实现,则不能承诺List<T>. 也许您可以承诺一个IList<T>或自定义集合类(您可以稍后更改)。

.NET 框架 BCL 与数组或自定义集合类一起使用。他们需要提供 100% 稳定的 API,所以他们需要小心。

在普通软件项目中,您可以更改调用者(.NET 框架的人不能),因此您可以更加宽容。如果你承诺太多,并且需要在一年后改变它,你可以做到(付出一些努力)。

只有一件事总是错误的:说一个人应该总是做(1)或(2)。总是总是错的。

下面是一个例子,说明特异性显然是正确的选择:

    public static T[] Slice<T>(this T[] list, int start, int count)
    {
        var result = new T[count];
        Array.Copy(list, start, result, 0, count);
        return result;
    }

只有一种合理的实现可能,所以我们可以明确地保证返回类型是一个数组。我们永远不需要返回列表。

另一方面,从某个持久存储中返回所有用户的方法可能会在内部发生很大变化。数据甚至可能比可用内存大,这需要流式传输。我们大概应该选择IEnumerable<User>.

于 2012-06-26T17:10:34.017 回答
4

围绕此类决策的指导方针的问题在于,我们的开发人员很容易盲目地遵循它们,不加思索。我认为应该逐案考虑这些问题,权衡每种情况下不同可能方法的优缺点。

对于返回类型,实际上(至少)有两种看待它的方式。我在 Stack Overflow 上看到的一种哲学通常更喜欢选择更通用的类型,以便为(代码的作者)提供更多的灵活性。例如,如果您编写了一个返回 an 的方法IList<T>并且返回值实际上是 a List<T>,那么您保留稍后实现您自己的优化数据结构的权利,该结构IList<T>在未来的版本中实现并返回它。

一个不同但在我看来同样有效的替代哲学是返回更多通用类型只会使您的库变得不那么有用,因此您应该返回更具体的类型。例如,一直积极返回IEnumerable<T>将导致您的库的最终用户不断调用ToArray(),ToList()等的情况。我相信这是您的同事在他写道“像列表和数组这样的东西有额外的方法时所提出的基本观点很有用。”

我有时会听到后一种方法提倡的措辞类似于“提供的内容要具体,接受的内容要通用”。换句话说,采用通用参数但提供特定的返回值。不管你是否同意这个原则,至少很容易看出它背后的原因。

无论如何,正如我所说,每个案例的细节都会有所不同。例如,在我的第一个IList<T>vs示例中List<T>,您可能已经计划实现一个自定义数据结构,只是还没有编写它。在这种情况下,返回 aList<T>可能是一个糟糕的选择,因为您已经知道界面会在几周或几个月内有所不同。

另一方面,您可能有一些方法总是返回 a T[],您选择仅将其公开为 a ,IEnumerable<T>即使此实现发生变化的可能性非常低。在这种情况下,选择更通用的类型可能在美学上更令人愉悦,但它实际上不可能使任何人受益,实际上剥夺了一个非常有用的功能的结果:随机访问。

因此,这始终是一种权衡,我认为您可以遵循的最佳指导方针是始终考虑您的选择并根据您的判断为相关案例选择最明智的选择。

于 2012-06-26T17:22:25.967 回答
2

返回 List 而不是 IEnumerable(或至少 IList)本质上违反了Liskov 替换原则

返回“List”将您锁定为需要返回该类型,并阻止您将方法的内部重构为可能更有效的实现(您承诺返回 List,但现在您想使用“yield”——哎呀,可以'不要在没有外部代码搅动的情况下这样做)。

许多(大多数?)情况下,这些方法的主要要求是迭代结果并做一些事情...... IEnumerable 完全适合这些用途。

如果用户需要一个可变集,这可以通过使用“ToList()”或另一端的 IEnumerable 上的其他扩展来轻松完成。

希望您公开直接突变的内部列表的代码也可能表明您也违反了封装......当心。我们发现在许多函数式编程语言中发现的越来越多的不变性可能是您代码中非常有价值的品质。

于 2013-05-14T18:59:41.933 回答
0

IMO 重要的是要记住这一点List<T>IEnumerable<T>具有不同的语义。

返回IEnumerable<T>只是给调用者一个可以枚举的东西序列(并且可能变成一个集合)。但是,不能保证序列已经实现。所以返回IEnumerable<T>支持延迟执行。此外,它是隐含的“只读”。

返回List<T>意味着这里是一个集合。当方法返回时,所有工作都已完成。此外, 的接口还List<T>允许调用者立即修改集合。

于 2012-06-26T17:21:49.367 回答