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

介绍代码契约 - 第 2 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (27投票s)

2010年8月25日

CPOL

5分钟阅读

viewsIcon

80779

downloadIcon

288

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

引言

本文基于我上一篇关于在 .NET 中使用代码契约的文章中提供的信息。那篇文章介绍了使用代码契约的基础知识,因此本文将更深入地探讨这项技术,我们将了解其中一些更晦涩和“有趣”的功能。

请在阅读本文之前阅读第一篇文章,因为假定您已经了解了代码契约设置的前置条件和后置条件,并且您了解二进制重写器。 

对象不变量

有时我们可能希望有一个条件始终对类的实例为真。我的意思是,我们有一个条件(或一组条件),在每次调用 `public` 方法后都必须为真。假设,例如,您的要求是有一个列表不能为空(我知道这是一个非常牵强的例子),我们该如何实现呢?好吧,我们创建一个 `private void` 方法,其中只包含对 `Contract.Invariant` 的调用,然后用 `ContractInvariantMethod` 属性标记该方法。

[ContractInvariantMethod]
private void TestList()
{
    Contract.Invariant(this.MonitoredList.Count > 0);
}

现在,每当我们在包含此列表的类中调用 `public` 方法时,如果后置条件指示由于不变量表明实例的状态不再“良好”而导致契约不再有效,就会抛出异常。

您会注意到我提到这发生在调用方法时,而不是发生在执行 `public` 方法时。这有一个非常好的理由;不变量作为后置条件进行检查,这样您就可以在实例中的一个 `public` 方法调用实例中的另一个 `public` 方法,而不执行第二个 `public` 方法中的后置条件。您可能会这样做,因为第二个方法暂时将对象移至无效状态,但第一个方法中剩余的代码会将对象恢复到有效状态。

在附加的示例中,我们有以下代码

[ContractInvariantMethod]
private void ValidateDocument()
{
    Contract.Invariant(!string.IsNullOrWhiteSpace(this.Filename), 
		"The filename must be set");
    Contract.Invariant(this.Filename.Length < 255, 
		"The filename must be less than 255 characters");
    Contract.Invariant(this.Metadata.Count > 0, "The metadata cannot be empty");
}

public void Change(string filename, List<string> metadata)
{
    Filename = null;
    Metadata.Clear();
    Filename = filename;
    Metadata.AddRange(metadata);
}

如您所见,`Change` 方法执行的操作如果我们直接调用它们会触发不变量条件,但由于它们存在于一个调用其他 `public` 方法的 `public` 方法中,因此不变量检查会在最后发生。要了解它是如何完成的,我们来看一下重写器生成的内容

public void Change(string filename, List<string> metadata)
{
    bool flag = this.$evaluatingInvariant$;
    this.$evaluatingInvariant$ = true;
    this.Filename = null;
    this.Metadata.Clear();
    this.Filename = filename;
    this.Metadata.AddRange(metadata);
    this.$evaluatingInvariant$ = flag;
    this.$InvariantMethod$();
}

不变量方法生成如下

[CompilerGenerated, ContractInvariantMethod]
protected override void $InvariantMethod$()
{
    if (!this.$evaluatingInvariant$)
    {
        this.$evaluatingInvariant$ = true;
        try
        {
            __ContractsRuntime.Invariant
		(!string.IsNullOrWhiteSpace(this.<filename>k__BackingField), 
                "The filename must be set", "!string.IsNullOrWhiteSpace(this.Filename)");
            __ContractsRuntime.Invariant(this.<filename>k__BackingField.Length < 0xff, 
                "The filename must be less than 255 characters", 
		"this.Filename.Length < 255");
            __ContractsRuntime.Invariant(this.<metadata>k__BackingField.Count > 0, 
                "The metadata cannot be empty", "this.Metadata.Count > 0");
        }
        finally
        {
            this.$evaluatingInvariant$ = false;
        }
    }
}

注意事项

  • 请勿直接调用标记为 `ContractInvariantMethod` 的方法。重写器会更改方法名称,因此您无法调用它。
  • 每个类只能有一个 `ContractInvariantMethod`。
  • 不变量不会被 `IDisposable.Dispose` 或对象终结器调用。
  • 在 `Dispose` 或 `Finalizer` 方法中,多个方法不检查不变量的技巧不起作用。从这里调用 `public` 方法将触发不变量检查。
  • 像在调用方法中直接清空实例内的列表这样的操作,不会触发不变量检查 - 这必须在实例内完成。
  • 不变量不适用于 `struct`。

缩写符

假设您的类中有一些方法具有一些相同的参数,并且需要应用相同的规则,那么将相同的契约元素复制到每个方法中并不是非常面向对象。契约缩写符允许我们添加方法来为我们执行通用的验证,而无需到处复制代码。让我们扩展上面的 `Change` 方法,看看会得到什么。

首先,让我们创建缩写符方法。将其创建为 `private` 方法,并应用 `ContractAbbreviator` 属性。坏消息是;您必须将 *%programfiles%\Microsoft\Contracts\Languages\ContractAbbreviator.cs* 添加到您的项目中才能做到这一点 - 这并不是太大的负担,但如果它是 CC 套件的默认组成部分就更好了。

[ContractAbbreviator]
private void ValidateNameAndMetadata
	(string filename, List<string> metadata, int maxlength)
{
    Contract.Requires(!string.IsNullOrEmpty(filename), "The filename cannot be blank");
    Contract.Requires(metadata != null);
    Contract.Requires(metadata.Count > 0);
    // The postcondition should ensure that the metadata must be less than maxlength
    Contract.Ensures(this.Metadata.Count < maxlength);
}

