代码合约无法反转条件?

时间:2014-07-10 15:02:03

标签: c# .net code-contracts

我有这个结构(简化为简洁):

public struct Period
{
    public Period(DateTime? start, DateTime? end) : this()
    {
        if (end.HasValue && start.HasValue && end.Value < start.Value)
        {
            throw new ArgumentOutOfRangeException("end", "...");
        }
        Contract.EndContractBlock();

        this.start = start;
        this.end = end;
    }

    private readonly DateTime? start;
    private readonly DateTime? end;

    public static Period operator +(Period p, TimeSpan t)
    {
        Contract.Assume(!p.start.HasValue || !p.end.HasValue || p.start.Value <= p.end.Value);
        return new Period(
            p.start.HasValue ? p.start.Value + t : (DateTime?) null,
            p.end.HasValue ? p.end.Value + t : (DateTime?) null);
    }
}

但静态检查员正在给我这个警告:

  

CodeContracts:需要未经证实:end.HasValue&amp;&amp; start.HasValue&amp;&amp; end.Value&gt; = start.Value

从自定义参数验证中推断出的这个要求是完全错误的。我想允许startend的空值,如果两者都提供,则只需要start <= end。但是,如果我将构造函数更改为:

public Period(DateTime? start, DateTime? end) : this()
{
    Contract.Requires(!start.HasValue || !end.HasValue || start.Value <= end.Value);
    this.start = start;
    this.end = end;
}

我收到此警告,看起来更正确,但很难理解为何无法证明这些要求:

  

CodeContracts:需要未经证实:!start.HasValue || !end.HasValue || start.Value&lt; = end.Value

我认为它可能会遇到?:的问题,但是当我将操作符更改为:

时,此警告仍然存在
public static Period operator +(Period p, TimeSpan t)
{
    var start = p.start.HasValue ? p.start.Value + t : (DateTime?) null;
    var end = p.end.HasValue ? p.end.Value + t : (DateTime?) null;

    Contract.Assume(!start.HasValue || !end.HasValue || start.Value <= end.Value);
    return new Period(start, end);
}

当然,如果我将.Requires更改为.Assume,则警告会完全消失,但这不是一个可接受的解决方案。

因此,Code Contracts中的静态检查器似乎无法正确反转条件。它不是简单地通过用!(…)包裹它或应用De Morgan's law来反转条件(如上所示),而是反转条件的最后一个组成部分。使用自定义参数验证时,静态检查程序无法正确解释复杂条件吗?

有趣的是,我试过这个,认为静态检查器会将!从前面剥离,但不会:

if (!(!start.HasValue || !end.HasValue || start.Value <= end.Value))
{
    throw new ArgumentOutOfRangeException("end", "...");
}
Contract.EndContractBlock();
  

CodeContracts:需要未经证实:!(!(!start.HasValue ||!end.HasValue || start.Value&lt; = end.Value))

在这种情况下,它 只用!(…)包裹整个条件,即使它没有。

另外,如果我将可空的DateTime更改为普通的非可空DateTime并重写这样的合同,它会按预期工作而不会发出任何警告:

public struct Period
{
    public Period(DateTime start, DateTime end) : this()
    {
        Contract.Requires(start <= end);
        this.start = start;
        this.end = end;
    }

    private readonly DateTime start;
    private readonly DateTime end;

    public static Period operator +(Period p, TimeSpan t)
    {
        Contract.Assume(p.start + t <= p.end + t);   // or use temp variables
        return new Period(p.start + t <= p.end + t);
    }
}

但仅使用Contract.Assume(p.start <= p.end)将无效。

  

CodeContracts:需要unproven:start&lt; = end

1 个答案:

答案 0 :(得分:3)

我认为部分问题可能是您在Contract.Requires电话中使用的条件。

以你的一个构造函数为例:

public Period(DateTime? start, DateTime? end) : this()
{
    Contract.Requires(!start.HasValue || !end.HasValue || start.Value <= end.Value);
    this.start = start;
    this.end = end;
}

如果start.HasValuefalse(意味着!start.HasValuetrue),但end确实有值,该怎么办? start.value <= end.Value在这种情况下评估的是什么,因为一个是null而另一个是值?

