.NET4.0中的契约式编程

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

.NET4.0中的契约式编程
契约式编程不是⼀门崭新的编程⽅法论。

C/C++ 时代早已有之。

Microsoft 在 .NET 4.0 中正式引⼊契约式编程库。

博主以为契约式编程是⼀种相当不错的编程思想,每⼀个开发⼈员都应该掌握。

它不但可以使开发⼈员的思维更清晰,⽽且对于提⾼程序性能很有帮助。

值得⼀提的是,它对于并⾏程序设计也有莫⼤的益处。

我们先看⼀段很简单的,未使⽤契约式编程的代码⽰例。

// .NET 代码⽰例
public class RationalNumber
{
private int numberator;
private int denominator;
public RationalNumber(int numberator, int denominator)
{
this.numberator = numberator;
this.denominator = denominator;
}
public int Denominator
{
get
{
return this.denominator;
}
}
}
上述代码表⽰⼀个在 32 位有符号整型范围内的有理数。

数学上,有理数是⼀个整数 a 和⼀个⾮零整数 b 的⽐,通常写作 a/b,故⼜称作分数(题外话:有理数这个翻译真是够奇怪)。

由此,我们知道,有理数的分母不能为 0 。

所以,上述代码⽰例的构造函数还需要写些防御性代码。

通常 .NET 开发⼈员会这样写:
// .NET 代码⽰例
public class RationalNumber
{
private int numberator;
private int denominator;
public RationalNumber(int numberator, int denominator)
{
if (denominator == 0)
throw new ArgumentException("The second argument can not be zero.");
this.numberator = numberator;
this.denominator = denominator;
}
public int Denominator
{
get
{
return this.denominator;
}
}
}
下⾯我们来看⼀下使⽤契约式编程的 .NET 4.0 代码⽰例。

为了更加⽅便的说明,博主在整个⽰例上都加了契约,但此⽰例并⾮⼀定都加这些契约。

// .NET 代码⽰例
public class RationalNumber
{
private int numberator;
private int denominator;
public RationalNumber(int numberator, int denominator)
{
Contract.Requires(denominator != 0, "The second argument can not be zero.");
this.numberator = numberator;
this.denominator = denominator;
}
public int Denominator
{
get
{
Contract.Ensures(Contract.Result<int>() != 0);
return this.denominator;
}
}
[ContractInvariantMethod]
protected void ObjectInvariant()
{
Contract.Invariant(this.denominator != 0);
}
}
详细的解释稍后再说。

按理,既然契约式编程有那么多好处,那在 C/C++ 世界应该很流⾏才对。

为什么很少看到关于契约式编程的讨论呢?看⼀下 C++ 的契约式编程⽰例就知道了。

下⾯是 C++ 代码⽰例:
//typedef long int32_t;
#include <stdint.h>
template
inline void CheckInvariant(T& argument)
{
#ifdef CONTRACT_FULL
argument.Invariant();
#endif
}
public class RationalNumber
{
private:
int32_t numberator;
int32_t denominator;
public:
RationalNumber(int32_t numberator, int32_t denominator)
{
#ifdef CONTRACT_FULL
ASSERT(denominator != 0);
CheckInvaraint(*this);
#endif
this.numberator = numberator;
this.denominator = denominator;
#ifdef CONTRACT_FULL
CheckInvaraint(*this);
#endif
}
public:
int32_t GetDenominator()
{
#ifdef CONTRACT_FULL
// C++ Developers like to use struct type.
class Contract
{
int32_t Result;
Contract()
{
}
~Contract()
{
}
}
#endif
#ifdef CONTRACT_FULL
Contract contract = new Contract();
contract.Result = denominator;
CheckInvairant(*this);
#endif
return this.denominator;
#ifdef CONTRACT_FULL
CheckInvaraint(*this);
#endif
}
protected:
#ifdef CONTRACT_FULL
virtual void Invariant()
{
this.denominator != 0;
}
#endif
}
Woo..., 上述代码充斥了⼤量的宏和条件编译。

对于习惯了 C# 优雅语法的 .NET 开发⼈员来说,它们是如此丑陋。

更重要的是,契约式编程在 C++ 世界并未被标准化,因此项⽬之间的定义和修改各不⼀样,给代码造成很⼤混乱。

这正是很少在实际中看到契约式编程应⽤的原因。

