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 契约持有,它必定能被计算。下面是这个原则的一些示例:
Contract.OldValue(xs.Length); // 很可能错误
Contract.OldValue(Contract.Result<int>() + x); // 错误
Contract.ForAll(0, Contract.Result<int>(), i => Contract.OldValue(xs[i]) > 3); // 错误 Contract.ForAll(0, xs.Length, i => Contract.OldValue(xs[i]) > 3); // OK Contract.ForAll(0, xs.Length, i => Contract.OldValue(i) > 3); // 错误
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. ForAllContract.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. PurePure 特性只声明在那些没有副作用的方法调用者上。.NET 现存的一些委托可以被认为如此,比如 System.Predicate<T> 和 System.Comparison<T>。 D. RuntimeContracts这是个程序集级别的特性(具体如何,俺也不太清楚)。 E. ContractPublicPropertyName这个特性用在字段上。它被用在方法契约中,且该方法相对于字段来说,更具可见性。比如私有字段和公共方法。如下所示: [ContractPublicPropertyName("PublicProperty")] private int field; public int PublicProperty { get { ... } } F. ContractVerification这个特性用来假设程序集、类型、成员是否可被验证执行。我们可以使用 [ContractVerification(false)] 来显式标记程序集、类型、成员不被验证执行。 .NET 契约库目前的缺陷接下来,讲一讲 .NET 契约库目前所存在的一些问题。
Well,.NET 契约式编程到这里就结束了。嗯,就到这里了。 PS : .NET 契约库虽然已经相当优雅。但博主以为,其跟 D 语言实现的契约式编程仍有一段距离。 PS : 有谁愿意当俺的 Mentor 。您能够享受这样的权利和义务:地狱般恐怖的提问和骚扰。非不厌其烦者勿扰。 (责任编辑:admin) |