为什么里氏代换原理需要论证是逆变的?

2024-01-02

其中一项规则是里氏替换原则 https://en.wikipedia.org/wiki/Liskov_substitution_principle施加在派生类中的方法签名是:

子类型中方法参数的逆变。

如果我理解正确的话,就是说派生类的重写函数应该允许逆变参数(超类型参数)。但是,我无法理解这个规则背后的原因。由于 LSP 主要讨论将类型与子类型(而不是超类型)动态绑定以实现抽象,因此允许超类型作为派生类中的方法参数对我来说非常困惑。 我的问题是:

  • 为什么 LSP 允许/要求派生类重写中的逆变参数 功能?
  • 逆变规则如何有助于实现数据/过程抽象?
  • 现实世界中有没有我们需要传递逆变的例子 派生类的重写方法的参数?

在这里,按照 LSP 的说法,“派生对象”应该可用作“基础对象”的替代品。

假设您的基础对象有一个方法:

class BasicAdder
{
    Anything Add(Number x, Number y);
}

// example of usage
adder = new BasicAdder

// elsewhere
Anything res = adder.Add( integer1, float2 );

这里,“Number”是类似数字的数据类型、整数、浮点数、双精度数等基本类型的概念。C++ 中不存在这样的东西,但是,我们在这里不讨论特定的语言。类似地,仅出于示例的目的,“任何”描述任何类型的不受限制的值。

让我们考虑一个“专门”使用 Complex 的派生对象:

class ComplexAdder
{
    Complex Add(Complex x, Complex y);
}

// example of usage
adder = new ComplexAdder

// elsewhere
Anything res = adder.Add( integer1, float2 ); // FAIL

因此,我们只是破坏了 LSP:它不能用作原始对象的替代品,因为它无法接受integer1, float2参数,因为它实际上requires复杂的参数。

另一方面,请注意协变返回类型是可以的:复杂的返回类型将适合Anything.

现在,让我们考虑另一种情况:

class SupersetComplexAdder
{
    Anything Add(ComplexOrNumberOrShoes x, ComplexOrNumberOrShoes y);
}

// example of usage
adder = new SupersetComplexAdder

// elsewhere
Anything res = adder.Add( integer1, float2 ); // WIN

现在一切都正常了,因为无论谁使用旧对象,现在也可以使用新对象,而不会影响使用点。

当然,并不总是可以创建这样的“联合”或“超集”类型,特别是在数字方面,或者在某些自动类型转换方面。但是,我们并不是在谈论特定的编程语言。整体想法很重要。

还值得注意的是,您可以在不同的“级别”坚持或打破 LSP

class SmartAdder
{
    Anything Add(Anything x, Anything y)
    {
        if(x is not really Complex) throw error;
        if(y is not really Complex) throw error;

        return complex-add(x,y)
    }
}

它确实看起来像在类/方法签名级别上符合 LSP。但真的是这样吗?通常不会,但这取决于很多因素。

逆变规则如何有助于实现数据/过程抽象?

这对我来说是显而易见的。如果您创建的组件是可交换/可交换/可替换的:

  • BASE:简单地计算发票总和
  • DER-1:并行计算多个核上的发票总和
  • DER-2:计算带有详细记录的发票总和

然后添加一个新的:

  • 计算不同货币的发票总和

假设它处理欧元和英镑输入值。以旧货币(例如美元)输入怎么样?如果省略,则新组件is not替换旧的。您不能只是取出旧组件并插入新组件并希望一切都好。系统中的所有其他事物仍可能发送美元值作为输入。

如果我们创建从 BASE 派生的新组件,那么每个人都应该可以放心地假设他们可以在之前需要 BASE 的任何地方使用它。如果某个地方需要 BASE,但使用了 DER-2,那么我们应该能够在那里插入新组件。这就是LSP。如果我们不能,那么有些东西就坏了:

  • 任一使用地点不仅需要 BASE,而且实际上需要更多
  • 或者我们的组件确实不是 BASE(请注意 is-a 措辞)

现在,如果没有任何损坏,我们可以拿一个换成另一个,无论是美元还是英镑,是单核还是多核。现在,从上一层的大局来看,如果不再需要关心特定类型的货币,那么我们成功地将其抽象出来,大局会更简单,当然,组件需要在内部处理它不知何故。

如果这对数据/过程抽象没有帮助,那么看看相反的情况:

如果从 BASE 派生的组件不遵守 LSP,那么当美元的合法值到达时,它可能会引发错误。或者更糟糕的是,它不会注意到并将它们处理为英镑。我们出现了问题。为了解决这个问题,我们需要修复新组件(以遵守 BASE 的所有要求),或者更改其他相邻组件以遵循新规则,例如“现在使用欧元而不是美元,否则加法器将抛出异常”,或者我们需要添加一些东西到大局中来解决它,即添加一些分支来检测旧式数据并将它们重定向到旧组件。我们只是“泄露”了邻居的复杂性(也许我们迫使他们破坏 SRP),或者我们让“大局”变得更加复杂(更多的适配器、条件、分支……)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么里氏代换原理需要论证是逆变的? 的相关文章