但是在 .NET 4.0 中,契约式编程变得简单优雅起来。

.NET 4.0 提供了契约式编程库。

实际上,.NET 4.0 仅仅是针对 C++ 宏和条件编译的再次抽象和封装。

它完全基于 CONTRACTS_FULL, CONTRACTS_PRECONDITIONS Symbol 和 System.Diagnostics.Debug.Assert
⽅法、System.Environment.FastFail ⽅法的封装。

那么,何谓契约式编程?
何谓契约式编程
契约是减少⼤型项⽬成本的突破性技术。

它⼀般由 Precondition(前置条件), Postcondition(后置条件) 和 Invariant(不变量) 等概念组成。

.NET 4.0 除上述概念之外,还增加了 Assert(断⾔),Assume(假设) 概念。

这可以由枚举 ContractFailureKind 类型⼀窥端倪。

契约的思想很简单。

它只是⼀组结果为真的表达式。

如若不然,契约就被违反。

那按照定义,程序中就存在纰漏。

契约构成了程序规格说明的⼀部分,只不过该说明从⽂档挪到了代码中。

开发⼈员都知道,⽂档通常不完整、过时,甚⾄不存在。

将契约挪移到代码中,就使得程序可以被验证。

正如前所述,.NET 4.0 对宏和条件编译进⾏抽象封装。

这些成果⼤多集中在 System.Diagnostics.Contracts.Contract 静态类中。

该类中的⼤多数成员都是条件编译。

这样,我们就不⽤再使⽤ #ifdef 和定义 CONTRACTS_FULL 之类的标记。

更重要的是,这些⾏为被标准化,可以在多个项⽬中统⼀使⽤,并根据情况是否⽣成带有契约的程序集。

1. Assert
Assert(断⾔)是最基本的契约。

.NET 4.0 使⽤ Contract.Assert() ⽅法来特指断⾔。

它⽤来表⽰程序点必须保持的⼀个契约。

Contract.Assert(this.privateField > 0);
Contract.Assert(this.x == 3, "Why isn’t the value of x 3?");
断⾔有两个重载⽅法,⾸参数都是⼀个布尔表达式,第⼆个⽅法的第⼆个参数表⽰违反契约时的异常信息。

当断⾔运⾏时失败,.NET CLR 仅仅调⽤ Debug.Assert ⽅法。

成功时则什么也不做。

2. Assume
.NET 4.0 使⽤ Contract.Assume() ⽅法表⽰ Assume(假设) 契约。

Contract.Assume(this.privateField > 0);
Contract.Assume(this.x == 3, "Static checker assumed this");
Assume 契约在运⾏时检测的⾏为与 Assert(断⾔) 契约完全⼀致。

但对于静态验证来说,Assume 契约仅仅验证已添加的事实。

由于诸多限制,静态验证并不能保证该契约。

或许最好先使⽤ Assert 契约,然后在验证代码时按需修改。

当 Assume 契约运⾏时失败时, .NET CLR 会调⽤ Debug.Assert(false)。

同样,成功时什么也不做。

3. Preconditions
.NET 4.0 使⽤ Contract.Requires() ⽅法表⽰ Preconditions(前置条件) 契约。

它表⽰⽅法被调⽤时⽅法状态的契约,通常被⽤来做参数验证。

所有 Preconditions 契约相关成员,⾄少⽅法本⾝可以访问。

Contract.Requires(x != null);
Preconditions 契约的运⾏时⾏为依赖于⼏个因素。

如果只隐式定义了 CONTRACTS PRECONDITIONS 标记,⽽没有定义CONTRACTS_FULL 标记,那么只会进⾏检测 Preconditions 契约,⽽不会检测任何 Postconditions 和 Invariants 契约。

假如违反了Preconditions 契约,那么 CLR 会调⽤ Debug.Assert(false) 和 Environment.FastFail ⽅法。

假如想保证 Preconditions 契约在任何编译中都发挥作⽤,可以使⽤下⾯这个⽅法:
Contract.RequiresAlways(x != null);
为了保持向后兼容性,当已存在的代码不允许被修改时,我们需要抛出指定的精确异常。

但是在 Preconditions 契约中,有⼀些格式上的限定。

