.NET 4.0中的新功能介绍:契约式设计 (Design By Contracts)

什么是契约

我们先来看一个很简单的例子:

Void WordList.Insert(string word)

这个函数负责将word以升序插入到WordList中的单词列表中,word不可以为NULL。

上面这些说明文字都是用来描述此函数的行为的。当使用该函数的调用者看到这些说明文字的时候,便知道函数应该如何调用以及在不同情况下的函数行为,换言之,上面这段说明文字简单的描述了函数调用者和被调用者的一种约定,这种约定也被称之为契约(Contracts)。契约一般来讲可以分成三类,包括:

1. Precondition:函数调用之前需要满足何种条件:比如,参数word不可以为NULL

2.Postcondition:函数调用之后需要满足何种条件:比如,参数word被加入到WordList的成员m_wordList中,m_wordList元素个数+1

3. Invariant:函数调用之前之后总是需要满足的条件是什么:比如,m_wordList中的单词总是以升序排列

契约式设计这个概念是Bertrand Meyer提出的,并在Eiffel Programming Language这本书中有详细的描述,Eiffel语言本身对契约式设计支持也非常好,有兴趣的朋友可以尝试并比较一下。

.NET 4.0中的Contracts

在.NET 4.0中引入了对契约式设计的支持,我们来看一下,如果上面那个例子用4.0中的契约式设计功能应该如何编写:

   1: public void WordList.Insert(string word)
  2: {
  3:  CodeContract.Requires(word != null);
  4:     CodeContract.Ensures(CodeContract.OldValue<int>(_words.Count) + 1 == _words.Count);
  5:    CodeContract.EnsuresOnThrow<ApplicationException>(CodeContract.OldValue<int>(_words.Count) == _words.Count);
  6:     …
  7: }
  8: 

其中:

1. Contract.Requires是Precondition

2. Contract.Ensures是PostCondition

3. Contract.EnsuresOnThrow是Postcondition,和Ensures的区别是这是在Throw的情况下需要满足的Postcondition

可以看到,Contracts在被显式的放在代码当中,而不仅仅是说明性的文字,那这样有什么好处呢?

1. 提供了运行时支持:这些Contracts都是可以被运行的,并且一旦条件不被满足,会弹出类似Assert的一样的对话框报错,如下:

clip_image002

2. 提供了静态分析支持:通过静态分析Contracts,静态分析工具可以比较容易掌握函数的各种有关信息,甚至可以作为Intellisense

看到这里,可能有些朋友会有一些疑问:Contracts可以做条件检查并且弹出类似Assert的对话框,这个和Assert有何区别呢?其实,Contracts和Assert的区别主要在于:

1. Contracts的意图更加清晰,通过不同的Requires/Ensures等等调用,代表不同类型的条件,比单纯的Assert更容易理解和进行自动分析

2. Contracts的位置更加统一,将3种不同条件都放在代码的开始处,而非散见在函数的开头和结尾,便于查找和分析。

3. 不同的开发人员、不同的小组、不同的公司、不同的库可能都会有自己的Assert,这就大大增加了自动分析的难度,也不利于开发人员编写代码。而Contracts直接被.NET 4.0支持,是统一的。

当然了,Contracts也和Assert有一些非常类似的地方,比如Contracts和Assert都可以运行时检查错误,也可以在随意的在代码中关闭打开。VS中支持Contracts的几种不同的典型配置:

1. CONTRACTS_FULL:打开所有Contracts

2. CONTRACTS_PRECONDITIONS:仅有Precondition

3. RequireAlways Only:仅有RequireAlways。RequireAlways的意思等会讲到

这些选项都可以通过项目的Code Contracts页面来进行修改,这个页面是通过安装Contracts工具包获得的:

clip_image004

在这里提醒大家一下,在使用Contracts功能之前,一定要下载最新版的Contracts开发工具包:https://msdn.microsoft.com/en-us/devlabs/dd491992.aspx

这个工具包提供了一系列的Contracts所需要的一些工具,文档以及VS的插件。不安装这个工具包将无法使用Contracts的功能。

刚才我们谈到了Requires和Ensures两种条件,这里把.NET 4.0中的最常用的几种Contracts列一下:

1. Requires:函数入口处必须满足的条件

2. Ensures:函数出口处必须满足的条件

3. Invariants:所有成员函数出口处都必须满足的条件

4. Assertions:在某一点必须满足的条件

5. Assumptions:在某一点必然满足的条件,用来减少不必要的警告信息