现在,我们需要将对此方法的调用添加到我们的代码中。让我们选择 `Change` 方法,并将调用添加到缩写符方法。

public void Change(string filename, List<string> metadata)
{
    ValidateNameAndMetadata(filename, metadata, 5);
    Filename = null;
    Metadata.Clear();
    Filename = filename;
    Metadata.AddRange(metadata);
}

现在,如果我们查看重写后的代码,我们会看到应用缩写符的效果。

[ContractAbbreviator]
private void ValidateNameAndMetadata
	(string filename, List<string> metadata, int maxlength)
{
}

public void Change(string filename, List<string> metadata)
{
    if (__ContractsRuntime.insideContractEvaluation <= 4)
    {
        try
        {
            __ContractsRuntime.insideContractEvaluation++;
            __ContractsRuntime.Requires(!string.IsNullOrEmpty(filename), 
                "The filename cannot be blank", "!string.IsNullOrEmpty(filename)");
            __ContractsRuntime.Requires(metadata != null, null, "metadata != null");
            __ContractsRuntime.Requires(metadata.Count > 0, null, "metadata.Count > 0");
        }
        finally
        {
            __ContractsRuntime.insideContractEvaluation--;
        }
    }
    bool flag = this.$evaluatingInvariant$;
    this.$evaluatingInvariant$ = true;
    this.Filename = null;
    this.Metadata.Clear();
    this.Filename = filename;
    this.Metadata.AddRange(metadata);
    if (__ContractsRuntime.insideContractEvaluation <= 4)
    {
        try
        {
            __ContractsRuntime.insideContractEvaluation++;
            __ContractsRuntime.Ensures(this.Metadata.Count < 5, 
			null, "this.Metadata.Count < maxlength");
        }
        finally
        {
            __ContractsRuntime.insideContractEvaluation--;
        }
    }
    this.$evaluatingInvariant$ = flag;
    this.$InvariantMethod$();
}

正是在这一点上,缩写符的强大功能显而易见。它自动应用了我们方法中的前置条件和后置条件(并方便地删除了缩写符方法中的任何主体),因此我们可以根据需要应用这些方法。一个类中应用的缩写符数量没有限制。

契约量词

实际上,代码契约提供了两种类型的契约量词:`Exists` 和 `ForAll`。对于 `ForAll`,我们评估枚举中的每个元素并执行比较。在以下示例中,我们将检查列表中的每个元素,以确保它们不为 null 或为空。

Contract.Requires(Contract.ForAll(metadata, x => !string.IsNullOrWhiteSpace(x)));

`Exists` 是应用于列表中的每个元素的谓词,直到匹配条件为止,此时它将完全返回。如果谓词未能找到匹配项,它将返回 `false`。以下代码示例通过确保文档以 `.doc`、`.jpg`、`.docs` 或 `.xls` 结尾来演示这一点。

public string[] types = new string[] { ".doc", ".jpg", ".docx", ".xls" };

public void ChangeDocument(string document)
{
    Contract.Requires(Contract.Exists(types, 
        x => string.Compare(x, Path.GetExtension(document), true) == 0));
    this.Filename = document;
} 

关注点

当程序集设置为“自定义参数验证”时,您无法使用 `Contract.Requires<T>`。如果想使用它,则需要将程序集更改为“标准契约 Requires”。您可以通过代码契约属性页来完成此操作,方法是设置程序集模式。

Assembly mode setting showing types

当您将程序集模式更改为 **标准契约 Requires** 并重新生成代码时,重写后的代码如下所示。

public void Change(string filename, List<string> metadata)
{
    bool flag;
    try
    {
        if (__ContractsRuntime.insideContractEvaluation <= 4)
        {
            try
            {
                __ContractsRuntime.insideContractEvaluation++;
                __ContractsRuntime.Requires<argumentoutofrangeexception>
		(!string.IsNullOrEmpty(filename), 
                    "The filename cannot be blank", "!string.IsNullOrEmpty(filename)");
                __ContractsRuntime.Requires(metadata != null, null, "metadata != null");
                __ContractsRuntime.Requires(metadata.Count > 0, 
				null, "metadata.Count > 0");
            }
            finally
            {
                __ContractsRuntime.insideContractEvaluation--;
            }
        }
        flag = this.$evaluatingInvariant$;
        this.$evaluatingInvariant$ = true;
        this.Filename = null;
        this.Metadata.Clear();
        this.Filename = filename;
        this.Metadata.AddRange(metadata);
        this.$evaluatingInvariant$ = flag;
        this.$InvariantMethod$();
    }
    catch (ArgumentOutOfRangeException exception)
    {
        if (__ContractsRuntime.insideContractEvaluation <= 4)
        {
            try
            {
                __ContractsRuntime.insideContractEvaluation++;
                __ContractsRuntime.EnsuresOnThrow(this.Metadata.Count < 5, null, 
                    "this.Metadata.Count < maxlength", exception);
            }
            finally
            {
                __ContractsRuntime.insideContractEvaluation--;
            }
        }
        this.$evaluatingInvariant$ = flag;
        this.$InvariantMethod$();
        throw;
    }
}

这需要几秒钟才能看到两种验证类型之间的区别,但第二种方法会将代码包装在 `try`/`catch` 块中,以确保抛出相关的异常。

结论

正如我们所见,代码契约非常强大。我在这里的主要抱怨是,整个 CC 体验不是一个连贯的整体 - 它需要下载东西,复制文件,所有这些都造成了不必要的复杂性。尽管如此,如果您可以容忍这些“问题”(我知道我可以),那么代码契约将为您提供一个有价值的编码工具。

历史

  • 2010年8月25日 - 初始版本
© . All rights reserved.