是否保证在调用链式构造函数之前评估代码合约?

Mat*_*son 8 c# code-contracts constructor-chaining

在我开始使用代码约定之前,有时在使用构造函数链时会遇到与参数验证相关的繁琐.

这是一个(人为的)示例最简单的解释:

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");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

if (s == null)
    throw new ArgumentNullException("s");
Run Code Online (Sandbox Code Playgroud)

这使得检查毫无用处.

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这有点繁琐,堆栈跟踪失败时并不理想,但它有效.

现在,随着Code Contracts,我开始使用它们:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

那么,关于我的实际问题:

  • 这种行为记录在哪里吗?
  • 在为这样的链式构造函数编写代码契约时,我可以依赖这种行为吗?
  • 还有其他方法我应该接近这个吗?

Mit*_*tch 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();
}
Run Code Online (Sandbox Code Playgroud)

答案很长

当您将一个Contract.*呼叫在你的代码,你是不是实际调用的成员System.Diagnostics.Contracts的命名空间.例如,Contract.Requires(bool)定义为:

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

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

重写者

基于项目属性中选择的设置,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;
}
Run Code Online (Sandbox Code Playgroud)

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

private string NullCoalesce(string input)
{
    if (input == null)
        return "";
    return input;
}
Run Code Online (Sandbox Code Playgroud)

编译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;
}
Run Code Online (Sandbox Code Playgroud)

最感兴趣的是我们Ensures(一个后置条件)被移到方法的底部,而我们Requires(一个先决条件)并没有真正移动,因为它已经在方法的顶部.

这符合文档的定义:

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

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

魔术生活在这里

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

public Test(string s)
    : this(int.Parse(s))
{
    Contract.Requires(s != null);
}
Run Code Online (Sandbox Code Playgroud)

被编译为

上面编译代码的MSIL

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

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

意思是...

避免不得不求助于进行参数验证的静态方法的方法是使用合同重写器.您可以通过使用Contract.Requires或通过以结尾来表示代码块是前提条件来调用重写器Contract.EndContractBlock();.这样做会导致重写器在调用构造函数初始化程序之前将其放在方法的开头.