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