我为什么要使用代码合同


26

最近,我偶然发现了Microsoft的代码合同框架。

我阅读了一些文档,发现自己不断问:“为什么我要这样做,因为它通常也不执行静态分析。”

现在,我已经有了一种防御性编程风格,并带有如下防护异常:

if(var == null) { throw new NullArgumentException(); }

我也大量使用NullObject Pattern,很少遇到任何问题。向其添加单元测试,就一切就绪。

我从未使用过断言,也从未错过它们。恰恰相反。我真的很讨厌其中包含很多毫无意义的断言的代码,这对我来说只是噪音,使我无法真正看到。代码合同,至少是Microsoft的方法,几乎​​是一样的,甚至更糟。它们给代码增加了很多噪音和复杂性。在99%的情况下,无论如何都会抛出异常-所以我不在乎它是来自断言/合同还是实际问题。程序状态真正被破坏的情况极少出现。

坦率地说,使用代码契约有什么好处?有没有?如果您已经在使用单元测试和防御性代码,我认为引入合同是不值得的,并且会给维护人员在更新方法时要诅咒的代码带来噪音,就像我看不到代码在做什么一样由于无用的断言。我还没有看到支付该价格的充分理由。



1
为什么说“它不能且通常不能执行静态分析”?我认为这是该项目的重点之一(而不是仅使用Asserts或防御性异常抛出)?
Carson63000 2013年

@ Carson63000仔细阅读文档,您会发现静态检查器将仅输出很多警告,大多数不必要的警告(开发人员承认是这样),并且大多数已定义的合同只会引发异常,除了执行其他操作外。
猎鹰

@猎鹰:奇怪的是,我的经历是完全不同的。静态分析并非完美无瑕,但效果很好,即使在警告级别4(最高级别)下,我也很少看到不必要的警告。
阿森尼·穆尔琴科

我猜我将不得不尝试一下它的行为。
猎鹰

Answers:


42

您可能还问过,静态键入何时比动态键入更好。这场辩论已经进行了多年,没有结束的余地。因此,请看诸如 动态类型与静态类型的语言研究之类的问题

但是基本的论点是在编译时发现和修复问题的潜力,否则这些问题可能会进入生产环境。

合同与守卫

即使有警卫和例外情况,您仍然会面临系统在某些情况下将无法执行其预期任务的问题。可能发生故障的实例将非常关键且代价高昂。

合同与单元测试

关于这一点的通常说法是,测试证明存在错误,而类型(合同)证明不存在错误。F.ex. 使用一种类型,您知道程序中没有路径可能无法提供无效的输入,而测试只能告诉您所覆盖的路径确实提供了正确的输入。

合约与空对象模式

现在,至少在同一个球场。像Scala和Haskell这样的语言在这种从程序中完全消除空引用的方法中取得了巨大的成功。(即使Scala正式允许使用null,约定也不使用它们)

如果您已经采用这种模式来消除NRE,那么您基本上已经消除了运行时失败的最大根源,从根本上讲,合同允许您这样做。

区别可能在于合同可以选择自动要求所有代码避免使用null,从而迫使您在更多地方使用此模式来传递编译。

最重要的是,合同还使您可以灵活地将目标定为null。因此,如果您在错误中不再看到任何NRE,则可能要使用合同来扼杀您可能遇到的下一个最常见的问题。一人去吗?索引超出范围?

但...

话虽如此。我确实同意,添加到代码中的语法噪声(甚至是结构噪声)合同相当可观,并且不应低估分析对构建时间的影响。因此,如果您决定向系统中添加合同,那么明智的做法是非常谨慎地进行操作,并且只关注于尝试解决的错误类别。


6
这是对程序员的一个很好的第一答案。很高兴您能参与社区活动。

13

我不知道“它不能且通常不能执行静态分析”的断言从何而来。断言的第一部分显然是错误的。第二个取决于您所说的“经常”是什么意思。我想说的是,它经常执行静态分析,而很少执行失败。在普通的商业应用,很少越接近从来没有

因此,这是第一个好处:

好处1:静态分析

普通的断言和参数检查有一个缺点:它们被推迟到执行代码为止。另一方面,无论是在编码步骤还是在编译应用程序时,代码协定都在更早的层次上表现出来。您越早发现错误,修复它的成本就越低。

好处2:始终保持最新文档

代码合同还提供了一种始终是最新的文档。如果该方法的XML注释SetProductPrice(int newPrice)告知newPrice要优于或等于零,你可能会希望文件是最新的,但你也可能发现某人改变了方法,以便newPrice = 0抛出ArgumentOutOfRangeException,但从来没有改变,则相应的文档。鉴于代码合同与代码本身之间的相关性,您不会遇到文档不同步的问题。

代码契约提供的这类文档也很珍贵,通常XML注释不能很好地解释可接受的值。有多少次是我想知道是否nullstring.Empty或者\r\n是一个方法的授权值,以及XML注释都沉默了上!

总之,没有代码合同,很多代码如下:

我会接受一些值,但不会接受其他值,但是您必须猜测或阅读文档(如果有)。实际上,不要阅读文档:它已经过时了。只需遍历所有值,您就会看到那些使我抛出异常的值。您还必须猜测可能返回的值的范围,因为考虑到过去几年对我进行的数百次更改,即使我告诉您更多有关它们的信息,也可能并非如此。

使用代码合同,它将变成:

title参数可以是长度为0..500的非空字符串。后面的整数是一个正值,仅当字符串为空时才可以为零。最后,我将返回一个IDefinition对象,永远不会为null。

好处3:接口合同

第三个好处是,代码契约可以授权接口。假设您有类似的内容:

public interface ICommittable
{
    public ICollection<AtomicChange> PendingChanges { get; }

    public void CommitChanges();

    ...
}

您将如何仅使用断言和异常来保证CommitChanges仅在PendingChanges不为空时才能调用?您如何保证PendingChanges永远不会null

好处4:强制执行方法的结果

最后,第四个好处是能够Contract.Ensure得到结果。如果编写返回整数的方法时,我想确定该值永远不会低于或等于零怎么办?包括五年后,在经历了来自许多开发人员的大量变更之后?一旦方法具有多个返回点,Asserts就会成为维护的噩梦。


考虑将代码约定不仅作为代码正确性的一种手段,而且还应作为一种更严格的代码编写方式。以类似的方式,专门使用动态语言的人可能会问为什么要在语言级别上强制类型,而在需要时可以在断言中执行相同的操作。您可以,但是与一堆断言相比,静态类型更易于使用,出错率更低,并且具有自我记录功能。

动态类型和静态类型之间的差异非常接近普通编程和按合同编程之间的差异。


3

我知道这个问题有点晚了,但是代码合同还有另一个好处值得一提

在方法之外检查合同

当我们的方法中有保护条件时,这就是检查发生和报告错误的地方。然后,我们可能必须研究堆栈跟踪,以找出错误的实际来源。

代码协定位于方法内,但是在任何尝试调用该方法的前提下,先决条件都会在方法外进行检查。合同成为该方法的一部分,并确定是否可以调用它。这意味着我们得到的错误报告更加接近问题的实际根源。

如果您有在很多地方都需要调用的受合同保护的方法,那么这可能是真正的好处。


2

确实有两件事:

  1. 借助工具支持,VS可以使合同数据脱颖而出,从而成为自动完成帮助的一部分。
  2. 当子类覆盖某个方法时,您失去的所有检查(如果遵循LSP仍然有效)与合同的合同将自动遵循其子类。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.