契约式编程不是一门崭新的编程方法论。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. AssertAssert(断言)是最基本的契约。.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. PostconditionsPostconditions 契约表示方法终止时的状态。它跟 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>() 方法。 (责任编辑:admin) |