我可以获得代码合同来警告我有关“非法”的行为吗?子类型?
抱歉,如果这个问题看起来太长。在我提出问题之前,我需要先表明它来自哪里。
设置:
给定以下不可变类型 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 的静态分析已经在编译时向我发出警告。
换句话说,我的目标是使用代码契约对以下规则进行编码:
矩形
的Width
和Height
可以彼此独立地更改。Square
的Width
和Height
不能彼此独立地更改,这从一开始就没有意义。
……并以这样的方式进行:每当这些规则发生“冲突”时,代码合约就会注意到。
到目前为止我所考虑的:
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
andHeight
of aRectangle
may be changed independently from one another.Width
andHeight
of aSquare
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
最近的 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.
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.
感谢 @BrokenGlass 和 @Mathias 提供了最近 MSDN 杂志文章的链接,该文章名为 代码契约:继承和里氏原则。虽然我认为其中有问题(我稍后会解决这个问题......请参阅此答案的第二部分),但它帮助我决定了解决方案。
我决定的解决方案:
我将以下后置条件添加到基类
Rectangle
:这些基本上断言
宽度
和高度
可以彼此独立设置。在派生类
Square
中添加一个不变量:这基本上表明了相反的情况。
总而言之,这些将最终导致合同违规——诚然不是在编译时,但似乎只能在某些极端情况下使用代码合同来完成(即,当派生类开始添加前提条件。)
为什么 MSDN 杂志文章中的解决方案没有帮助?
MSDN 文章似乎展示了一种具有编译时警告优势的解决方案。为什么我坚持使用上面没有这个好处的解决方案?
简答:
代码示例中的一个无法解释的变化清楚地表明,本文设置了一个人为的情况,以展示代码契约的静态分析。
长答案:
本文以
Rectangle
类的代码示例(→ 图 1)开始,其中包含Width
和的单独设置器>高度
。下次显示Rectangle
类时(→图 2),setter 已设为私有
,并且只能通过添加的方法SetSize(width,高度)
。文章没有说解释为什么默默地引入这个改变。事实上,这种更改在单独的
Rectangle
上下文中可能根本没有任何意义,除非您当然已经知道您将派生一个类似Square
的类,您可以在其中派生一个类似Square
的类。需要添加一个前置条件宽度==高度
。当Width
和Height
的设置器彼此分离时,您无法将其添加为先决条件。如果您无法添加该前提条件,您将不会从代码契约中收到编译时警告。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:
I'm adding the following post-conditions to the base class
Rectangle
:These basically assert that
Width
andHeight
can be set independently from one another.One invariant is added in the derived class
Square
: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 bothWidth
andHeight
. The next time that theRectangle
class is shown (→ Figure 2), the setters have been madeprivate
and are only used through an added methodSetSize(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 likeSquare
where you need to add a pre-conditionwidth == height
. You couldn't add this as a pre-condition when the setters forWidth
andHeight
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.