C# 我是否可以获得代码合同来警告我;“非法”;子类型?

C# 我是否可以获得代码合同来警告我;“非法”;子类型?,c#,compiler-warnings,code-contracts,invariants,subtyping,C#,Compiler Warnings,Code Contracts,Invariants,Subtyping,抱歉,如果这个问题太长。在我问之前,我需要说明它是从哪里来的 设立: 给定以下不可变类型矩形: class Rectangle { public Rectangle(double width, double height) { … } public double Width { get { … } } public double Height { get { … } } } public double Width { set { Contract.Ensur

抱歉,如果这个问题太长。在我问之前,我需要说明它是从哪里来的

设立: 给定以下不可变类型
矩形

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

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

public double Height
{
    set { Contract.Ensures(Width == Contract.OldValue(Width)); }
}
…从中派生类型
Square
似乎完全合法:

using System.Diagnostics.Contracts;

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

    [ContractInvariantMethod]
    void WidthAndHeightAreAlwaysEqual()
    {
        Contract.Invariant(Width == Height);
    }
}
…因为派生类可以确保其自身的不变量不会被违反

但一旦我将
矩形
设置为可变:

class Rectangle
{
    public double Width  { get; set; }
    public double Height { get; set; }
    …
}
…我不应该再从它派生出
Square
,因为
Square
不应该有独立的
宽度和
高度设置器

问题: 如何处理代码契约,以便在我从可变的Rectangle类派生Square时,它会立即警告我契约违反?最好是,代码契约的静态分析已经在编译时给了我一个警告

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

  • 矩形的
    宽度
    高度
    可以相互独立地改变
  • 正方形的
    宽度
    高度
    不能相互独立地改变,而这一点从一开始就没有意义
…并以这样一种方式进行:每当这些规则“冲突”时,代码契约都会注意到

到目前为止,我考虑的是: 1。向
矩形添加不变量

class Rectangle
{
    …
    [ContractInvariantMethod]
    void WidthAndHeightAreIndependentFromOneAnother()
    {
        Contract.Invariant(Width != Height || Width == Height);
    }
}
public double Width
{
    get { … }
    set { Contract.Ensures(Height == Contract.OldValue(Height)); … }
}

public double Height
{
    get { … }
    set { Contract.Ensures(Width == Contract.OldValue(Width)); … }
}
这种方法的问题是,尽管不变量正确地表示“宽度和高度不一定相等,但它们可以相等”,但它是无效的,(1)因为它是重言式,(2)因为它比派生类
Square
中的不变量
宽度==高度
限制性更小。也许在代码契约看到它之前,编译器甚至已经对它进行了优化

2。将post条件添加到
矩形的setters:

class Rectangle
{
    …
    [ContractInvariantMethod]
    void WidthAndHeightAreIndependentFromOneAnother()
    {
        Contract.Invariant(Width != Height || Width == Height);
    }
}
public double Width
{
    get { … }
    set { Contract.Ensures(Height == Contract.OldValue(Height)); … }
}

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

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

最近的MSDN杂志上有一篇非常相关的文章讨论了基本相同的问题:-我无法找到更好的答案


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

中有一篇文章描述了您的问题,使用了相同的示例,并讨论了代码契约的使用和Liskov替换原理

感谢@BrokenGlass和@Mathias链接到MSDN杂志最近的一篇名为。虽然我相信这里面有问题(我马上就要谈到这个问题了……请看答案的第二部分),但它帮助我决定了解决方案

我决定的解决方案是:
  • 我正在将以下post条件添加到基类
    Rectangle

    class Rectangle
    {
        public Rectangle(double width, double height) { … }
    
        public double Width  { get { … } }
        public double Height { get { … } }
    }
    
    public double Width
    {
        set { Contract.Ensures(Height == Contract.OldValue(Height)); }
    }
    
    public double Height
    {
        set { Contract.Ensures(Width == Contract.OldValue(Width)); }
    }
    
    它们基本上断言
    宽度
    高度
    可以彼此独立设置

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

    Contract.Invariant(Width == Height);
    
    这基本上是相反的

  • 综上所述,这些最终将导致契约冲突——诚然不是在编译时,但似乎只有在某些特殊情况下(即,当派生类开始添加前置条件时)才能使用代码契约

    为什么MSDN杂志文章中的解决方案没有帮助? MSDN文章似乎展示了一个具有编译时警告优势的解决方案。为什么我要坚持使用上面的解决方案,而这个解决方案没有奖金

    简短回答:

    class Rectangle
    {
        …
        [ContractInvariantMethod]
        void WidthAndHeightAreIndependentFromOneAnother()
        {
            Contract.Invariant(Width != Height || Width == Height);
        }
    }
    
    public double Width
    {
        get { … }
        set { Contract.Ensures(Height == Contract.OldValue(Height)); … }
    }
    
    public double Height
    {
        get { … }
        set { Contract.Ensures(Width == Contract.OldValue(Width)); … }
    }
    
    代码示例中的一个无法解释的变化清楚地表明,本文设置了一个人为的场景,以展示代码契约的静态分析

    长答案:

    class Rectangle
    {
        …
        [ContractInvariantMethod]
        void WidthAndHeightAreIndependentFromOneAnother()
        {
            Contract.Invariant(Width != Height || Width == Height);
        }
    }
    
    public double Width
    {
        get { … }
        set { Contract.Ensures(Height == Contract.OldValue(Height)); … }
    }
    
    public double Height
    {
        get { … }
        set { Contract.Ensures(Width == Contract.OldValue(Width)); … }
    }
    
    本文从一个代码示例开始(→ 图1)一个
    矩形
    类,其
    宽度
    高度
    都有单独的设置器。下次显示
    矩形
    类时(→ 图2),设置器是私有的,仅通过添加的方法
    SetSize(宽度、高度)
    使用


    这篇文章并没有解释为什么会悄悄地引入这一变化。事实上,仅在
    Rectangle
    的上下文中,这种更改可能毫无意义,除非您已经知道将派生一个类,如
    Square
    ,其中需要添加一个前置条件
    width==height
    。当
    宽度
    高度
    的设置器彼此分离时,不能将此作为先决条件添加。如果您不能添加该前置条件,您将不会从代码契约中得到编译时警告。

    a-ha!“方法'Square.SetSize'覆盖了'Rectangle.SetSize',因此无法添加Requires.”太棒了!太糟糕了,它需要将
    宽度
    高度
    的设置器人工重构为一个组合的
    设置大小
    方法。。。我想知道是否有类似的解决方案覆盖了
    压缩变量方法
    s?也许我应该再解释一下我的上述评论:他们对
    矩形
    的整个重新设计使得两个setter组合成
    SetSize
    似乎是为了炫耀代码契约:重构对
    矩形
    (其中
    宽度的独立设置器