.NET 4.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> 和 System.Comparison<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 。您能够享受这样的权利和义务:地狱般恐怖的提问和骚扰。非不厌其烦者勿扰。

0
0
(请您对文章做出评价)
« 上一篇:一个简单有效的洗牌算法
» 下一篇:再说 lock-free 编程
posted @ 2009-03-21 21:56 Angel Lucifer 阅读(3995) 评论(40)  编辑 收藏 网摘 所属分类: 契约式编程

  回复  引用  查看    
#1楼2009-03-21 22:10 | Jeffrey Zhao      
.NET代码契约组件目前已经提供下载
http://www.infoq.com/cn/news/2009/02/Code-Contracts-.NET

  回复  引用  查看    
#2楼[楼主]2009-03-21 22:11 | Angel Lucifer      
@Jeffrey Zhao
正是有了这个库,才有了这篇文章,呵呵。
在 .NET 4.0 中,我只关注这个契约库和并行扩展库。

  回复  引用  查看    
#3楼2009-03-21 22:13 | Anders Cui      
老兄写的真好。我想知道有没有办法防止产生副作用呢?
  回复  引用  查看    
#4楼[楼主]2009-03-21 22:17 | Angel Lucifer      
@Anders Cui
没办法。
只能在一些已知的委托类型上加 [Pure] 特性,因为 .NET 中的委托实现了闭包。或者自己写一些没有副作用的方法,听起来这跟 FP 中的纯函数很像,呵呵。

  回复  引用  查看    
#5楼2009-03-21 22:22 | Nick Wang      
Design by Contract已经出现不是一年两年了,但是仍然没有普遍的应用起来,个人觉得还是有一定的原因的,因此不是很看好这个库,不是因为库本身,而是整个大环境。

ps:希望有人能反驳一下我,给我个使用他的理由。

  回复  引用  查看    
#6楼[楼主]2009-03-21 22:25 | Angel Lucifer      
@Nick Wang
文中给出了一些理由,不妨参考一下,:-)

  回复  引用  查看    
#7楼2009-03-21 22:28 | Nick Wang      
TDD的测试就可以覆盖了吧

  回复  引用  查看    
#8楼2009-03-21 22:28 | Anders Cui      
@Angel Lucifer
看来你需要做个PetShopByContract了,相信很多人都像NickWang这么想:)

  回复  引用  查看    
#9楼2009-03-21 22:30 | Nick Wang      
@Anders Cui
呵呵,这个工作量好像不小啊

  回复  引用  查看    
#10楼[楼主]2009-03-21 22:33 | Angel Lucifer      
@Anders Cui
现在没有时间做这个,D 语言的那个核心库就够我头大了。

@Nick Wang
我相信没多少人喜欢 TDD ,完全照 TDD 走,在我看来是噩梦,呵呵。

  回复  引用  查看    
#11楼[楼主]2009-03-21 22:34 | Angel Lucifer      
再就是 Design by Contract 非常简单,不但能防止很多出错,同时还能解放一部分性能,何乐而不为呢。以我在 D 语言契约式编程的经验来看,应用契约式编程+单元测试能很大程度避免出错。因为 D 语言没有良好的调试器,所以采用这种方式,也能很大程序避免使用调试器排错。 当然,每一种方法论都不是完美的,契约式编程也不能滥用。
  回复  引用  查看    
#12楼2009-03-21 22:35 | Nick Wang      
@Angel Lucifer
为什么TDD是噩梦呢?很多项目(包括MS的)都是用TDD做的啊,VS已经支持TDD了,Asp.net mvc已经支持TDD了,很多东西都支持了

  回复  引用  查看    
#13楼[楼主]2009-03-21 22:42 | Angel Lucifer      
@Nick Wang
我喜欢简单的东西。 TDD 对于我来说太复杂了。虽然它的理念挺简单,但是实施起来,我真受不了,呵呵。

  回复  引用  查看    
#14楼2009-03-21 22:43 | Nick Wang      
@Angel Lucifer
青菜萝卜,各有所爱啊。

  回复  引用  查看    
#15楼2009-03-21 22:55 | Jeffrey Zhao      
--引用--------------------------------------------------
Nick Wang: @Angel Lucifer
为什么TDD是噩梦呢?很多项目(包括MS的)都是用TDD做的啊,VS已经支持TDD了,Asp.net mvc已经支持TDD了,很多东西都支持了
--------------------------------------------------------
微软只是单元测试,没有TDD啊。

  回复  引用  查看    
#16楼2009-03-21 23:02 | 谦虚的天下      
好文!
  回复  引用  查看    
#17楼2009-03-21 23:02 | Jeffrey Zhao      
呵呵,发现文章其实没有解释为什么:“它不但可以使开发人员的思维更清晰,而且对于提高程序性能很有帮助。值得一提的是,它对于并行程序设计也有莫大的益处。”
  回复  引用    