如下代码所⽰:
if (x == null) throw new ArgumentException("The argument can not be null.");
Contract.EndContractBlock(); // 前⾯所有的 if 检测语句皆是 Preconditions 契约
这种 Preconditions 契约的格式严格受限:它必须严格按照上述代码⽰例格式。

⽽且不能有 else 从句。

此外,then 从句也只能有单个 throw 语句。

最后必须使⽤ Contract.EndContractBlock() ⽅法来标记 Preconditions 契约结束。

看到这⾥,是不是觉得⼤多数参数验证都可以被 Preconditions 契约替代?没有错,事实的确如此。

这样这些防御性代码完全可以在Release 被去掉,从⽽不⽤做那些冗余的代码检测,从⽽提⾼程序性能。

但在⾯向验证客户输⼊此类情境下,防御性代码仍有必要。

再就是,Microsoft 为了保持兼容性,并没有⽤ Preconditions 契约代替异常。

4. Postconditions
Postconditions 契约表⽰⽅法终⽌时的状态。

它跟 Preconditions 契约的运⾏时⾏为完全⼀致。

但与 Preconditions 契约不
同,Postconditions 契约相关的成员有着更少的可见性。

客户程序或许不会理解或使⽤ Postconditions 契约表⽰的信息,但这并不影响客户程序正确使⽤ API 。

对于 Preconditions 契约来说,它则对客户程序有副作⽤:不能保证客户程序不违反 Preconditions 契约。

A. 标准 Postconditions 契约⽤法
.NET 4.0 使⽤ Contract.Ensures() ⽅法表⽰标准 Postconditions 契约⽤法。

它表⽰⽅法正常终⽌时必须保持的契约。

Contract.Ensures(this.F > 0);
B. 特殊 Postconditions 契约⽤法
当从⽅法体内抛出⼀个特定异常时,通常情况下 .NET CLR 会从⽅法体内抛出异常的位置直接跳出,从⽽辗转堆栈进⾏异常处理。

假如我们需要在异常抛出时还要进⾏ Postconditions 契约验证,我们可以如下使⽤:
Contract.EnsuresOnThrows<T>(this.F > 0);
其中⼩括号内的参数表⽰当异常从⽅法内抛出时必须保持的契约,⽽泛型参数表⽰异常发⽣时抛出的异常类型。

举例来说,当我们把 T ⽤Exception 表⽰时,⽆论什么类型的异常被抛出,都能保证 Postconditions 契约。

哪怕这个异常是堆栈溢出或任何不能控制的异常。

强烈推荐当异常是被调⽤ API ⼀部分时,使⽤ Contract.EnsuresOnThrows<T>() ⽅法。

C. Postconditions 契约内的特殊⽅法
以下要讲的这⼏个特殊⽅法仅限使⽤在 Postconditions 契约内。

⽅法返回值在 Postconditions 契约内,可以通过 Contract.Result<T>() ⽅法表⽰,其中 T 表⽰⽅法返回类型。

当编译器不能推导出 T 类型时,我们必须显式指出。

⽐如,C# 编译器就不能推导出⽅法参数类型。

Contract.Ensures(0 < Contract.Result<int>());
假如⽅法返回 void ,则不必在 Postconditions 契约内使⽤ Contract.Result<T>() 。

前值(旧值)在 Postconditions 契约内,通过 Contract.OldValue<T>(e) 表⽰旧有值,其中 T 是 e 的类型。

当编译器能够推导 T 类型时,可以忽略。

此外 e 和旧有表达式出现上下⽂有⼀些限制。

旧有表达式只能出现在 Postconditions 契约内。

旧有表达式不能包含另⼀个旧有表达式。

⼀个很重要的原则就是旧有表达式只能引⽤⽅法已经存在的那些旧值。

⽐如,只要⽅法 Preconditions 契约持有,它必定能被计算。

下⾯是这个原则的⼀些⽰例:
⽅法的旧有状态必定存在其值。

⽐如 Preconditions 契约暗含 xs != null ,xs 当然可以被计算。

但是,假如 Preconditions 契约为 xs != null || E(E 为任意表达式),那么 xs 就有可能不能被计算。

Contract.OldValue(xs.Length); // 很可能错误
⽅法返回值不能被旧有表达式引⽤。

Contract.OldValue(Contract.Result<int>() + x); // 错误
out参数也不能被旧有表达式引⽤。

