C#/.NET 中的契约

C#/.NET 中的契约

发布于 2017-12-20 15:04 更新于 2018-04-25 09:11

将文档放到代码里面,文档才会及时地更新!

微软从 .NET Framework 4.0 开始,增加了 System.Diagnostics.Contracts 命名空间,用来把契约文档融入代码。然而后面一直不冷不热,Visual Studio 都没天然支持。ReSharper 不知何时加入了 ReSharper Annotations,在 ReSharper 插件工作的情况下能够进行静态契约的验证。C#8.0 的可空引用类型是 Roslyn 对 null 的验证,本以为会带来编译级别的警告,没想到也只是契约。


契约式编程

当你调用某个类库里面的方法时,你如何能够知道传入的参数是否符合规范?如何能够知道方法调用结束之后是否要对结果进行判断?

T DoSomething<T>(T parent) where T : class;

▲ 对于上面的方法,你知道 null 传入参数是合理的吗?返回的参数需要判空吗?

代码的编写者可能是这么写的:

public T DoSomething<T>(T parent) where T : class
{
    if (parent == null)
    {
        throw new ArgumentNullException(nameof(parent));
    }
    // 后续逻辑。
}

有些静态代码检查工具也许可以根据这里的参数判断代码块来认定为此处的参数不能为 null,但这种判断代码无处不在,静态检查工具如何能够有效地捕获每一处的检查呢?难道我们真的要去翻阅文档吗?然而除非是专门提供 SDK 的团队,否则文档通常都会滞后于代码,那么对于这些契约的修改可能就不太准确。

于是,契约式编程就应运而生。

它将前置条件(Precondition)、后置条件(Postcondition)、不变量(Invariant)等代码分离出来,按照特定的格式编写以便能够被静态检查工具分析出来。

有了静态分析工具以及契约代码的帮助,Visual Studio 的智能感知提示将能够直接告诉我们代码编写的潜在问题,而不必等到运行时再抛出异常,那时将降低开发效率,将增加生产环境运行的风险。

几种不同的契约方法

ReSharper Annotations

ReSharper 并没有将其称之为“契约”,因为它真的只是“文档级别”的约束,只会在写代码的时候具备一定程度的静态分析能力以便给出提示,并不提供运行时的检查。不过,ReSharper 会为我们生成运行时检查的代码。只要是装了 ReSharper 插件并用它写过代码的,应该都见过 ReSharper Annotations 了,因为它会在我们试图添加契约代码时自动添加契约标记(Attribute)。

▲ 生成 ReSharper Annotations

如果错过了首次提示,可以在 ReSharper 的设置界面中生成 Annotations 的代码。(复制一份代码然后新建一个文件粘贴。)

▲ 手动生成 ReSharper Annotations

ReSharper 中常用的契约 Attribute

  • CanBeNull
    • 表示参数或返回值可能为 null。
  • CannotApplyEqualityOperator
    • 表示某个类型的相等比较不应该用 == 或 !=,而应该用 Equals。
  • ItemCanBeNull
    • 表示集合参数或集合返回值里某一项可能为 null。
  • ItemNotNull
    • 表示集合参数或集合返回值里每一项都不为 null。
  • LinqTunnel
    • 表示某个方法就像 linq 方法一样。
  • LocalizationRequired
    • 表示参数字符串需要被本地化。
  • NotNull
    • 表示参数或返回值不可能为 null。
  • PathReference
    • 表示参数字符串是一个路径。
  • Pure
    • 表示方法不会修改任何状态(这意味着如果连返回值都不用,那调用了也相当于什么都没做)。
  • RegexPattern
    • 表示参数字符串是一个正则表达式(会被 ReSharper 代码着色)。
  • 还有 100+ 个……
  • ContractAnnotation

我的朋友林德熙使用 Resharper 特性 一文中有这些契约对编写代码的更详细的效果描述和截图。

System.Diagnostics.Contracts

此命名空间下的 Contract 类型定义了几个方法,覆盖了我们编写一个方法所要遵循的契约模式。

private T DoSomething<T>(T parent) where T : class
{
    // * 要开始此任务必须先满足某些条件(Requires,RequiresAlways,EndContractBlock)
    // 做一些操作。
    // * 此时认定一定满足某个条件(Assume)
    // 继续执行一些操作。
    // * 操作执行完后一定满足某组条件(Ensures,EnsuresOnThrows)
}

以上代码中,星号(*)表示契约代码,其他表示方法内的普通代码。一个典型的例子如以下代码所示:

