17

一次又一次地问自己什么是好的代码,我阅读了“接受最弱的,返回最强的”的建议。

对我来说,为什么要接受最弱的很明显:该方法声明与其客户的可能最弱的合同。因此,客户端不需要针对非常“强大”的界面进行“专业化”。

“最强归来”对我来说不是很清楚。为什么我要尽可能返回最强的接口?什么接口最强?你如何量化强度?

假设有一个方法返回一个元素序列。最弱的接口将是 IEnumerable 类型。按照指南,我们应该返回类似 IList 的东西。但为什么?

我想请教一下为什么要返回最强的接口。

4

6 回答 6

9

我不认为返回“最强”(或者更确切地说,最具体或派生的)类型一定是一个好主意,因为如果你的接口或合同指定你返回List<T>,如果你想Collection<T>在将来改变它,你会被卡住,因为这两者都是IList<T>没有共同祖先的不同实现。您必须修改界面。

您引用的口头禅似乎非常教条-总是有例外,我只想说您应该坚持适合给定工作的内容。这听起来像是对网络/IO 编程原则“对你接受的输入自由,但对输出的严格”的混蛋——但与定义 OO 接口或实现的类型相比,网络/IO 编程非常不同,因为与人类可读的规范(例如 HTTP 的 RFC)相比,ABI 是一种非常不同的野兽。

于 2013-06-05T07:56:33.827 回答
6

这条规则有几种形​​式,这里是另一种形式:

  • 输入应该是最通用/通用的(不是 .NET/C# 方式)类型
  • 输出应该是最具体的类型

这背后的原因是:

  1. 支持通用接口的各种类型都容易上手
  2. 明确返回实际返回的数据结构类型,为调用者提供更大的灵活性,甚至可能获得更高的性能

让我们举个例子。

假设您要创建一个方法,该方法接受一个整数集合,处理它们,然后返回一个新的整数集合。

一个非常好的实现可能是:

public IEnumerable<int> Process(IEnumerable<int> input) { ... }

请注意,这里我不一定遵循第二点,但这取决于方法的作用。如果该方法一次处理一个项目,则上述声明非常好,它表明您不一定要返回一个列表、一个数组或诸如此类的东西,而只是“一个集合”。

但是,让我们假设该方法必须首先收集所有值,然后处理它们,然后返回它们。在这种情况下,更好的声明可能是:

public List<int> Process(IEnumerable<int> input) { ... }

为什么这样更好?好吧,首先,调用者经常想要收集所有值,现在他不必这样做,它已经作为包含所有值的单个集合返回。此外,如果调用者只想继续使用值作为IEnumerable<int>,那也是可能的。

因此,该规则的目的是提高灵活性,并在某些情况下提高性能(内存或速度)。

请注意,该规则并不意味着您应该始终努力返回列表或数组或其他东西。相反,您应该努力从您已经建立的数据结构中返回“功能最强大”的类型。另请注意,您当然不应该返回对将继续存在于方法调用之外的内部数据结构的引用,而是返回一个副本。

于 2013-06-05T08:24:11.857 回答
5

有趣的概念——一个我以前从未明确听说过的概念。这基本上是Postel 定律“在你发送的内容上保持保守,在你收到的内容上保持自由”,这通常应用于跨系统通信。

IList是“更强”的,因为它比 . 提供更多的保证IEnumerable。AnIList支持添加、删除、枚举等,而IEnumerable仅支持枚举。当然,List更强大,是一个具体的类型。

至于建议本身的智慧——我认为在应用程序内部通常是一个好主意,对 API 设计人员有一些严重的警告。

从积极的方面来说,返回“最强”类型允许调用者进行所有可能的操作,而不会破坏合同(例如,通过强制转换)或做额外的工作(例如,将 复制IEnumerable到 a 中List)。这通常是对应用程序的帮助,并且由于它位于单个应用程序中,因此任何重大更改都将在编译时被捕获并且可以轻松修复。

但是,对于需要关注向后兼容性的 API(例如,由多个应用程序使用或独立部署),返回类型是合同的一部分。在那种情况下,你真的必须权衡永远支持IList回报而不是更弱的回报的意义IEnumerable。那里的细节应该由 API 的意图决定,而不是遵守这个规则。

FWIW,我个人的简洁陈述类似于“返回对调用者有用的东西,仅此而已。” 我认为这是在单个应用程序的上下文中决定“什么对调用者有用”的捷径,但在外部 API 中却是有害的。

于 2013-06-05T08:15:20.917 回答
2

“最强”是一个有点模糊的术语,至少可能意味着两件事:

  • 返回“最衍生”接口,例如 return IList<T>orList<T>而不是ICollection<T>orIEnumerable<T>
  • 返回“最受约束”的值范围。例如,返回 10..20 范围内的数字,而不是 5..25 范围内的数字

我将采取相反的策略,并建议您也许不应该总是遵循该建议,至少在第一点上是这样。

考虑 Linq 的情况。想象一下,如果 Linq 的接口遵循该建议并在List<T>任何地方返回而不是IEnumerable<T>. 显然,那是行不通的。

举一个具体的例子,考虑Enumerable<T>.Range(int start, int count). 这可能会返回 a List<T>,但在大多数情况下效率很低。

还假设您想从一个您不希望调用者修改的类中返回一系列项目。您应该返回 aReadOnlyCollection<T>而不是 a List<T>。事实上,在某些情况下,我认为你应该返回一个IEnumerable<T>.

派生类中的代码约定和覆盖方法

在派生类中实现重写方法时需要考虑不同的事情。

你可以弱化一个前提条件,因为这将保证基类版本的任何调用者继续工作。你不能让派生类对传递给方法的值和/或类型更严格(在这种情况下,编译器不允许传入“较弱”(即派生较少)类型 - 但它将无法控制可以传入的值的允许范围)。

同样,您可以加强后置条件。这可以适用于类型和输出范围。例如,如果基类方法返回 an,IEnumerable<T>您可以返回 an IList<T>(尽管它当然会作为 an 返回IEnumerable<T>)。

如果基类规定返回值必须在 0..100 范围内,您可以在不破坏代码协定的情况下返回 40..60 范围内的值。

于 2013-06-05T08:03:32.420 回答
1

好吧,在我看来,当返回值被强烈定义时,用户更容易知道如何处理它,而无需文档。

只需查看返回值是对象的 java-script 示例。如果不使用某种值的文档,您将无法使用它。(或运行代码以查看其含义)。

希望这可以帮助..

于 2013-06-05T07:49:31.100 回答
1

我想这是因为调用者可能想要最强接口的特性(比如 IList 的索引能力),并返回最强的保证。如果客户不想要这些功能,它们不会妨碍他。

于 2013-06-05T07:50:59.680 回答