#18楼2009-03-21 23:20 | 大城小 格[未注册用户]
这玩意儿很难吗?

自己写个好像一样。传入一个bool,如果false就抛出异常,异常的内容就是传入的那个字符串。

这个和.net 4.0有个啥关系。

再说 deisgn by contact早就有了 也不是.net4.0的什么特点。

  回复  引用    
#19楼2009-03-21 23:23 | 大城小 格[未注册用户]
但是 如果在.net 4.0里面,非运行环境下就能够给出提示,就是在编译的时候这些契约能够生效,那么还算是.net4.0的一些进步

不过明显不可能,很多传入参数都是运行时决定的。

还是一句话,根本和.net4.0没有任何关系

  回复  引用  查看    
#20楼[楼主]2009-03-21 23:24 | Angel Lucifer      
@Jeffrey Zhao
其实很好理解,呵呵。就写在评论里好了。

理解契约式编程那几个简单的概念,应用在实践中,会发现很多地方变得更有条理。这是进一步细分的结果。

至于提升性能,看看 Preconditions 契约就知道了,那些防御性代码完全可以用这个契约代替。当编译成 Release 版本,因为契约代码并不会被编译进去,这样程序就能全速运行,不用做那些冗余的代码检测。自然性能提升。

并行的话,说起来就复杂了。我记得有篇英文 Paper 专门阐述了这个问题,可以 Google 之。

上面说的这些内容,文中都有穿插体现,呵呵。

  回复  引用  查看    
#21楼[楼主]2009-03-21 23:29 | Angel Lucifer      
@大城小 格
契约式编程思想确实很简单,正因为简单才值得推荐。

当然也可以如老兄所提及的那样做。但是若是多个项目交互,难保契约式编程项目之间的定义和修改各不一样,给代码造成很大混乱。

Microsoft 在 .NET 4.0 提供的契约库正是优雅的提供了标准实现和概念定义,这才是 Microsoft 做出的最有意义的事情。

  回复  引用  查看    
#22楼2009-03-21 23:30 | Jeffrey Zhao      
@Angel Lucifer
public方法的防御性代码是不可少的,而原本在private方法里用Assert作防御性代码,release编译后也不会出现阿。所以我觉得对于性能是没有影响的,只是代码更优雅些。

  回复  引用  查看    
#23楼[楼主]2009-03-21 23:43 | Angel Lucifer      
@Jeffrey Zhao
我没有说所有都替代,而是大多都可以替代。我在文中讲 Preconditions 契约的时候说过了,比方验证客户输入就不能用契约。通常公共方法那些防御性代码可以用 Preconditions 契约代替,在 Debug 版测试时,大多错误会排除掉。实在不行,可以用 throw 异常的 Contract.EndContractBlock(),或者直接把某部分代码的契约编译进 Release 版本。比如 Contract.RequiresAlways() 方法。

任何东西都不完美,就像当初异常并没有取代错误码,契约也不可能取代异常。都有其适合的领域,呵呵。
其实,Microsoft 做的这个契约库想的很周到嘀。

  回复  引用  查看    
#24楼2009-03-22 00:51 | Jeffrey Zhao      
@Angel Lucifer
这话不假,呵呵

  回复  引用  查看    
#25楼2009-03-22 08:31 | 怪怪      
我有强烈的冲动,拿尖括号代替你的宏...
  回复  引用    
#26楼2009-03-22 09:09 | zhu liangxiong[未注册用户]
说实话 这个库不是很优雅。DoC最早是在Eiffle语言里面提出来的。C/C++标准委员会好像从来没有说过要支持它.
个人觉得在.net里面做DoC 最优雅的方式是使用Attribute.采用申明式的语法来做.
这样在接口和class上都可以很容易完成。interface 定义interface的C,具体到实现的时候,实现可以添加自己的C上去...

反正都是编译器做的语法糖 为什么不做的简单一点呢?

  回复  引用  查看    
#27楼2009-03-22 09:55 | 算法城管      
@zhu liangxiong

同感~对现在的实现效果比较无爱~

  回复  引用  查看    
#28楼2009-03-22 10:04 | Nick Wang      
--引用--------------------------------------------------
Jeffrey Zhao: --引用--------------------------------------------------
Nick Wang: @Angel Lucifer
为什么TDD是噩梦呢?很多项目(包括MS的)都是用TDD做的啊,VS已经支持TDD了,Asp.net mvc已经支持TDD了,很多东西都支持了
--------------------------------------------------------
微软只是单元测试,没有TDD啊。
--------------------------------------------------------
前天看scott发的那个免费的MVC章节,感觉是用TDD的,也可能是我理解错了 :p

◎zhu liangxiong
同感

  回复  引用  查看    
