频道栏目
读书频道 > 软件开发 > C# > Visual C#2010开发权威指南
1.2.5 .NET 4.0中的新特性 - 契约式设计
2012-12-07 15:41:05     我来说两句
收藏   我要投稿

本文所属图书 > Visual C#2010开发权威指南

Visual Studio是微软公司推出的开发环境,是目前最流行的Windows平台应用程序开发环境。Visual Studio 2010版本于2010年4月12日面市,其集成开发环境(IDE)的界面被重新设计和组织,变得更加简单明了。Visual ...  立即去当当网订购

1. 什么是契约

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

void WordList.Insert(string word)

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

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

契约一般来讲可以分成三类。

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

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

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

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

2. .NET 4.0中的Contracts

在.NET 4.0中引入了对契约式设计的支持,我们来看一下如果上面那个例子用.NET 4.0中的契约式设计功能应该如何编写:
public void WordList.Insert(string word)
{
CodeContract.Requires(word != null);
CodeContract.Ensures(
CodeContract.OldValue<int>(_words.Count) + 1 == _words.Count);
CodeContract.EnsuresOnThrow<ApplicationException>(
CodeContract.OldValue<int>(_words.Count) == _words.Count);
}

其中:

 Contract.Requires是Precondition类契约。

 Contract.Ensures是PostCondition类契约。

 Contract.EnsuresOnThrow是Postcondition类契约,与Ensures的区别在于,这是在Throw的情况下需要满足的Postcondition类契约。

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

(1) 提供了运行时支持。这些Contracts都是可以被运行的,并且一旦条件不被满足,会弹出类似Assert的对话框报错,如图1.47所示。


 

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

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

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

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

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

当然,Contracts也与Assert有一些非常类似的地方,比如Contracts和Assert都可以运行时检查错误,也可以随意地在代码中关闭或打开。

VS中支持Contracts的几种不同的典型配置。

 CONTRACTS_FULL:打开所有Contracts。

 CONTRACTS_PRECONDITIONS:仅有Precondition。

 RequireAlways Only:仅有RequireAlways。

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


 

在这里提醒一下,在使用Contracts功能之前,一定要下载最新版的Contracts开发工具包:

http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx

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

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

 Requires:函数入口处必须满足的条件。

 Ensures:函数出口处必须满足的条件。

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

 Assertions:在某一点必须满足的条件。

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

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

3. Contracts的奥秘

看到这里,不知道有些读者发现没有,不管是Ensures还是Invariant,它们的位置并不是代码应该存在的位置。对于Ensures来讲,它是函数出口条件,那么必然在出口时候被调用,但是为什么Ensures写在前面呢?同样地,Invariant是对每个函数起作用,如果单独写一个函数作为Invariant,怎么保证它会被每个函数调用到呢?其实这些都是很合理的问题。

首先,Ensure和Invariant的这种写法是很合理的,原因在前面也提到过了,剩下的问题是,.NET如何保证这些条件会在正确的时候被执行。其实在编译的时候,Contracts的工具包中有一个小工具ccrewrite,这个工具负责对编译出来的二进制代码进行调整,如图1.49所示。


 

可以看到,ccrewrite负责对各种条件的位置进行调整,最终使得条件处于正确的位置。
4. 接口级别的契约

除了在方法实现中加入契约之外,接口也可以加入契约。接口首先需要加上一个ContractClassAttribute,指向对应的ContractClass:
[ContractClass(typeof(IFooContract))]
interface IFoo {
int Count { get; }
void Put(int value);
}

而ContractClass则需声明ContractClassForAttribute,说明是IFoo的ContractClass,然后显式实现IFoo,并加入Contract:
[ContractClassFor(typeof(IFoo))]
sealed class IFooContract : IFoo {
int IFoo.Count {
get {
Contract.Ensures(0 <= Contract.Result<int>());
return default(int); // dummy return
}
}
void IFoo.Put(int value)
{
Contract.Requires(0 <= value);
}

现在我们来看一个完整的例子。在这个例子中WordList使用了Contract. Requires/ Ensures/ EnsuresOnThrow/Invariant等契约,还附送几个Bug,程序比较简单,这里就不多说了:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics.Contracts;
namespace ContractDemo
{
class WordList
{
private List<string> _words;
public WordList(int capacity)
{
Contract.Requires(capacity > 0);
_words = new List<string>(capacity);
}
public void Insert(string word)
{
Contract.Requires(word != null);
Contract.Ensures(
Contract.OldValue<int>(_words.Count) + 1 == _words.Count);
Contract.EnsuresOnThrow<ApplicationException>(
Contract.OldValue<int>(_words.Count) ==_words.Count);
int i;
for (i=0; i<_words.Count; ++i)
{
int compare = string.Compare(word, _words[i]);
if (compare > 0)
break;
else if (compare == 0)
throw new ApplicationException("Already exist!");
}
_words.Insert(i, word);
}
[ContractInvariantMethod()]
internal void Invariant()
{
// No null string
Contract.Invariant(Contract.ForAll(_words, w => w != null));
// Make sure the words are in ascending order
Contract.Invariant(IsAscending());
}
[Pure]
internal bool IsAscending()
{
bool isAscending = true;
for (int i=1; i<_words.Count; ++i)
{
if (string.Compare(_words[i - 1], _words[i]) > 0)
{
isAscending = false;
break;
}
}
return isAscending;
}
}
class Program
{
static void Main(string[] args)
{
WordList wordList = new WordList(0);
wordList.Insert("Hello");
wordList.Insert("World");
}
}
}

您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:1.2.4 .NET 4.0中的新特性 - 等价类型(Type Equivalency)
下一篇:1.2.6 .NET 4.0中的新特性 – 交互新特性
相关文章
图文推荐
排行
热门
最新书评
特别推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站