在开始使用代码契约之前,我在使用构造函数链时遇到与参数验证有关的繁琐。
这是一个(人为的)例子最简单的解释:
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);
}
}
这有点繁琐,当它失败时堆栈跟踪并不理想,但它有效。
现在,随着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);
}
}
一切都很好。它工作正常。但后来我发现我可以这样做:
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()
测试!
所以,关于我的实际问题:
答案 0 :(得分: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
无条件地抛出ContractException
,因此在没有重写编译的二进制文件的情况下,如果定义了CONTRACTS_FULL
,代码就会崩溃。如果未定义,则前提条件永远不会被检查,因为由于存在[Conditional]
attribute,C#编译器会忽略对Requires
的调用。
根据项目属性中选择的设置,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
(一个先决条件)并没有真正移动,因为它已经位于顶部方法。
这符合documentation's definition:
[Preconditions]是调用方法时世界状态的契约 ...
后置条件是终止时方法状态的契约。换句话说,在退出方法之前检查条件。
现在,您的场景中的复杂性存在于前置条件的定义中。基于上面列出的定义,前提条件在方法运行之前运行。问题是C#规范说构造函数初始化器(链式构造函数)必须在构造函数体[CSHARP 10.11.1]之前立即调用,这与前置条件的定义不一致。
ccrewrite
生成的代码因此不能表示为C#,因为该语言没有提供在链式构造函数之前运行代码的机制(除非通过在链接的构造函数参数列表中调用静态方法)。 ccrewrite
,根据定义的要求采用构造函数
public Test(string s)
: this(int.Parse(s))
{
Contract.Requires(s != null);
}
编译为
并在调用链式构造函数之前将调用移到require:
避免不得不求助于进行参数验证的静态方法的方法是使用合同重写器。您可以使用Contract.Requires
来调用重写器,或者通过用Contract.EndContractBlock();
结束代码块来表示代码块是一个先决条件。这样做会导致重写器在调用构造函数初始化程序之前将其放在方法的开头。