我可以获得代码合同来警告我有关“非法”的行为吗?子类型?

发布于 2024-11-28 21:48:10 字数 2318 浏览 1 评论 0原文

抱歉,如果这个问题看起来太长。在我提出问题之前,我需要先表明它来自哪里。

设置:

给定以下不可变类型 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 不应该有独立的设置器宽度高度

问题:

我可以如何处理代码契约,以便在我从可变 Rectangle 类派生 Square 时立即警告我违反契约?最好是,Code Contracts 的静态分析已经在编译时向我发出警告。

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

  • 矩形WidthHeight可以彼此独立地更改。
  • SquareWidthHeight 不能彼此独立地更改,这从一开始就没有意义。

……并以这样的方式进行:每当这些规则发生“冲突”时,代码合约就会注意到。

到目前为止我所考虑的:

1。向矩形添加不变量:

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

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

2.向 Rectangle 的设置器添加后置条件:

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

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

这将禁止派生类 Square 简单地将 Height 更新为 每当 Width 更改时,它不会阻止我从 Square 类派生 Square 类,反之亦然。 >矩形。但这就是我的目标:让代码契约警告我 Square 不得从可变的 Rectangle 派生。

Sorry if this question seems too long. Before I can ask it, I need to show where it's coming from.

Set-up:

Given the following immutable type Rectangle:

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

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

… it seems perfectly legal to derive a type Square from it:

using System.Diagnostics.Contracts;

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

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

… because the derived class can make sure that its own invariant isn't ever violated.

But as soon as I make Rectangle mutable:

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

… I should no longer derive Square from it, because Square shouldn't ever have independent setters for Width and Height.

Question:

What can I do with Code Contracts such that it will warn me of a contract violation as soon as I derive Square from the mutable Rectangle class? Preferably, Code Contracts' static analysis would already give me a warning at compile-time.

In other words, my goal is to encode the following rules with Code Contracts:

  • Width and Height of a Rectangle may be changed independently from one another.
  • Width and Height of a Square cannot be changed independently from one another, and that wouldn't be meaningful in the first place.

… and do it in such a way that Code Contracts will notice whenever these rules "collide".

What I've considered so far:

1. Adding an invariant to Rectangle:

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

The problem with this approach is that while the invariant correctly states, "Width and Height don't have to be equal, but they can be", it is ineffectual, (1) because it is a tautology, and (2) because it is less restrictive than the invariant Width == Height in the derived class Square. Perhaps it's even optimised away by the compiler before Code Contracts ever sees it.

2. Adding post-conditions to Rectangle's setters:

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

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

This will forbid the derived class Square from simply updating Height to Width whenever Width is changed and vice versa, it won't stop me per se from deriving a Square class from Rectangle. But that's my goal: getting Code Contracts to warn me that Square must not be derived from a mutable Rectangle.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

桜花祭 2024-12-05 21:48:11

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

您所认为的“非法”子类型基本上违反了里氏替换原则,本文展示了代码合约如何帮助检测此问题。

There was a very relevant article in a recent MSDN magazine discussing basically the same problem: "Code Contracts: Inheritance and the Liskov Principle" - I wouldn't be able to come up with a better answer.

What your regard as “illegal” subtyping is basically a violation of the Liskov substitution principle, the article shows how code contracts can help detect this problem.

小嗷兮 2024-12-05 21:48:11

MSDN 杂志 2011 年 7 月号中有一篇文章描述了很多你的问题,用同样的例子,并讨论代码契约的使用和里氏替换原则。

There is an article in the July 2011 issue of MSDN Magazine which describes pretty much your problem, with the same example, and discusses the use of Code Contracts and the Liskov substitution principle.

相思碎 2024-12-05 21:48:11

感谢 @BrokenGlass 和 @Mathias 提供了最近 MSDN 杂志文章的链接,该文章名为 代码契约:继承和里氏原则。虽然我认为其中有问题(我稍后会解决这个问题......请参阅此答案的第二部分),但它帮助我决定了解决方案。

我决定的解决方案:

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

    公共双宽度
    {
        设置 { Contract.Ensures(Height == Contract.OldValue(Height)); }
    }
    
    公共双高
    {
        设置 { Contract.Ensures(Width == Contract.OldValue(Width)); }
    }
    

    这些基本上断言宽度高度可以彼此独立设置。

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

    Contract.Invariant(宽度==高度);
    

    这基本上表明了相反的情况。

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

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

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

简答:

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

长答案:

本文以 Rectangle 类的代码示例(→ 图 1)开始,其中包含 Width的单独设置器>高度。下次显示 Rectangle 类时(→图 2),setter 已设为私有,并且只能通过添加的方法 SetSize(width,高度)

文章没有说解释为什么默默地引入这个改变。事实上,这种更改在单独的 Rectangle 上下文中可能根本没有任何意义,除非您当然已经知道您将派生一个类似 Square 的类,您可以在其中派生一个类似 Square 的类。需要添加一个前置条件宽度==高度。当 WidthHeight 的设置器彼此分离时,您无法将其添加为先决条件。如果您无法添加该前提条件,您将不会从代码契约中收到编译时警告。

Thanks to @BrokenGlass and @Mathias for the link to the recent MSDN Magazine article named Code Contracts: Inheritance and the Liskov Principle. While I believe there's something wrong in it (I'll get to that in a second… see the second part of this answer), it's helped me decide on a solution.

Solution I've decided on:

  1. I'm adding the following post-conditions to the base class Rectangle:

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

    These basically assert that Width and Height can be set independently from one another.

  2. One invariant is added in the derived class Square:

    Contract.Invariant(Width == Height);
    

    This basically states just the opposite.

Taken together, these will eventually result in a contract violation — admittedly not at compile-time, but it seems that could only be done with Code Contracts in certain corner cases (namely, when a derived class starts adding pre-conditions.)

Why is the solution in the MSDN Magazine article not helpful?

The MSDN article seemingly shows a solution that has the advantage of compile-time warnings. Why do I stick with the above solution that doesn't have this bonus?

Short answer:

An unexplained change in the code examples makes it clear that the article sets up an artificial situation in order to show off Code Contracts' static analysis.

Long answer:

The article starts off with a code example (→ Figure 1) of a Rectangle class with individual setters for both Width and Height. The next time that the Rectangle class is shown (→ Figure 2), the setters have been made private and are only used through an added method SetSize(width, height).

The article does not say explain why this change was silently introduced. In fact, that change would probably not make any sense at all in the context of Rectangle alone, unless of course you already knew that you would derive a class like Square where you need to add a pre-condition width == height. You couldn't add this as a pre-condition when the setters for Width and Height are separated from one another. And if you could not add that pre-condition, you would not get a compile-time warning from Code Contracts.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文