#29楼[楼主]2009-03-22 10:52 | Angel Lucifer      
@zhu liangxiong
@算法城管
老兄也知道这是个库。编译器在里面起到的作用并不大,语法糖应用很少。
我最喜欢的还是 D 语言的契约式编程。因为它直接就是语言特性,集成到了语法中。举个 D 语言的契约式编程示例:
public class RationalNumber
{
    
private int numberator;
    
private int denominator;

    
public this(int numberator, int denominator)
    
in
    {
        
// Preconditions 契约
        assert(denominator != 0"The second argument can not be zero.");
    }
    body
    {
        
this.numberator = numberator;
        
this.denominator = denominator;
    }

    
public int GetDenominator()
    
out(result)
    {
        
// Postconditions 契约
        assert(result != 0);
    }
    body
    {
        
return this.denominator;

    }

    
// Object Invariants 契约
    invariant()
    {
        assert(
this.denominator != 0);
    }
}

  回复  引用  查看    
#30楼2009-03-22 10:56 | 一箭      
看完有如下几个感觉:
1.契约式编程是将防御性检,根据需要进行了动态管理.
2.契约式编程有一个前提就是相信客户代码在REALSE状态下所传递的值是安全的.
我设想我如何运用契约式编程:
1.我仍然继续保留防御性检测,不会相信客户代码所传递的值.
原因有两个:a.这些检测对性能的影响非常小,完全可以忽略不计.
b.对运用IOC的框架项目而言,由于服务实现是完全独立的,因此客户代码的质量,之前根本无法得知.
2.有利于规范代码开发,而且有利于调试.
就我的感觉,未来开发思想一定会朝着自然语言的方向发展,即编程方式与人类思想问题的形为相似.契约性编程正是这个潮流又一次体现..NET在这方面已经有非常明显的展示,如:lambda.不过,我有一点遗憾,目前面向自然开发思想,还仅仅停留在计算机语言层面,在设计层面还没有很好的体现.

  回复  引用    
#31楼2009-03-22 11:30 | oldrev[未注册用户]
@Angel Lucifer
没有语法支持的契约果然是很丑恶

  回复  引用  查看    
#32楼2009-03-22 12:16 | Jeffrey Zhao      
Nick Wang:
--------------------------------------------------------
前天看scott发的那个免费的MVC章节,感觉是用TDD的,也可能是我理解错了 :p
--------------------------------------------------------
阿,那个东西我没看,不过就算有也就是个示例啊,内部开发都没有用TDD的,呵呵。

  回复  引用  查看    
#33楼2009-03-22 12:19 | Jeffrey Zhao      
@一箭
不应该相信客户端代码在Release模式下是正确的,也就是说,设计外部的代码还是必须有防御,这时候可能会使用不变量进行监测,前置后置代码就不能用了。

  回复  引用  查看    
#34楼[楼主]2009-03-22 12:39 | Angel Lucifer      
@oldrev
其实还好啦,呵呵。
跟 D 语言比,有差距。不过比起 C++ 来就好太多了。

PS: oldrev 老大最近在忙啥呢,在 D 社区也很少见到。

@一箭
怎么说呢,其实文中和评论已经讲的很多了。只有实际体会后,才能明白哪些地方可以用,哪些地方不能用,呵呵。

看看我与老赵之间的评论吧。

  回复  引用    
#35楼2009-03-22 21:12 | Eri[未注册用户]
伟大的Juval Lowy教导我们,平均每5行要有一个断言。
不过我一直没实践。
我想问的问题就是,用这个契约组件能比目前有的Debug类多什么好处?

  回复  引用  查看    
#36楼[楼主]2009-03-22 22:10 | Angel Lucifer      
@Eri
1.一致的契约观感。
2.IDE 强有力的支持。
3.易于管理和实施契约。
4.等等,呵呵。

  回复  引用    
#37楼2009-03-22 22:38 | Eri[未注册用户]
Angel Lucifer: @Eri
1.一致的契约观感。
2.IDE 强有力的支持。
3.易于管理和实施契约。
4.等等,呵呵。
--------------------------------------------------------

谢谢,
IDE 强有力的支持。
这个比较实在。

  回复  引用  查看    
#38楼2009-03-23 10:35 | 装配脑袋      
这套库长得像库,其实都是编译器做了很大支持的吧。
只是没有对语法做更改而已。

  回复  引用  查看    
#39楼2009-03-27 11:04 | Sail      
The document is good
  回复  引用    
#40楼2009-04-08 10:34 | alan000
武汉某五百强美资公司正在招.net方面的人才
1. senior .net developer,五年以上.net经验
2. technical leader,五年以上.net经验,英文流利
有兴趣请联系:alan.lan@sj-boren.com
MSN:lrry111@live.cn