其中,对于Invariant需要稍作一点说明。因为Invariant是需要对每个成员函数都需要起到作用,显然如果把这个条件放在每个函数的末尾处可以起到这个效果,但是这么做显得比较笨。.NET 4.0中采取的方式是这样的:使用某个成员函数作为Invariant,上面标记ContractInvariantMethod属性,然后在这个成员函数里面用Contracts.Invariant来指定每一条Invariant条件。这样,Invariant就会对每个函数起作用了。同时,这个Invariant方法也应该标记上PureAttribute属性,表明该函数不存在副作用(不会修改对象的状态)

Contracts的奥秘

看到这里,不知道有些朋友发现没有,不管是Ensures还是Invariant,它们的位置并不是代码应该所存在的位置。对于Ensures来讲,它是函数出口条件,那么必然在出口时候被调用,但是为什么Ensures写在前面呢?同样的,Invariant是对每个函数起作用,如果单独写一个函数作为Invariant怎么保证它会被每个函数调用到呢?其实这些都是很合理的问题。首先,Ensure和Invariant的这种写法是很合理的,原因之前也提到过了,剩下的问题是,.NET如何保证这些条件会在正确的时候被执行。其实,在编译的时候,Contracts的工具包中有一个小工具ccrewrite,这个工具负责将编译出来的二进制代码进行调整,如下:

clip_image006

可以看到,ccrewrite负责将各种条件的位置进行调整,最终使得条件处于正确的位置。

接口级别的契约

除了在方法实现中加入契约之外,接口也可以加入契约。接口首先需要加上一个ContractClassAttribute,指向对应的ContractClass:

   1: [ContractClass(typeof(IFooContract))]
  2: interface IFoo {
  3:    int Count { get; }
  4:   void Put(int value );
  5: }

而ContractClass则需声明ContractClassForAttribute,说明是IFoo的ContractClass,然后显式实现IFoo,并加入Contract:

   1: [ContractClassFor(typeof(IFoo))]
  2: sealed class IFooContract : IFoo {
  3:   int IFoo.Count {
  4:     get {
  5:        Contract.Ensures( 0 <= Contract.Result<int>() );
  6:        return default( int ); // dummy return
  7:     }
  8: }
  9: 
 10: void IFoo.Put(int value)
 11: {
 12:  Contract.Requires( 0 <= value );
 13: }

一个完整的例子

现在我们来看一个完整的例子。在这个例子中WordList使用了Contract.Requires/Ensures/EnsuresOnThrow/Invariant等契约,还附送几个Bug,程序比较简单,这里就不多说了:

   1: using System;
  2: using System.Collections.Generic;
  3: using System.Linq;
  4: using System.Text;
  5: using System.Diagnostics.Contracts;
  6: 
  7: namespace ContractDemo
  8: {
  9:     class WordList
 10:     {
 11:         private List<string> _words;
 12: 
 13:         public WordList(int capacity)
 14:         {
 15:             Contract.Requires(capacity > 0);
 16:             _words = new List<string>(capacity);
 17:         }
 18: 
 19:         public void Insert(string word)
 20:         {
 21:             Contract.Requires(word != null);
 22:             Contract.Ensures(Contract.OldValue<int>(_words.Count) + 1 == _words.Count);
 23:             Contract.EnsuresOnThrow<ApplicationException>(Contract.OldValue<int>(_words.Count) == _words.Count);
 24: 
 25:             int i;
 26:             for (i = 0; i < _words.Count; ++i)
 27:             {
 28:                 int compare = string.Compare(word, _words[i]);
 29:                 if (compare > 0)
 30:                     break;
 31:                 else if (compare == 0)
 32:                     throw new ApplicationException("Already exist!");
 33:             }
 34: 
 35:             _words.Insert(i, word);
 36:         }
 37: 
 38:         [ContractInvariantMethod()]
 39:         internal void Invariant()
 40:         {
 41:             // No null string
 42:             Contract.Invariant(Contract.ForAll(_words, w => w != null));
 43: 
 44:             // Make sure the words are in ascending order
 45:             Contract.Invariant(IsAscending());
 46:         }
 47: 
 48:         [Pure]
 49:         internal bool IsAscending()
 50:         {
 51:             bool isAscending = true;
 52:             for (int i = 1; i < _words.Count; ++i)
 53:             {
 54:                 if (string.Compare(_words[i - 1], _words[i]) > 0)
 55:                 {
 56:                     isAscending = false;
 57:                     break;
 58:                 }
 59:             }
 60: 
 61:             return isAscending;
 62:         }
 63:     }
 64: 
 65:     class Program
 66:     {
 67:         static void Main(string[] args)
 68:         {
 69:             WordList wordList = new WordList(0);
 70:             wordList.Insert("Hello");
 71:             wordList.Insert("World");
 72:         }
 73:     }
 74: }