是否可以保证在调用链式构造函数之前对代码协定进行评估? [英] Are code contracts guaranteed to be evaluated before chained constructors are called?

查看:63
本文介绍了是否可以保证在调用链式构造函数之前对代码协定进行评估?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Before I started using Code Contracts I sometimes ran into fiddlyness relating to parameter validation when using constructor chaining.

用(做作的)示例最容易解释:

This is easiest to explain with a (contrived) example:

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().

I want the Test(string) constructor to chain the Test(int) constructor, and to do so I use int.Parse().

当然,int.Parse()不喜欢使用null参数,因此,如果 s 为null,它将在我到达验证行之前抛出:

Of course, int.Parse() doesn't like having a null argument, so if s is null it will throw before I reach the validation lines:

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

这使检查无效.

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

How to fix that? Well, I sometimes used to do this:

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);
    }
}

有点奇怪,当失败时堆栈跟踪不是理想的,但它可以工作.

That's a bit fiddly, and the stack trace isn't ideal when it fails, but it works.

现在,代码合同随之而来,所以我开始使用它们:

Now, along come Code Contracts, so I start using them:

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);
    }
}

一切顺利.它工作正常.但是后来我发现我可以做到这一点:

All well and good. It works fine. But then I discover that I can do this:

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

And then if I do var test = new Test(null), the Contract.Requires(s != null) is executed before this(int.Parse(s)). This means that I can do away with the convertArg() test altogether!

所以,请问我的实际问题:

  • 此行为在任何地方都有记录吗?
  • 在为像这样的链式构造函数编写代码协定时,我可以依靠这种行为吗?
  • 还有其他方法可以解决这个问题吗?

推荐答案

简短答案

是的,该行为记录在前提条件"的定义中,以及在不调用Contract.EndContractBlock的情况下如何处理旧式验证(如果/然后/抛出).

The short answer

Yes, the behavior is documented in the definition of "precondition", and in how legacy verification (if/then/throw) without a call to Contract.EndContractBlock is handled.

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

If you don't want to use Contract.Requires, you can change your constructor to

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

长答案

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

The long answer

When you place a Contract.* call in your code, you are not actually calling a member in the System.Diagnostics.Contracts namespace. For example, Contract.Requires(bool) is defined as:

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

AssertMustUseRewriter无条件地抛出ContractException,因此如果不重写已编译的二进制文件,则仅在定义CONTRACTS_FULL的情况下代码才会崩溃.如果未定义,则前提条件甚至不会检查,因为由于

AssertMustUseRewriter unconditionally throws a ContractException, so in absence of rewriting the compiled binary, the code will just crash if CONTRACTS_FULL is defined. If it is not defined, the pre-condition is never even checked, as the call to Requires is omitted by the C# compiler due to the presence of the [Conditional] attribute.

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

Based off of the settings selected in the project properties, Visual Studio will define CONTRACTS_FULL and call ccrewrite to generate the appropriate IL to check the contracts at runtime.

合同示例:

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进行编译,您将得到:

Compiled with csc program.cs /out:nocontract.dll, you get:

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运行,您将获得实际执行运行时检查的代码:

Compiled with csc program.cs /define:CONTRACTS_FULL /out:prerewrite.dll and run through ccrewrite -assembly prerewrite.dll -out postrewrite.dll you will get the code which will actually perform runtime checking:

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(一个先决条件)并没有真正移动,因为它已经在方法的顶部.

Of prime interest is that our Ensures (a postcondition) got moved to the bottom of the method, and our Requires (a precondition) didn't really move since it was already at the top of the method.

这符合文档的定义:

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

[Preconditions] are contracts on the state of the world when a method is invoked.
...
Postconditions are contracts on the state of a method when it terminates. In other words, the condition is checked just prior to exiting a method.

现在,您的方案中的复杂性存在于前提的定义中.根据上面列出的定义,前提条件在方法运行之前之前运行.问题在于C#规范指出必须在构造函数主体

Now, the complexity in your scenario exists in the very definition of a precondition. Based off of the definition listed above, the precondition runs before the method runs. The problem is that the C# specification says that the constructor initializer (chained constructor) must be invoked immediately before the constructor-body [CSHARP 10.11.1], which is at odds with the definition of a precondition.

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

The code that ccrewrite generates cannot therefore be represented as C#, as the language provides no mechanism to run code before the chained constructor (except by calling static methods in the chained constructor parameter list as you mention). ccrewrite, as required by the definition takes your constructor

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

被编译为

,并将对的调用移至对链式构造函数的调用之前:

and moves the call to requires to before the call to the chained constructor:

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

The way to avoid having to resort to static methods doing argument validation is to use the contract rewriter. You can invoke the rewriter by using Contract.Requires, or by signifying that a block of code is a precondition by ending it with Contract.EndContractBlock();. Doing so will cause the rewriter to place it at the start of the method, before the call to the constructor initializer.

这篇关于是否可以保证在调用链式构造函数之前对代码协定进行评估?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