相反,您的Contract.Requires条件应如下所示:

Contract.Requires(!(start.HasValue && end.HasValue) || start.Value <= end.Value);

如果startend中的任何一个没有值,则条件将返回true(并且OR条件短路,从不评估是否start.Value <= end.Value)。但是,如果startend都分配了值,则条件的第一部分返回false,此时start.Value必须小于或等于end.Value以使条件总体评估为true。这就是你追求的目标。

以下是您的问题:Period的任何实例都要求start.Value <= end.Value或其中一个或另一个(或两者)null,这是真的吗?如果是这样,您可以将其指定为不变。这意味着在任何方法进入或退出时,!(start.HasValue && end.HasValue) || start.Value <= end.Value必须成立。这可以在合理的时候简化你的合同。

<强>更新

查看我在评论(TDD and Code Contracts)中发布的博客文章,您可以使用代码合约operator +(Period p, TimeSpan t)属性安全地注释您的PureAttribute实施。此属性告诉Code Contracts静态分析器该方法不会改变调用该方法的对象的任何内部状态,因此不会产生副作用:

[Pure]
public static Period operator +(Period p, TimeSpan t)
{
    Contract.Requires(!(p.start.HasValue && p.end.HasValue) || p.start.Value <= p.end.Value)

    return new Period(
        p.start.HasValue ? p.start.Value + t : (DateTime?) null,
        p.end.HasValue ? p.end.Value + t : (DateTime?) null);
}

<强>更新

好的,我更多地想到了这一点,我想我现在明白了Code Contracts与你的合同有关的问题。我认为您还需要在构造函数中添加Contract.Ensures合约(即帖子条件合约):

public Period(DateTime? start, DateTime? end) : this()
{
    Contract.Requires(!(start.HasValue && end.HasValue) || start.Value <= end.Value);
    Contract.Ensures(!(this.start.HasValue && this.end.HasValue) || this.start.Value <= this.end.Value);
    this.start = start;
    this.end = end;
}

这告诉代码合同,当构造函数退出时,对象的startend字段(如果它们都有值)必须满足start.Value <= end.Value的条件。如果不满足该条件,(可能)代码合同将抛出异常。这也应该有助于静态分析仪。

更新(再次,主要是为了完整性)

我在&#34;未经证实的&#34;上做了更多的调查。警告。 RequiresEnsures都可能发生这种情况。这是另一个遇到类似问题的人的例子:http://www.go4answers.com/Example/ensures-unproven-contractresult-79084.aspx

添加合约不变量可以按如下方式进行(对于OP所讨论的代码):

[ContractInvariantMethod]
protected void PeriodInvariants()
{
    Contract.Invariant(!(start.HasValue && end.HasValue) || start.Value <= end.Value);
}

每次进入/退出对象的方法时都会调用此方法,以确保此条件成立。

另一篇应该证明有趣的博客文章

我发现了另一个可能有趣的博客条目:http://www.rareese.com/blog/net-code-contracts

在这种情况下,我不同意作者的解决方案&#34;摆脱requires unproven警告。这是作者的代码:

public static void TestCodeContract(int value)
{
    if(value > 100 && value < 110)
        TestLimits(value); 
}

public static void TestLimits(int i)
{
    Contract.Requires(i > 100);
    Contract.Requires(i < 110);

    //Do Something
}

在这里,问题的真正解决方案应该如下:

public static void TestCodeContract(int value)
{
    Contract.Requires(value > 100 && value < 110);
    // You could alternatively write two Contract.Requires statements as the blog
    // author originally did.
}

这也应该消除警告,因为静态分析器现在知道value必须在101到109的范围内,这也恰好满足TestLimits方法的合同标准。

因此,我建议您检查调用Period构造函数的位置和/或Period.operator +(...)方法,以确保调用方法也具有必要的Contract.Requires语句(或者,Contract.Assume,它告诉静态分析器假设提供的条件为真。)

使用代码合约时,您需要检测所有代码。你通常不能选择&#34;哪个部分用于指定合同,因为静态分析器很可能没有足够的信息来完成其分析(因此,确保合同得到保证),您将收到许多警告。