介绍代码契约






4.95/5 (83投票s)
使用代码契约来编写优雅的代码。
引言
你写过多少次这样的方法:最终在前面加入一堆条件测试,以确保输入有效?你通常会得到一段看起来像这样的代码:
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。

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

现在是实用部分
虽然这都非常方便,但 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 - 初始版本