接受最弱,回报最强。但为什么?

时间:2013-06-05 07:44:09

标签: c# interface

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

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

“回归最强”对我来说并不是那么清楚。为什么我应该返回最强的接口?什么是最强大的界面?你如何量化强度?

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

我想请求解释为什么要返回最强的界面。

6 个答案:

答案 0 :(得分:9)

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

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

答案 1 :(得分:6)

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

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

这背后的原因是:

  1. 易于采用支持通用界面的各种类型
  2. 明确实际返回的数据结构类型,为调用者提供更大的灵活性,甚至可以提高性能
  3. 我们举一个例子。

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

    完美的实现可能是:

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

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

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

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

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

    因此,规则的目的是增加灵活性,并在某些情况下提高性能(记忆或速度)。

    请注意,规则意味着您应该始终努力返回列表或数组或其他内容。您应该努力从已经构建的数据结构中返回“最具功能性”的类型。另请注意,您当然应该永远不会返回对内部数据结构的引用,这些内部数据结构将继续存在于方法调用之外,而是返回副本。

答案 2 :(得分:5)

有趣的概念 - 我以前从未明确听过的概念。基本上Postel's Law“你发送的内容是保守的,你收到的内容是自由的”,这通常适用于跨系统通信。

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

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

从积极的方面来说,返回“最强”类型允许调用者在不违反合同的情况下进行所有可能的操作(例如,通过强制转换)或进行额外的工作(例如,将IEnumerable复制到{{{} 1}})。这通常是应用程序中的一个帮助,因为它在一个应用程序中,所以任何重大更改都将在编译时捕获并且可以很容易地修复。

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

FWIW,我个人精辟的陈述将是“回报对调用者有用的东西,而不是更多。”我认为这是在单个应用程序的上下文中决定“对调用者有用的东西”的快捷方式,但在外部API中是有害的。

答案 3 :(得分:2)

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

  • 返回“Most Derived”界面,例如返回IList<T>List<T>,而不是ICollection<T>IEnumerable<T>
  • 返回“最受约束”值范围。例如,返回10..20范围内的数字,而不是5..25
  • 范围内的数字

我将采取相反的方针,并建议你或许不应该总是遵循这个建议,至少在第一点。

考虑Linq的案例。想象一下,如果Linq的界面遵循该建议并返回List<T>,而不是IEnumerable<T>。显然,这是不可行的。

有关具体示例,请考虑Enumerable<T>.Range(int start, int count)。这可能会返回List<T>,但在大多数情况下效率会非常低。

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

派生类中的代码合约和重写方法

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

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

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

如果基类表示返回的值必须在0..100范围内,则可以返回40..60范围内的值,而不会违反代码协定。

答案 4 :(得分:1)

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

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

希望这会有所帮助..

答案 5 :(得分:1)

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