Liskov 替换原则与普通继承有何不同?

How Liskov substitution principle is different from normal inheritance?

我正在尝试了解 Liskov 替换原则。但我无法确定 Liskov 替换原则与正常继承有何不同。 下面的代码是关于普通继承的。我应该对下面的代码做些什么来说明我的代码遵循里氏替换原则

    public class ClassA
    {
        public virtual void MethodA()
        {
            Console.WriteLine("-----------------ClassA.MethodA---------------------");
        }

        public virtual void MethodB()
        {
            Console.WriteLine("-----------------ClassA.MethodB---------------------");
        }
    }

    public class ClassB: ClassA
    {
        public override void MethodA()
        {
            Console.WriteLine("-----------------ClassB override ClassA.MethodA---------------------");
        }
    }

这是一个通用的定义:

https://www.tomdalling.com/blog/software-design/solid-class-design-the-liskov-substitution-principle/

The Liskov Substitution Principle (LSP): functions that use pointers to base classes must be able to use objects of derived classes without knowing it.

这里有更严谨的解释:

https://en.wikipedia.org/wiki/Liskov_substitution_principle

..Liskov 替换原则 (LSP) 是子类型关系的特定定义,称为(强)行为子类型,最初由 Barbara Liskov 在 1987 年题为数据抽象和层次结构的会议主题演讲中引入。它是一种语义关系,而不仅仅是句法关系,因为它旨在保证层次结构中类型的语义互操作性,尤其是 object 类型。芭芭拉·利斯科夫 (Barbara Liskov) 和珍妮特·温 (Jeannette Wing) 在 1994 年的一篇论文中简要描述了该原理,如下所示:

子类型要求

Let ϕ ( x ) be a property provable about objects x of type T. Then ϕ ( y ) should be true for objects y of type S where S is a subtype of T.

Liskov 原则对较新的 object-oriented 编程语言中采用的签名施加了一些标准要求(通常在 类 级别而不是类型;请参阅名义与结构子类型以了解区别):

  • 子类型中方法参数的逆变。
  • 子类型中 return 类型的协方差。
  • 子类型的方法不应抛出新异常,除非这些异常本身是超类型方法抛出的异常的子类型。

除了签名要求外,子类型还必须满足一些行为条件。这些在类似于契约设计方法的术语中有详细说明,导致对契约如何与继承交互的一些限制:

  • 无法在子类型中加强先决条件。
  • 不能在子类型中弱化后置条件。
  • 超类型的不变量必须保留在子类型中。
  • 历史约束("history rule")。 Objects 被认为只能通过它们的方法(封装)进行修改。因为子类型可能会引入超类型中不存在的方法,所以这些方法的引入可能会允许子类型中的状态更改,而这在超类型中是不允许的。历史约束禁止这样做。

问:你的例子符合吗?

答:我不这么认为。因为 A.MethodA() 在语义上是 "different" 来自 B.MethodA()。 B 类未通过 The Duck Test.

我提议这个counter-example:

public class ClassA {
  public virtual void MethodA() {
    Console.WriteLine("ClassA.MethodA");
  }

  public virtual void MethodB(){
    Console.WriteLine("ClassA.MethodB");
  }
}

public class ClassC: ClassA {
  public void MethodC() {
    Console.WriteLine("ClassC.MethodC");
  }
}

这也是为什么 LSP 不是 "the same as" 继承的一个很好的例子。