3

对不起,如果这个问题似乎太长了。在我问它之前,我需要说明它来自哪里。

设置:

给定以下不可变类型Rectangle

class Rectangle
{
    public Rectangle(double width, double height) { … }

    public double Width  { get { … } }
    public double Height { get { … } }
}

…从中派生一个类型似乎是完全合法的Square

using System.Diagnostics.Contracts;

class Square : Rectangle
{
    public Square(double sideLength) : base(sideLength, sideLength) { }

    [ContractInvariantMethod]
    void WidthAndHeightAreAlwaysEqual()
    {
        Contract.Invariant(Width == Height);
    }
}

…因为派生类可以确保它自己的不变量永远不会被违反。

但是一旦我变得Rectangle可变:

class Rectangle
{
    public double Width  { get; set; }
    public double Height { get; set; }
    …
}

......我不应该再从中派生Square,因为Square不应该有独立的设置器Widthand Height

问题:

我可以用代码合同做什么,以便一旦我Square从可变Rectangle类派生它就会警告我违反合同?最好,Code Contracts 的静态分析已经在编译时给了我一个警告。

换句话说,我的目标是使用代码合同对以下规则进行编码:

  • WidthHeightaRectangle可以相互独立地改变。
  • WidthHeightaSquare不能相互独立地改变,这首先是没有意义的。

......并以这样一种方式进行,即每当这些规则“冲突”时,代码合同都会注意到。

到目前为止我所考虑的:

1. 添加一个不变量Rectangle

class Rectangle
{
    …
    [ContractInvariantMethod]
    void WidthAndHeightAreIndependentFromOneAnother()
    {
        Contract.Invariant(Width != Height || Width == Height);
    }
}

这种方法的问题在于,虽然不变量正确地说明了“宽度和高度不必相等,但它们可以相等”,但它是无效的,(1)因为它是重言式,(2)因为它比Width == Height派生类中的不变量限制更少Square。也许它甚至在代码契约看到它之前就被编译器优化掉了。

2. 给Rectanglesetter 添加后置条件:

public double Width
{
    get { … }
    set { Contract.Ensures(Height == Contract.OldValue(Height)); … }
}

public double Height
{
    get { … }
    set { Contract.Ensures(Width == Contract.OldValue(Width)); … }
}

这将禁止派生类简单Square地更新HeightWidth无论何时Width更改,反之亦然,它本身Square不会阻止我从Rectangle. 但这就是我的目标:让代码合同警告我Square不能从可变的Rectangle.

4

3 回答 3

4

最近的 MSDN 杂志上有一篇非常相关的文章,讨论了基本相同的问题:“代码契约:继承和 Liskov 原则” ——我想不出更好的答案。

您认为“非法”子类型化基本上违反了 Liskov 替换原则,这篇文章展示了代码契约如何帮助检测这个问题。

于 2011-08-07T23:15:09.490 回答
2

在2011 年 7 月的 MSDN 杂志上有一篇文章用相同的示例描述了您的问题,并讨论了代码合同的使用和 Liskov 替换原则。

于 2011-08-07T23:15:23.583 回答
0

感谢 @BrokenGlass 和 @Mathias 提供了指向最近 MSDN 杂志文章的链接,该文章名为Code Contracts: Inheritance and the Liskov Principle。虽然我认为其中有问题(我会在一秒钟内解决这个问题……请参阅此答案的第二部分),但它帮助我决定了解决方案。

我决定的解决方案:

  1. 我将以下后置条件添加到基类Rectangle

    public double Width
    {
        set { Contract.Ensures(Height == Contract.OldValue(Height)); }
    }
    
    public double Height
    {
        set { Contract.Ensures(Width == Contract.OldValue(Width)); }
    }
    

    这些基本上断言Width并且Height可以彼此独立地设置。

  2. 在派生类中添加了一个不变量Square

    Contract.Invariant(Width == Height);
    

    这基本上正好相反。

总而言之,这些最终将导致违反合同——诚然不是在编译时,但似乎只能在某些极端情况下(即,当派生类开始添加前置条件时)使用代码合同来完成。

为什么 MSDN 杂志文章中的解决方案没有帮助?

MSDN 文章似乎显示了一种具有编译时警告优势的解决方案。为什么我坚持上面没有这个奖励的解决方案?

简短的回答:

代码示例中的一个无法解释的变化清楚地表明,文章设置了一个人为的情况,以展示 Code Contracts 的静态分析。

长答案:

本文从一个Rectangle类的代码示例(→ 图 1)开始,其中 和 具有单独的设置WidthHeight。下一次显示Rectangle该类时(→ 图 2),设置器已创建private并且仅通过添加的方法使用SetSize(width, height)

文章没有说明为什么会默默地引入这个变化。事实上,这种变化在单独的上下文中可能根本没有任何意义Rectangle,除非你当然已经知道你会派生一个类Square,比如你需要添加一个前置条件的地方width == heightWidth当设置器和设置器Height彼此分离时,您不能将此添加为前提条件。如果您无法添加该前提条件,您将不会从代码合同中获得编译时警告。

于 2011-08-08T09:20:35.743 回答