private T DoSomething<T>(T parent) where T : class
{
    // * 要开始此任务必须先满足某些条件(Requires,EndContractBlock)
    Contract.Requires<ArgumentNullException>(parent != null);

    // 做一些操作。

    // * 此时认定一定满足某个条件(Assume)
    Contract.Assume(parent != null);

    // 继续执行一些操作。

    // * 操作执行完后一定满足某组条件(Ensures,EnsuresOnThrows)
    Contract.EnsuresOnThrow<InvalidOperationException>(Value != null);
}

在这里,Requires 是真的会抛出异常的,但 AssumeEnsuresOnThrow 是需要写条件编译符为 CONTRACTS_FULL 的。

或者,这样用普通的抛异常的方式。如果使用普通方式抛出异常,需要遵循 if-then-throw 的模式,即有问题立刻就抛出异常。例如下面对 null 的判断就符合这样的模式。

private T DoSomething<T>(T parent) where T : class
{
    // * 要开始此任务必须先满足某些条件(Requires,EndContractBlock)
    if (parent == null) throw new ArgumentNullException(nameof(parent));
    Contract.EndContractBlock();

    // 做一些操作。

    // * 此时认定一定满足某个条件(Assume)
    Contract.Assume(parent != null);

    // 继续执行一些操作。

    // * 操作执行完后一定满足某组条件(Ensures,EnsuresOnThrows)
    Contract.EnsuresOnThrow<InvalidOperationException>(Value != null);
}

当然也可以不止是这样简单的判断,也可以调用其他方法,但要求方法必须是 [Pure] 方法,即方法执行完之后,除了返回一个值之外,不改变应用程序的任何状态。

对此契约的静态分析微软有提供工具:Microsoft/CodeContracts: Source code for the CodeContracts tools for .NET,ReSharper 对此也有一丁点儿的支持。

Roslyn

Roslyn 相比于任何第三方契约的优势在于它甚至能在语法层面形成契约(比如 C#8.0 中的可空引用类型)。

实际应用

事实上在 GitHub 中,使用各种契约的都有,不过以 ReSharper Annotations 和 System.Diagnostics.Contracts 的居多;C#8.0 的可空引用类型等到 8.0 发布以后再看吧。

在实际应用中,并没有严格的说哪一个更好哪一个一般,两者都可以用,只要我们有分析和提示此契约的工具,就可以在项目中推行开来。

但是,基于契约编写代码的模式却能帮助我们写出更加健壮的代码来。也就是说,用哪个并不重要,重要的是——用起来


参考资料

本文会经常更新,请阅读原文: https://walterlv.com/post/contracts-in-csharp.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 (walter.lv@qq.com)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏企鹅号快讯

Java 9 逆天的十大新特性

在介绍 Java 9 之前,我们先来看看 Java 成立到现在的所有版本。 1990 年初,最初被命名为 Oak; 1995 年 5 月 23 日,Java 语...

24850
来自专栏逸鹏说道

AutoMapper 使用实践

一. 使用意图 常常在开发过程中,碰到一个实体上的属性值,要赋值给另外一个相类似实体属性时,且属性有很多的情况。一般不利用工具的话,就要实例化...

372130
来自专栏WeTest质量开放平台团队的专栏

手游热更新方案--Unity3D下的CsToLua技术

原文链接:http://wetest.qq.com/lab/view/387.html

24820
来自专栏Golang语言社区

利用Go语言实现简单Ping过程的方法

、准备工作 安装最新的Go 1、由于Google被墙的原因,如果没有VPN的话,就到这里下载:http://www.golangtc.com/download ...

51750
来自专栏王清培的专栏

.NET/ASP.NET Routing路由(深入解析路由系统架构原理)

阅读目录: 1.开篇介绍 2.ASP.NET Routing 路由对象模型的位置 3.ASP.NET Routing 路由对象模型的入口 4.ASP.NET R...

25190
来自专栏C/C++基础

jsoncpp初探

首先说一下JSON。JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它基于ECMAScript的一个子集。 JSO...

19620
来自专栏互扯程序

Java 9 逆天的十大新特性

KS Knowledge Sharing 知识分享 现在是资源共享的时代,同样也是知识分享的时代,如果你觉得本文能学到知识,请把知识与别人分享。 在介绍...

27460
来自专栏debugeeker的专栏

公司内部文档安全软件coredump分析实例

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xuzhina/article/detai...

11620
来自专栏草根专栏

使用静态基类方案让 ASP.NET Core 实现遵循 HATEOAS Restful Web API

Hypermedia As The Engine Of Application State (HATEOAS) HATEOAS(Hypermedia as t...

44150
来自专栏微服务生态

跟着小程学微服务-自己动手扩展分布式调用链

微服务是当下最火的词语,现在很多公司都在推广微服务,当服务越来越多的时候,我们是否会纠结以下几个问题:

13540

扫码关注云+社区

领取腾讯云代金券