.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的一样的对话框报错,如下:
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工具包获得的:
在这里提醒大家一下,在使用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,这个工具负责将编译出来的二进制代码进行调整,如下:
可以看到,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: }