8

在我开始使用代码契约之前,我有时会在使用构造函数链接时遇到与参数验证相关的问题。

用一个(人为的)例子最容易解释:

class Test
{
    public Test(int i)
    {
        if (i == 0)
            throw new ArgumentOutOfRangeException("i", i, "i can't be 0");
    }

    public Test(string s): this(int.Parse(s))
    {
        if (s == null)
            throw new ArgumentNullException("s");
    }
}

我希望Test(string)构造函数链接Test(int)构造函数,为此我使用int.Parse().

当然,int.Parse()不喜欢有一个 null 参数,所以如果s是 null 它会在我到达验证行之前抛出:

if (s == null)
    throw new ArgumentNullException("s");

这使得该检查无用。

如何解决?好吧,我有时会这样做:

class Test
{
    public Test(int i)
    {
        if (i == 0)
            throw new ArgumentOutOfRangeException("i", i, "i can't be 0");
    }

    public Test(string s): this(convertArg(s))
    {
    }

    static int convertArg(string s)
    {
        if (s == null)
            throw new ArgumentNullException("s");

        return int.Parse(s);
    }
}

这有点繁琐,堆栈跟踪在失败时并不理想,但它可以工作。

现在,随着代码合同的出现,我开始使用它们:

class Test
{
    public Test(int i)
    {
        Contract.Requires(i != 0);
    }

    public Test(string s): this(convertArg(s))
    {
    }

    static int convertArg(string s)
    {
        Contract.Requires(s != null);
        return int.Parse(s);
    }
}

一切都很好。它工作正常。但后来我发现我可以做到这一点:

class Test
{
    public Test(int i)
    {
        Contract.Requires(i != 0);
    }

    public Test(string s): this(int.Parse(s))
    {
        // This line is executed before this(int.Parse(s))
        Contract.Requires(s != null);
    }
}

然后如果我这样做var test = new Test(null),则在之前Contract.Requires(s != null)执行。这意味着我可以完全取消测试! this(int.Parse(s))convertArg()

所以,关于我的实际问题:

  • 这种行为是否记录在任何地方?
  • 在为这样的链式构造函数编写代码契约时,我可以依赖这种行为吗?
  • 还有其他方法我应该解决这个问题吗?
4

1 回答 1

7

简短的回答

是的,行为记录在“前提条件”的定义中,以及如何Contract.EndContractBlock处理没有调用的遗留验证(if/then/throw)。

如果您不想使用Contract.Requires,可以将构造函数更改为

public Test(string s): this(int.Parse(s))
{
    if (s == null)
        throw new ArgumentNullException("s");
    Contract.EndContractBlock();
}

长答案

当您Contract.*在代码中调用时,您实际上并没有调用System.Diagnostics.Contracts命名空间中的成员。例如,Contract.Requires(bool)定义为:

[Conditional("CONTRACTS_FULL")]
public static void Requires(bool condition) 
{
    AssertMustUseRewriter(ContractFailureKind.Precondition, "Requires"); 
}

AssertMustUseRewriter无条件地抛出 a ContractException,所以在没有重写编译的二进制文件的情况下,如果CONTRACTS_FULL定义了代码,代码就会崩溃。如果未定义,则从不检查前置条件,因为由于存在属性Requires,C# 编译器省略了对的调用。[Conditional]

重写器

根据在项目属性中选择的设置,Visual Studio 将定义CONTRACTS_FULL并调用ccrewrite以生成适当的 IL 以在运行时检查合同。

合同示例:

private string NullCoalesce(string input)
{
    Contract.Requires(input != "");
    Contract.Ensures(Contract.Result<string>() != null);

    if (input == null)
        return "";
    return input;
}

编译csc program.cs /out:nocontract.dll,你得到:

private string NullCoalesce(string input)
{
    if (input == null)
        return "";
    return input;
}

编译csc program.cs /define:CONTRACTS_FULL /out:prerewrite.dll并运行ccrewrite -assembly prerewrite.dll -out postrewrite.dll您将获得实际执行运行时检查的代码:

private string NullCoalesce(string input)
{
    __ContractRuntime.Requires(input != "", null, null);
    string result;
    if (input == null)
    {
        result = "";
    }
    else
    {
        result = input;
    }
    __ContractRuntime.Ensures(result != null, null, null);
    return input;
}

最有趣的是,我们的Ensures(后置条件)被移到了方法的底部,而我们的Requires(前置条件)并没有真正移动,因为它已经在方法的顶部。

这符合文档的定义

[前置条件] 是调用方法时世界状态的契约。
...
后置条件是方法终止时的状态契约。换句话说,在退出方法之前检查条件。

现在,您的场景中的复杂性存在于前提条件的定义中。根据上面列出的定义,前提条件在方法运行之前运行。问题在于 C# 规范规定必须在构造函数主体[CSHARP 10.11.1]之前立即调用构造函数初始化程序(链式构造函数) ,这与先决条件的定义不一致。

魔法住在这里

因此,生成的代码ccrewrite不能表示为 C#,因为该语言没有提供在链式构造函数之前运行代码的机制(除非您提到在链式构造函数参数列表中调用静态方法)。 ccrewrite,根据定义的要求,采用您的构造函数

public Test(string s)
    : this(int.Parse(s))
{
    Contract.Requires(s != null);
}

编译为

上面已编译代码的 MSIL

并在调用链式构造函数之前将调用移动到 requires :

上面代码的msil通过了合约重写器

意思是...

避免使用静态方法进行参数验证的方法是使用合约重写器。您可以通过使用来调用重写器Contract.Requires,或者通过以 结束代码块来表示代码块是先决条件Contract.EndContractBlock();。这样做会导致重写器将它放在方法的开头,在调用构造函数初始化程序之前。

于 2014-02-15T00:18:14.533 回答