65.9K
CodeProject 正在变化。 阅读更多。
Home

介绍代码契约

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (83投票s)

2010年8月22日

CPOL

3分钟阅读

viewsIcon

252010

downloadIcon

737

使用代码契约来编写优雅的代码。

引言

你写过多少次这样的方法:最终在前面加入一堆条件测试,以确保输入有效?你通常会得到一段看起来像这样的代码:

public void Initialize(string name, int id)
{
    if (string.IsNullOrEmpty(value))
        throw new ArgumentException("name");
    if (id < 0) 
        throw new ArgumentOutOfRangeException("id");
    // Do some work here.
}

我赞赏你的勤奋和真正的专业精神,但是这样的代码对你来说是不是很糟糕?对我来说当然是,它真的会让方法变得杂乱无章。如果我们可以设置某种形式的契约来映射它,那不是很好吗?如果我们在方法调用上设置一些前置和后置条件,那不是很好吗?如果我们有一个代码契约,那不是很好吗?微软那些好人显然也这么认为,并且基于他们在 Spec# 中的研究,帮助我们提供了他们的劳动成果。

基础知识

契约基本上分为两类:前置条件和后置条件。前置条件声明在方法执行之前必须满足某些条件,后置条件声明在方法执行之后必须满足某些条件。到目前为止,一切都很好,而且非常方便。

让我们看看一个实际操作的例子

public void Initialize(string name, int id)
{ 
    Contract.Requires(!string.IsNullOrEmpty(name));
    Contract.Requires(id > 0);
    Contract.Ensures(Name == name);
    Contract.Ensures(Id = id); 
    // Do some work here.
} 

这个简单的方法有两个前置条件(使用Contract.Requires表达)和两个后置条件(使用Contract.Ensures表达)。我喜欢这里语法的清晰性,很明显这些构成了契约的一部分(而且 Code Contracts 可以直接从你的代码自动创建 XML 文档)。

你应该注意的一些事项:必须在尝试在方法中执行任何工作之前指定契约;如果你不小心,你不能选择契约抛出的异常(你需要使用适当的泛型,例如Contract.Requires<ArgumentOutOfRangeException>);你需要从 Dev Labs 下载一个二进制重写器,以便在运行时实际使用契约(称为ccrewriter) - 它带有一个方便的 VS 插件,使使用 ccrewriter 变得更好;可以从一个非常有用的属性页访问,通过指定要执行运行时契约检查来触发 ccrewriter。

CodeContractsPropertyPage.png

现在,如果我调用这个方法并传入一个无效的值(例如 Id 0),我应该触发契约失败

PreconditionExceptionInInterfaceContract.png

现在是实用部分

虽然这都非常方便,但 Code Contracts 还可以做更多的事情。比如,设置一个接口,并在每次使用该接口时应用契约?天啊,这对我说太好了,我打赌这对任何忙于编写框架的人来说都是好消息。

“你肯定在开玩笑,皮特”,我听到你说。我不是在开玩笑 - 使用 Code Contracts 很容易实现,以下是实现方法。首先,你定义一个接口。让我们设置一个我们可以使用的接口

public interface IUseful
{ 
    void Initialize(string name, int id);
} 

然后我们设置一个abstract 类,我们用它来实际定义契约

public abstract class UsefulContract : IUseful
{ 
    public void Initialize(string name, int id)
    {
        Contract.Requires(!string.IsNullOrEmpty(name));
        Contract.Requires(id > 0);
        Contract.Ensures(Name == name);
        Contract.Ensures(Id = id);
    }
} 

现在,我们需要修饰接口,告诉它我们有一个类构成了契约。我们需要使用ContractClassAttribute 属性

[ContractClass(typeof(UsefulContract))]
public interface IUseful

然后,我们需要修饰实际的契约实现,将其与接口联系起来

[ContractClassFor(typeof(IUseful))]
public abstract class UsefulContract : IUseful  

现在,每当我们使用该接口时,契约都会自动应用。

性能如何?

Code Contracts 的美妙之处在于它们不依赖于反射来执行它们的魔力,二进制重写器将契约转换为运行时代码。如果我们看一下我们在附加的示例项目中为 Name 属性指定的代码,我们会看到它看起来像这样

public string Name
{
    get
    {
        return _name;
    }
    set
    {
        if (_name == value) return;
        OnChanging("Name");
        _name = value;
        OnChanged("Name");
    }
}

契约像这样在契约类中设置

public string Name
{
    get
    {
        return string.Empty;
    }
    set
    {
        Contract.Requires(!string.IsNullOrEmpty(value), 
            "The name must be greater than 0 characters long.");
    }
}

现在,凭借 Code Contracts 的美妙之处,这会转换为以下运行时代码

public string Name
{ 
    get
    { 
        return this._name;
    }
    set
    {
        __ContractsRuntime.Requires(!string.IsNullOrEmpty(value), 
            "The name must be greater than 0 characters long.", 
            "!string.IsNullOrEmpty(value)");
        if (this._name != value)
        {
            this.OnChanging("Name");
            this._name = value;
            this.OnChanged("Name");
        }
    }
} 

结论

我希望我已经激起了你出去玩代码契约的兴趣。我用得越多,就越喜欢它们。

本文的第 2 部分现在可以在这里找到。

历史

  • 23/08/10 - 初始版本
© . All rights reserved.