C# 是否保证在调用链式构造函数之前对代码契约进行评估?

C# 是否保证在调用链式构造函数之前对代码契约进行评估?,c#,code-contracts,constructor-chaining,C#,Code Contracts,Constructor Chaining,在开始使用代码契约之前,在使用构造函数链接时,我有时会遇到与参数验证相关的烦躁 这是最容易用(人为的)例子来解释的: 我希望Test(string)构造函数链接Test(int)构造函数,为此我使用int.Parse() 当然,int.Parse() if (s == null) throw new ArgumentNullException("s"); 这使得支票没用了 如何解决这个问题?嗯,我有时会这样做: class Test { public Test(int i)

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

这是最容易用(人为的)例子来解释的:

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

当然,
int.Parse()

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)
,则在
这个(int.Parse(s))
之前执行
Contract.Requires(s!=null)
。这意味着我可以完全取消
convertArg()
测试

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

  • 这种行为是否有记录在案
  • 在为这样的链式构造函数编写代码契约时,我可以依赖这种行为吗
  • 我还有别的办法吗
简短的回答 是的,行为记录在“前提条件”的定义中,以及在不调用
Contract.EndContractBlock
的情况下如何处理遗留验证(if/then/throw)

如果您不想使用合同要求,可以将构造函数更改为

public Test(string s): this(int.Parse(s))
{
    if (s == null)
        throw new ArgumentNullException("s");
    Contract.EndContractBlock();
}
长话短说 当您在代码中放置
合约。*
调用时,实际上并不是在
System.Diagnostics.Contracts
命名空间中调用成员。例如,
Contract.Requires(bool)
定义为:

[Conditional("CONTRACTS_FULL")]
public static void Requires(bool condition) 
{
    AssertMustUseRewriter(ContractFailureKind.Precondition, "Requires"); 
}
AssertMustUseRewriter
无条件抛出一个
ContractException
,因此在不重写编译的二进制文件的情况下,如果定义了
CONTRACTS\u FULL
,代码将崩溃。如果未定义它,则前置条件甚至不会被检查,因为C#编译器会由于存在的而忽略对
Requires
的调用

重写者 基于在项目属性中选择的设置,Visual Studio将定义
CONTRACTS\u 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/define:CONTRACTS\u 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;
}
最令人感兴趣的是,我们的
确保了
(后条件)被移动到方法的底部,而我们的
要求
(先决条件)没有真正移动,因为它已经位于方法的顶部

这符合以下条件:

[前提条件]是调用方法时世界状态的契约。

后置条件是方法终止时状态的契约。换言之,在退出方法之前检查条件

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

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

public Test(string s)
    : this(int.Parse(s))
{
    Contract.Requires(s != null);
}
它被编译为

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

也就是说。。。 避免使用静态方法进行参数验证的方法是使用契约重写器。您可以使用
Contract.Requires
调用重写器,或者通过以
Contract.EndContractBlock()结尾表示代码块是先决条件来调用重写器。这样做将导致重写器将其放置在方法的开头,在调用构造函数初始值设定项之前

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;
}
public Test(string s)
    : this(int.Parse(s))
{
    Contract.Requires(s != null);
}