如果某些标记的⽅法依赖⽅法返回值,那么这些⽅法也不能被旧有表达式引⽤。

Contract.ForAll(0, Contract.Result<int>(), i => Contract.OldValue(xs[i]) > 3); // 错误
旧有表达式不能在 Contract.ForAll() 和 Contract.Exists() ⽅法内引⽤匿名委托参数,除⾮旧有表达式被⽤作索引器或⽅法调⽤参数。

Contract.ForAll(0, xs.Length, i => Contract.OldValue(xs[i]) > 3); // OK
Contract.ForAll(0, xs.Length, i => Contract.OldValue(i) > 3); // 错误
如果旧有表达式依赖于匿名委托的参数,那么旧有表达式不能在匿名委托的⽅法体内。

除⾮匿名委托是 Contract.ForAll() 和
Contract.Exists() ⽅法的参数。

Foo( ... (T t) => Contract.OldValue(... t ...) ... ); // 错误
D. out 参数
因为契约出现在⽅法体前⾯,所以⼤多数编译器不允许在 Postconditions 契约内引⽤ out 参数。

为了绕开这个问题,.NET 契约库提供了Contract.ValueAtReturn<T>(out T t) ⽅法。

public void OutParam(out int x)
{
Contract.Ensures(Contract.ValueAtReturn(out x) == 3);
x = 3;
}
跟 OldValue ⼀样,当编译器能推导出类型时,泛型参数可以被忽略。

该⽅法只能出现在 Postconditions 契约。

⽅法参数必须是 out 参数,且不允许使⽤表达式。

需要注意的是,.NET ⽬前的⼯具不能检测确保 out 参数是否正确初始化,⽽不管它是否在 Postconditions 契约内。

因此, x = 3 语句假如被赋予其他值时,编译器并不能发现错误。

但是,当编译 Release 版本时,编译器将发现该问题。

5. Object Invariants
对象不变量表⽰⽆论对象是否对客户程序可见,类的每⼀个实例都应该保持的契约。

它表⽰对象处于⼀个“良好”状态。

在 .NET 4.0 中,对象的所有不变量都应当放⼊⼀个受保护的返回 void 的实例⽅法中。

同时⽤[ContractInvariantMethod]特性标记该⽅法。

此外,该⽅法体内在调⽤⼀系列 Contract.Invariant() ⽅法后不能再有其他代码。

通常我们会把该⽅法命名为 ObjectInvariant 。

[ContractInvariantMethod]
protected void ObjectInvariant()
{
Contract.Invariant(this.y >= 0);
Contract.Invariant(this.x > this.y);
}
同样,Object Invariants 契约的运⾏时⾏为和 Preconditions 契约、Postconditions 契约⾏为⼀致。

CLR 运⾏时会在每个公共⽅法末端检测Object Invariants 契约,但不会检测对象终结器或任何实现 System.IDisposable 接⼝的⽅法。

6. Contract 静态类中的其他特殊⽅法
.NET 4.0 契约库中的 Contract 静态类还提供了⼏个特殊的⽅法。

它们分别是:
A. ForAll
Contract.ForAll() ⽅法有两个重载。

第⼀个重载有两个参数:⼀个集合和⼀个谓词。

谓词表⽰返回布尔值的⼀元⽅法,且该谓词应⽤于集合中的每⼀个元素。

任何⼀个元素让谓词返回 false ,ForAll 停⽌迭代并返回 false 。

否则, ForAll 返回 true 。

下⾯是⼀个数组内所有元素都不能为 null 的契约⽰例:
public T[] Foo<T>(T[] array)
{
Contract.Requires(Contract.ForAll(array, (T x) => x != null));
}
B. Exists
它和 ForAll ⽅法差不多。

7. 接⼝契约
因为 C#/VB 编译器不允许接⼝内的⽅法带有实现代码,所以我们如果想在接⼝中实现契约,需要创建⼀个帮助类。

接⼝和契约帮助类通过⼀对特性来链接。

如下所⽰:
[ContractClass(typeof(IFooContract))]
interface IFoo
{
int Count { get; }
void Put(int value);
}
[ContractClassFor(typeof(IFoo))]
sealed class IFooContract : IFoo
{
int IFoo.Count
{
get
{
Contract.Ensures(Contract.Result<int>() >= 0);
return default(int); // dummy return
}
}
void IFoo.Put(int value)
{
Contract.Requires(value >= 0);
}
}
.NET 需要显式如上述声明从⽽把接⼝和接⼝⽅法相关联起来。

