Mac*_*Mac 7 liskov-substitution-principle contravariance
Liskov替代原理对派生类中的方法签名施加的规则之一是:
子类型中方法参数的矛盾性。
如果我理解正确的话,就是说派生类的重写函数应允许使用自变量(超类型自变量)。但是,我不明白这条规则背后的原因。由于LSP主要讨论动态地将类型与那里的子类型(而不是超类型)进行绑定以实现抽象,因此让超类型作为派生类中的方法参数对我来说是很困惑的。我的问题是:
在这里,按照 LSP 所说的,“派生对象”应该可用作“基础对象”的替代品。
假设您的基础对象有一个方法:
class BasicAdder
{
Anything Add(Number x, Number y);
}
// example of usage
adder = new BasicAdder
// elsewhere
Anything res = adder.Add( integer1, float2 );
Run Code Online (Sandbox Code Playgroud)
这里,“Number”是类似数字的数据类型、整数、浮点数、双精度数等的基本类型的概念。即 C++ 中不存在这样的东西,但是,我们在这里不讨论特定的语言。同样,仅出于示例的目的,“Anything”描述了任何类型的不受限制的值。
让我们考虑一个“专门”使用 Complex 的派生对象:
class ComplexAdder
{
Complex Add(Complex x, Complex y);
}
// example of usage
adder = new ComplexAdder
// elsewhere
Anything res = adder.Add( integer1, float2 ); // FAIL
Run Code Online (Sandbox Code Playgroud)
因此,我们刚刚破坏了 LSP:它不能用作原始对象的替代品,因为它不能接受integer1, float2参数,因为它实际上需要复杂的参数。
另一方面,请注意协变返回类型是可以的: Complex as return type will fit Anything。
现在,让我们考虑另一种情况:
class SupersetComplexAdder
{
Anything Add(ComplexOrNumberOrShoes x, ComplexOrNumberOrShoes y);
}
// example of usage
adder = new SupersetComplexAdder
// elsewhere
Anything res = adder.Add( integer1, float2 ); // WIN
Run Code Online (Sandbox Code Playgroud)
现在一切正常,因为无论谁使用旧对象,现在也可以使用新对象,而不会影响使用点。
当然,创建这样的“联合”或“超集”类型并不总是可能的,尤其是在数字方面,或者在一些自动类型转换方面。但是,我们不是在谈论特定的编程语言。总体思路很重要。
还值得注意的是,您可以在各个“级别”上坚持或打破 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)
}
}
Run Code Online (Sandbox Code Playgroud)
它肯定看起来像在类/方法签名级别符合 LSP。但是是吗?通常不会,但这取决于很多事情。
逆变规则如何有助于实现数据/过程抽象?
很好……对我来说很明显。如果您创建组件,这些组件是可交换/可交换/可替换的:
然后添加一个新的:
假设它处理欧元和英镑输入值。旧货币的投入怎么样,比如美元?如果忽略这一点,那么新的组件不是替换旧的。您不能只是取出旧组件并插入新组件并希望一切正常。系统中的所有其他事物仍可能会发送美元值作为输入。
如果我们创建从 BASE 派生的新组件,那么每个人都应该安全地假设他们可以在之前需要 BASE 的任何地方使用它。如果某个地方需要 BASE,但使用了 DER-2,那么我们应该能够在那里插入新组件。这是 LSP。如果我们不能,那么某些东西就坏了:
现在,如果没有任何问题,我们可以将一个替换为另一个,无论是美元还是英镑,还是单核还是多核。现在,从上一层看大图,如果不再需要关心具体的货币种类,那么我们成功地将其抽象出来,大图会更简单,当然,组件需要内部处理不知何故。
如果这对数据/过程抽象没有帮助,那么看看相反的情况:
如果派生自 BASE 的组件不遵守 LSP,那么当美元合法值到达时,它可能会引发错误。或者更糟的是,它不会注意到并将它们作为英镑处理。我们出现了问题。为了解决这个问题,我们需要要么修复新组件(以遵守 BASE 的所有要求),要么更改其他相邻组件以遵循新规则,例如“现在使用欧元而不是美元,否则加法器将抛出异常”,或者我们需要添加一些东西到大局来解决它,即添加一些分支来检测旧式数据并将它们重定向到旧组件。我们只是将复杂性“泄露”给了邻居(也许我们强迫他们破坏了 SRP),或者我们使“大局”更加复杂(更多的适配器、条件、分支……)。
短语“方法参数的逆变”可能很简洁,但它是模棱两可的。让我们以此为例:
class Base {
abstract void add(Banana b);
}
class Derived {
abstract void add(Xxx? x);
}
Run Code Online (Sandbox Code Playgroud)
现在,“方法参数的逆变”可能意味着Derived.add必须接受任何具有类型Banana或超类型的对象,例如? super Banana. 这是对 LSP 规则的错误解释。
实际解释是:“Derived.add必须使用类型声明Banana,就像 in 一样Base,或者使用Banana诸如 的某个超类型声明Fruit。” 您选择哪种超类型取决于您。
我相信使用这种解释不难看出该规则是完全合理的。您的子类与父 API 兼容,但它也可以选择涵盖基类不支持的额外情况。因此,它是基类的 LSP 可替代的。
实际上,在子类中扩展类型很有用的例子并不多。我想这就是为什么大多数语言都懒得去实现它。要求严格相同的类型也会保留 LSP,只是不能在实现 LSP 的同时为您提供完全的灵活性。