里氏原理中的论证逆变如何起作用?

How could argument contravariance in Liskov principle work?

Liskov 替换原则的基本要点是超类可以替换为遵循相同契约(行为)的子类。或者正如 Martin Fowler 所说:"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it."

提到参数逆变是 LSP 的一部分,但我似乎无法理解它为什么以及如何工作。即,在子类中,重写方法可以接受更宽的(派生更少的参数)。

像这样:

class Base
{
  int GetLenght(string s)
  {
     return s.lenght;
  }  
}
class Derived: Base
{
 override int GetLenght(object s)
  {
    ??  I cannot return any lenght of an object..
  }
}

这怎么行得通?我的意思是,如果派生较少的论证没有我需要的属性,我怎么能遵守合同呢?

PS:我知道大多数 OO 语言不支持,我只是好奇。

这可行,因为基 class 的消费者使用基 class 的接口调用它(他们不需要知道派生的 class),并且由于 subclass 的限制较少,任何可以传递给基本 class 方法的参数都可以传递给 subclass 方法。 (在您的示例中,Base 的使用者会将 string 传递给 Base.GetLenght(并且它不能传递任何其他类型,因为它引用的是 [=11= 的方法] 参数),它被 Derived.GetLenght 覆盖,并且 Derived.GetLenght 将接收对它有效的 string 参数。

subclass 的使用者可以将引用类型化为 subclass 的类型(即他们知道它的限制较少的接口),因此可以传递任何接受的参数子class。这个参数被传递给 subclass 方法(它可以接受那个参数)。当然,如果覆盖方法是调用基本 class 方法,它必须将参数转换(或替换)为对基本 class.

有效的参数

当然,覆盖方法必须能够处理它的参数。在示例中,这意味着它必须有一种获取任何对象的 'length' 的方法,并且必须定义其实际含义。

语义

一个写得很好的基础 class 将定义方法应该做什么的契约(例如,在注释中)。 在派生的 class 中符合 Liskov 替换原则也需要在语义上符合它(至少不矛盾),这样它就不会 return 出乎意料(但 句法 base class.

消费者的有效)值

在示例中,这可能意味着(取决于该方法的语义契约)如果参数是 string,覆盖方法必须 return 长度(需要检查其输入并转换它)。

例如,基本 class 方法可能有一条注释说明:returns the number of characters in the string and returns -1 if 's' is null.

重写方法可能有一条注释 Given any sequence or collection (including a string, which is treated as a sequence of characters for this purpose), this returns the number of elements in the sequence or collection, and returns -1 if the argument is null or not a collection or sequence. 请注意,根据此定义,它在传递字符串时与基 class 做同样的事情。如果它没有(例如,通过 returning 0 或在传递 null 时抛出异常),that 将违反 Liskov,即使其参数类型相同.

然后它必须通过检查参数的类型来实现它,并转换为适当的类型以获得集合的大小或字符串的长度(它可以转换并调用基 class 对于后者)。

可能的实现

在大多数面向对象的语言中,当两个方法的参数类型都是对象时(通常由一段以引用类型信息开头的数据实现), Derived.GetLenght 的虚方法 table 中的条目可以与具有相同参数类型(即直接指向该方法)的条目相同,并在编译时完成类型检查。

如果其中一个方法有一个与另一个不兼容的参数(例如,如果基数 class 采用了 int(编译器只为其传递了一个 32 位值)并且derived class 接受了一个对象),覆盖方法(在源代码中)将被编译为一个新方法(在虚拟方法 table 中有自己的条目),并且编译器可以在内部生成一个方法与基础 class 方法的原型,转换参数并将其传递给重写方法。 (后者是虚方法 table 条目在派生的 class 中指向的内容。)

如果在派生的 class 派生的 class 中再次重写该方法,也可以使用此机制,但编译器必须在内部为所有 superclass 方法(包括顶级方法),在这个最低的 class.

中转换和调用重写的方法

让我们稍微改变一下示例:

让我们暂时假设这里有一个实现了 GetLength 方法的接口 Sequence,并且 String 实现了这个接口。 我们还假设在您的示例中,使用的不是对象 Sequence,它是比 String 更广泛的类型(在这种情况下是它的实际实现)。

Base base;
Derived derived;

String s;
Sequence o;

int i;
i = base.GetLength(s); // valid
i = derived.GetLength(o); // valid
i = base.GetLength(o); // obviously invalid

base = derived;
base.GetLength(s); // valid
base.getLength(o); // still invalid, 
// the contract of "Base" still requires an argument of type string,
// despite actually being of type "Derived"

你的实际实现是无关紧要的,重要的是类型。只要你在获取字符串时不破坏功能,你就可以 return 任何让你的船漂浮的东西作为任意对象的长度,例如:

class Derived : Base {
    override int GetLenght(Sequence s) {
      return s.GetLength();
    }
}

如你所见,你可以给 derived any 类型的 Sequence,但是 Base 仍然需要 [=41] =]特定类型String

