是否有可能在构造函数中违反 Liskov 替换原则?

Is it possible to violate Liskov Substitution Principle in a constructor?

我刚刚安装了 Microsoft Code Contracts。它是 .NET Framework 和 Visual Studio add-on 的一部分。它提供了对已定义合约的运行时检查和静态检查。

该工具有四个警告级别,所以我设置了最高级别。

我已声明 classes 旨在违反 Liskov 替换原则。

public class Person
{
    protected int Age { get; set; }

    public Person(int age)
    {
        Contract.Requires(age > 0);
        Contract.Requires(age < 130);
        this.Age = age;
    }
}

public class Child : Person
{
    public Child(int age) : base(age)
    {
        Contract.Requires(age > 0); 
        Contract.Requires(age < Consts.AgeOfMajority);
        Contract.Requires(age < 130);
        this.Age = age;
    }
}

public static class Consts
{
    public readonly static int AgeOfMajority = 18;
}

LSP 状态:

if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of that program

在我的示例中,违规行为是此分配:Person person = new Child(23);。我们应该可以做到这一点,但我们做不到,因为 children 不能比 class 要求的年龄小一些。

然而分析的结果却令人惊讶CodeContracts: Checked 11 assertions: 11 correct。是我的例子错了还是代码契约没有检测到这样的事情?

虽然 LSP 确实指定子类型不能对方法设置更严格的先决条件,但这不适用于构造函数,因为您不以多态方式使用构造函数。

合同违规将是 new Child(23);,它发生在分配给 Person 之前。

所以示例违规是错误的,它没有创建子类型 S 的实例,更不用说用 T 替换它了。

我会说 liskov 替换控制 class 的构造实例的行为。因此,一个正确构造的 Child 实例可以毫无问题地替换 Person。

您对如何构建 Child 有限制。我没有看到框架没有将此标记为问题。

有一个著名的鸭子违反 LSP 的例子:

然而,我们不能在构造函数中违反它。假设我们有 Duck 和 WildDuck 类:

public abstract class Duck
{
    public abstract string Quack();
    public double Weight { get; set; }

    public Duck(double weight)
    {
        Contract.Requires(weight > 0);
        this.Weight = weight;
    }
}

public class WildDuck : Duck
{
    public WildDuck(double weight)
        : base(weight)
    {
        Contract.Requires(weight > 0);
        this.Weight = weight;
    }

    public override string Quack()
    {
        return "wild quack";
    }
}

下面介绍一下ElectricDuck:

public class ElectricDuck : Duck
{
    public Battery Battery { get; set; }

    public override string Quack()
    {
        return "electric quack";
    }

    public ElectricDuck(double weight, Battery battery)
        : base(weight)
    {
        Contract.Requires(weight > 0);
        Contract.Requires(battery != null);
        this.Weight = weight;
        this.Battery = battery;
    }
}

public class Battery
{
    public bool IsEmpty { get; set; }
}

乍一看它似乎违反了 LSP,因为 ElectricDuck 需要的不仅仅是 WildDuck 或 abstract Duck。但是只要ElectricDuck提供了Quack方法就可以了,不需要额外的要求。

如果 ElectricDuck 需要电池发光 - 从 LSP 的角度来看,这是完全正确的:

public void Glow()
{
    Contract.Requires(!this.Battery.IsEmpty);
}

当我们将要求添加到继承方法时,违反了 LSP:

public override string Quack()
{
    Contract.Requires(!this.Battery.IsEmpty);
    return "electric quack";
}

并且此修改将导致 CodeContracts 显示警告。