一次又一次问自己什么是好的代码,我读到了“接受最弱,最强的回报”的建议。
对我来说很明显为什么接受最弱的:该方法声明与其客户最弱的合同。因此,客户不需要“专门”对抗非常“强大”的界面。
“回归最强”对我来说并不是那么清楚。为什么我应该返回最强的接口?什么是最强大的界面?你如何量化强度?
假设有一个方法返回一系列元素。最弱的接口是IEnumerable类型。按照指南,我们应该像IList一样返回。但为什么呢?
我想请求解释为什么要返回最强的界面。
答案 0 :(得分:9)
我不认为返回“最强”(或者更确切地说,最具体或派生)类型是一个好主意,因为如果您的界面或合同指定您返回List<T>
,那么您将被卡住您希望以后将其更改为Collection<T>
,因为这两者都是IList<T>
的不同实现,没有共同的祖先。你必须修改界面。
你引用的口头禅似乎非常教条 - 总有例外,我只是说你应该坚持适合某项工作。这听起来像是网络/ IO编程原则的混蛋“在接受输入时要自由,但在输出的内容上要严格” - 但是与定义OO接口或实现的类型相比,网络/ IO编程是非常不同的与人类可读的规范(例如HTTP的RFC)相比,ABI是一种非常不同的野兽。
答案 1 :(得分:6)
这条规则有几种形式,这是另一种形式:
这背后的原因是:
我们举一个例子。
假设您要创建一个方法,该方法接受整数集合,处理它们,然后返回一个新的整数集合。
完美的实现可能是:
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)
“最强”是一个模糊的术语,可能至少意味着两件事:
IList<T>
或List<T>
,而不是ICollection<T>
或IEnumerable<T>
我将采取相反的方针,并建议你或许不应该总是遵循这个建议,至少在第一点。
考虑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的索引能力),并返回最强的保证。如果客户不想要这些功能,他们就不会妨碍他。