因此,逆变在很多情况下都不会违反 LSP。正如您在自己的示例中看到的那样,您不能使用 object 而不是 string 而不可能在这方面违反 LSP (您可以 Base/Derived还是不要违背LSP,LSP违背是隐藏在里面推导出来的,对外不可见)。

Eric Lippert 关于 C# 中的协变和逆变有一些非常棒的文章,首先是 Covariance and Contravariance in C#, Part One(到第 11 部分)。

可以在这里找到更多内容:
Covariance and Contravariance

作为旁注: 虽然不违反 LSP 是一件值得努力的事情,但它并不总是最经济的选择。使用第 3 方或遗留 API,有时简单地破坏 LSP 可以节省理智、时间和资源。

Argument contravariance is mentioned to be a part of LSP but I cannot seem to understand why and how it can work. I.e., in a subclass, the overriding method could accept wider (less derived argument).

首先让我们确保我们已经定义了我们的术语。

"Covariance" 是 关系和转换 的 属性。具体来说,属性 在特定转换 上维持特定关系。 "Contravariance" 与协方差相同,不同之处在于它是 保持特定关系但在转换过程中反转

举个例子吧。我手头有一个类型,我希望通过 T 转换为 Func<T> 的规则将其转换为不同的类型。我有一个类型之间的关系: "an expression of type X can be assigned to a variable of type Y" 例如,类型 Giraffe 的表达式可以分配给类型 Animal 的变量。转换是 协变的 因为关系在整个转换中被保留:类型 Func<Giraffe> 的表达式可以分配给类型 Func<Animal>.[=36= 的变量]

转换T转换为Action<T>反转关系:Action<Animal>可以赋值给Action<Giraffe>

但是Action<T>中的T是委托代表的方法的形参类型。因此,如您所见,我们可以对形式参数类型进行逆变。

这对方法覆盖意味着什么?当你说

class B 
{
  public virtual void M(Giraffe g) { b body }
}
class D : B 
{
  public override void M(Giraffe g) { d body }
}

这在逻辑上与

相同
class B 
{
  protected Action<Giraffe> a = g => { b body }; 
  public void M(Giraffe g) { this.a(g); }
}
class D : B 
{
  public D() {  
    this.a = g => { d body };
  }
}

对吧?我们用

替换 D 的构造函数是完全合法的
   this.a = some Action<Animal>

对吧?然而,C#——和大多数其他 OO 语言,但不是全部——不允许

class D : B 
{
  public override void M(Animal a) { d body }
}

尽管逻辑上它的工作原理与通用委托逆变工作一样好。这只是一个可以实现但从未实现的功能,因为还有很多更好的事情要做。

How could this ever work? I mean, how could I comply with the contract if the less derived argument does not have the properties I need?

好吧,如果你不能,那你就不会,对吗?

假设我需要

int CompareHeights(Giraffe g1, Giraffe g2)

我可以用一种方法来代替它,这看起来难以置信吗

int CompareHeights(Animal a1, Animal a2)

?我需要一个比较长颈鹿高度的方法,我有一个比较动物高度的方法,所以我完成了,对吧?

假设我需要

void Paint(Circle, Color)

我可以用一种方法替换它似乎难以置信吗

void Paint(Shape, Color)

?这对我来说似乎是合理的。我需要一个画圆圈的方法,我有一个画任何形状的方法,所以我完成了。

如果我需要

int GetLength(string)

我有

int GetLength(IEnumerable)

那我就好了。我需要一种获取字符串长度的方法,该字符串是一个字符序列。我有一个方法可以得到任何序列的长度,所以我很好。