如果 Car 是 Vehicle 的子类型,为什么 Vehicle->void 被视为 Car->void 的子类型?

If Car is a subtype of Vehicle, why is Vehicle->void considered a subtype of Car->void?

提问人:John V 提问时间:10/12/2018 最后编辑:TamaMcGlinnJohn V 更新时间:10/12/2018 访问量:202

问:

阅读一篇关于继承的基础论文,我无法理解下面所示的推理。显然这是正确的,因为逆变确实有效,我只是想了解其中的道理。 首先,它表明:

  • 如果函数返回 Car,则它是返回 Vehicle 的函数的子类型。那是因为所有的车辆也是汽车。(返回类型的协方差)。
  • 如果一个函数接受 Vehicle,那么它就是接受 Car 的函数的子类型,因为“一般来说,车辆上的每个函数也是 汽车。
    我无法理解这种反转的解释,所以我在下面展示这个:
    enter image description here

我幼稚的解释:

由于所有车辆上的函数都将适用于所有车辆,因此函数 take Vehicle 是函数 take Car 的子类型,因为 Vehlice 的函数集较小 - Car 可以具有更多功能。

继承 函数编程 语言不可知 协方差 逆变

评论

2赞 GSerg 10/12/2018
所有车辆都是车辆,但并非所有车辆都是车辆 (C <= V)。所有在车辆上运行的功能都可以在汽车上运行,但并非所有汽车的功能都可以在车辆上运行 (f(V) <= f(C))。

答:

2赞 TamaMcGlinn 10/12/2018 #1

你的问题是关于函数的协方差和逆方差,我认为如果我们将论文中一些与语言无关的符号关系映射到实际代码中,这将有助于你的理解。在 C++ 中,这里讨论的函数是:

int GetSpeedOf(Vehicle vehicle);

必须从Liskov替代的角度来理解亚型;如果一个函数需要任何类型的动物,你可以给它一只猫,一切都应该可以正常工作,但情况并非如此;需要猫的功能不能在任何类型的动物上起作用。

假设您了解可以将 Car 传递给 GetSpeedOf 函数,现在我们将解决函数接受函数的更复杂的情况,这带来了逆变。

下面的 CarWrapper 有一辆私家车,它将使用外部提供的功能对它做一些事情。该功能必须适用于汽车。因此,如果你给出一个更普遍适用于所有战车的功能,那就没问题了。

#include <functional>

class Vehicle { };
class Car : public Vehicle { };

class CarWrapper
{
    Car car;
    typedef std::function<auto(Car) -> void> CarCallback;
    CarCallback functionPtr;

public:
    void PassCallback(CarCallback cb)
    {
        functionPtr = cb;
    }

    void DoStuff()
    {
        functionPtr(car);
    }
};

void Works(Vehicle v){}

int main() {
    CarWrapper b;
    b.PassCallback(&Works);
}

因此,PassCallback 函数需要类型 CarCallback(论文中的“Car -> void”),但我们给它一个子类型“Vehicle -> void”,因为“&Works”的类型实际上是 。因此,“如果一个函数接受 Vehicle,那么它就是接受 Car 的函数的子类型”。这是可能的,因为“Works”功能将执行的所有操作也必须在实际传入的东西上成为可能 - 汽车。std::function<auto(Vehicle) -> void>

一个用例是,Works 函数也可以传递给 BoatWrapper 类,该类需要在船上操作的函数。我们可以给出该函数类型的子类型,“Vehicle -> void”,因为 Boat 是 Vehicle 的一个子类型,而我们的实际函数只使用参数的更通用的 Vehicle'ness 来操作,因为实际传递的函数中的所有操作都必须在传递给它的 Boat 上可用。

另一方面,协方差对返回类型进行运算;如果 CarWrapper 类期望为我们生成 Car 的回调,我们将无法传入 Vehicle-generating 函数,因为这样 CarWrapper 将无法以特定于汽车的方式使用结果。

但是,如果我们有一个期望车辆发电机的函数,我们将能够给它一个汽车发电机或船发电机;因此 (void -> Car) 是 (void -> Vehicle 的子类型) iff Car 是 Vehicle 的子类型。

协方差意味着子类型关系保持在同一方向, 因此,我们可以继续坚持使用另一个功能应用程序,并且“汽车端”仍将是“车辆端”的子类型,即:

Car is a subtype of Vehicle means that:
(void -> Car) is a subtype of (void -> Vehicle) - as in the code sample above
(void -> (void -> Car)) is a subtype of (void -> (void -> Vehicle))
(void -> (void -> (void -> Car))) is a subtype of (void -> (void -> (void -> Vehicle)))

这意味着,如果我们期望一个 VehicleFactoryFactoryFactory,那么当给定一个 CarFactoryFactoryFactory 时,我们应该感到满意:

#include <functional>

class Vehicle { };
class Car : public Vehicle { };

typedef std::function<auto() -> Vehicle> VehicleFactory;
typedef std::function<auto() -> VehicleFactory> VehicleFactoryFactory;
typedef std::function<auto() -> VehicleFactoryFactory> VehicleFactoryFactoryFactory;

void GiveMeAFactory(VehicleFactoryFactoryFactory factory)
{
    Vehicle theVehicle = factory()()();
}

typedef std::function<auto() -> Car> CarFactory;
typedef std::function<auto() -> CarFactory> CarFactoryFactory;
typedef std::function<auto() -> CarFactoryFactory> CarFactoryFactoryFactory;

Car ActualCarCreateFunc() { return Car(); }
CarFactory CarFactoryCreateFunc() { return &ActualCarCreateFunc; }
CarFactoryFactory CarFactoryFactoryCreateFunc() { return &CarFactoryCreateFunc; }

int main() {
    GiveMeAFactory(&CarFactoryFactoryCreateFunc);
}

由于参数类型存在逆变,则关系会随着每个函数应用而反转。

Car is a subtype of Vehicle means that:
(Vehicle -> void) is a subtype of (Car -> void)
((Car -> void) -> void) is a subtype of ((Vehicle -> void) -> void)
(((Vehicle -> void) -> void) -> void) is a subtype of (((Car -> void) -> void) -> void)

在逆变的情况下,很难用直观的术语来理解这一点。这就是为什么我的 CarWrapper 只尝试针对规则的单个应用来解释它,而 CarFactory 示例包含三个协方差应用。

评论

0赞 John V 10/12/2018
谢谢你,它确实帮助了我,但如果你能详细说明 Void(Vehicle) 的那部分是 void(Car) 的子类型,我将不胜感激。理想情况下,用简单的术语,就像我最初问题下的评论一样。
1赞 John V 10/12/2018
我认为你的最后一段有错误——Car->Void 不是 Vehicle->void 的子类型。
0赞 TamaMcGlinn 10/12/2018
正确;我不得不把它画在纸上,看看逆变的多种应用应该如何工作。希望现在我在多行上都有它更清楚了。在第一个代码示例之后,我还添加了一些更简单的英语。希望如果你考虑一下这些函数的内容,你就会明白为什么当 PassCallback 的签名请求实际在 Cars 上运行的函数时,Works 函数是有效的,这在直觉上是有道理的。
0赞 John V 10/12/2018
多谢。我添加了我自己的答案(正如我的一位同事用简单的话向我解释的那样),但当然我会留下你的答案,因为它提供了很多细节和实现示例。