注意,我们不得不产⽣⼀个哑元返回值。

最简单的⽅式就是返回 default(T),不要使⽤ Contract.Result<T> 。

由于 .NET 要求显式实现接⼝⽅法,所以在契约内引⽤相同接⼝的其他⽅法就显得很笨拙。

由此,.NET 允许在契约⽅法之前,使⽤⼀个局部变量引⽤接⼝类型。

如下所⽰:
[ContractClassFor(typeof(IFoo))]
sealed class IFooContract : IFoo
{
int IFoo.Count
{
get
{
Contract.Ensures(Contract.Result<int>() >= 0);
return default(int); // dummy return
}
}
void IFoo.Put(int value)
{
IFoo iFoo = this;
Contract.Requires(value >= 0);
Contract.Requires(iFoo.Count < 10); // 否则的话,就需要强制转型 ((IFoo)this).Count
}
}
8. 抽象⽅法契约
同接⼝类似,.NET 中抽象类中的抽象⽅法也不能包含⽅法体。

所以同接⼝契约⼀样,需要帮助类来完成契约。

代码⽰例不再给出。

9. 契约⽅法重载
所有的契约⽅法都有⼀个带有 string 类型参数的重载版本。

如下所⽰:
Contract.Requires(obj != null, "if obj is null, then missiles are fired!");
这样当契约被违反时,.NET 可以在运⾏时提供⼀个信息提⽰。

⽬前,该字符串只能是编译时常量。

但是,将来 .NET 可能会改变,字符串可以运⾏时被计算。

但是,如果是字符串常量,静态诊断⼯具可以选择显⽰它。

10. 契约特性
A. ContractClass 和 ContractClassFor
这两个特性,我们已经在接⼝契约和抽象⽅法契约⾥看到了。

ContractClass 特性⽤于添加到接⼝或抽象类型上,但是指向的却是实现该类型的帮助类。

ContractClassFor 特性⽤来添加到帮助类上,指向我们需要契约验证的接⼝或抽象类型。

B. ContractInvariantMethod
这个特性⽤来标记表⽰对象不变量的⽅法。

C. Pure
Pure 特性只声明在那些没有副作⽤的⽅法调⽤者上。

.NET 现存的⼀些委托可以被认为如此,⽐如 System.Predicate<T> 和
parison<T>。

D. RuntimeContracts
这是个程序集级别的特性(具体如何,俺也不太清楚)。

E. ContractPublicPropertyName
这个特性⽤在字段上。

它被⽤在⽅法契约中,且该⽅法相对于字段来说,更具可见性。

⽐如私有字段和公共⽅法。

如下所⽰:[ContractPublicPropertyName("PublicProperty")]
private int field;
public int PublicProperty { get { ... } }
F. ContractVerification
这个特性⽤来假设程序集、类型、成员是否可被验证执⾏。

我们可以使⽤ [ContractVerification(false)] 来显式标记程序集、类型、成员不被验证执⾏。

.NET 契约库⽬前的缺陷
接下来,讲⼀讲 .NET 契约库⽬前所存在的⼀些问题。

值类型中的不变量是被忽略的,不发挥作⽤。

静态检测还不能处理 Contract.ForAll() 和 Contract.Exists() ⽅法。

C# 迭代器中的契约问题。

我们知道 Microsoft 在 C# 2.0 中添加了 yield 关键字来帮助我们完成迭代功能。

它其实是 C# 编译器做的糖果。

现在契约中,出现了问题。

编译器产⽣的代码会把我们写的契约放⼊到 MoveNext() ⽅法中。

这个时侯,静态检测就不能保证能够正确完成 Preconditions 契约。

Well,.NET 契约式编程到这⾥就结束了。

嗯,就到这⾥了。

PS : .NET 契约库虽然已经相当优雅。

但博主以为,其跟 D 语⾔实现的契约式编程仍有⼀段距离。

PS : 有谁愿意当俺的 Mentor 。

您能够享受这样的权利和义务:地狱般恐怖的提问和骚扰。

⾮不厌其烦者勿扰。

相关文档
最新文档