原文链接:https://doc.rust-lang.org/nomicon/subtyping.html

子类型和变性

子类型是类型之间的一种关系,可以让静态类型语言更加地灵活自由。

Rust中的子类型与其他语言中的子类型有些不同。这使得很难给出简单的例子,这是一个问题,因为子类型,尤其是变性,已经很难正确理解。在这种情况下,即使是编译器编写者也经常将其弄乱。

为了使事情简单,本节将考虑对Rust语言的一个小扩展,它添加了新的更简单的子类型关系。在这个更简单的系统下建立了概念和问题之后,我们将把它与Rust中子类型的实际发生方式联系起来。

因此,这是我们的简单扩展,Objective Rust,具有三种新类型:


#![allow(unused)]
fn main() {
trait Animal {
    fn snuggle(&self);
    fn eat(&mut self);
}

trait Cat: Animal {
    fn meow(&self);
}

trait Dog: Animal {
    fn bark(&self);
}
}

但是,与普通trait不同,我们可以像结构一样将它们用作具体的有大小的类型。

现在,假设我们有一个非常简单的函数,该函数采用动物,如下所示:

fn love(pet: Animal) {
    pet.snuggle();
}

默认情况下,静态类型必须完全匹配才能编译程序。 因此,此代码将无法编译:

let mr_snuggles: Cat = ...;
love(mr_snuggles);         // ERROR: expected Animal, found Cat

mr_snuggles 是 Cat,而猫 Cat 不是确切动物,所以我们不能爱它!😿

这很烦人,因为猫动物。它们支持动物支持的每项操作,因此从直觉上来说,love 应该不在乎我们是否将其传递给 Cat。我们应该能够忘记 Cat 的非动物部分,因为它们不需要。

这正是子类型旨在解决的问题。因为猫是动物,甚至更多,所以我们说猫是动物的子类型(因为猫是所有动物的子集)。等效地,我们说动物是猫的超类型。使用子类型,我们可以通过一个简单的规则来调整我们过于严格的静态类型系统:在任何期望为 T 的值的地方,我们也将接受 T 子类型的值。

或更具体地讲:只要是动物可行,猫或狗也是可行的。

正如我们将在本节的其余部分中看到的那样,子类型化比这要复杂得多和微妙得多,但是这个简单的规则是非常好的直觉。并且,除非你编写不安全的代码,否则编译器将自动为你处理所有极端情况。

但这是Rustonomicon,我们正在编写不安全的代码,因此我们需要了解这些东西是如何工作的,以及如何使用它。

核心问题是,天真地应用此规则将导致喵喵叫的狗。也就是说,我们可以说服某人狗实际上是猫。这完全破坏了我们的静态类型系统的结构,使其变得比无用更糟(并最终导致未定义的行为)。

这是当我们在完全天真的情况下应用子类型化时发生的一个简单“查找并替换”的示例。

fn evil_feeder(pet: &mut Animal) {
    let spike: Dog = ...;

    // `pet` 是 Animal,Dog 是 Animal 的子类,
    // 所以这是可行的,对吗..?
    *pet = spike;
}

fn main() {
    let mut mr_snuggles: Cat = ...;
    evil_feeder(&mut mr_snuggles);  // 使用 Dog 替换 mr_snuggles
    mr_snuggles.meow();             // 噢不,喵喵叫的狗!
}

显然,我们需要比“查找并替换”更健壮的系统。该系统是 变性(variance),它控制子类型应如何组成的一组规则。最重要的是,变性定义了应禁用子类型化的情况。

但是在讨论变性之前,让我们快速看一下Rust中子类型的实际发生位置:生命周期

注意:生命周期的类型性是一个相当随意的构造,有些人不同意这种构造。 但是,这简化了我们对生命周期和类型进行统一处理的分析。

生命周期只是代码区域,区域可以通过包含(contains)(有效期(outlives))关系进行部分排序。生命周期的子类型指的是:如果'big: 'small(big包含small,或者big比small长寿),那么'big就是'small的子类型。这一点很容易弄错,因为它和我们的直觉是相反的:大的范围是小的范围的子类型。不过如果你对比一下我们举的Animal的例子就清楚了:Cat是一个Animal,外加一些独有的东西,正如'big'small,外加一些独有的东西。

换一个角度想,如果需要一个在'small内有效的引用,实际指的是至少'small中有效的引用。我们并不在乎生命周期是不是完全的一致。 因此,让我们忘记某些东西在'big中有效地引用,而只记得在'small中有效地引用,这应该没问题。

生命周期的喵喵叫的狗问题将使我们能够在寿命更长的地方存储一个短暂的引用,创建一个悬垂的引用并让在释放后使用。

值得注意的是,永久生命周期 'static 是每个生命周期的子类型,因为根据定义,它比所有东西都寿命长。在后面的示例中,我们将使用这种关系以使它们尽可能简单。

话虽如此,我们仍然不知道如何真正使用生命周期的子类型,因为从来没有类型'a。生命周期仅作为某些较大类型的一部分出现,例如&'a u32IterMut <'a,u32>。 要应用生命周期子类型化,我们需要知道如何构建子类型化。 再一次,我们需要变性

变性

变性显得有一点复杂。

变性是类型构造函数与它的参数相关的一个属性。Rust中的类型构造函数是任意带有无界参数的通用类型。比如,Vec是一个构造函数,它的参数是类型 T,返回值是vec<T>&&mut也是构造函数,它们有两个类型:一个生命周期,和一个引用指向的类型。

注意:为了方便起见,我们通常将F <T>称为类型构造函数,以便我们可以轻松地讨论T。希望这在上下文中很清楚。

构造函数F的变性表示了它的输入的子类型如何影响它输出的子类型。给定两个类型 SubSuper,其中,SubSuper 的子类型,Rust中有三种变性:

  • 如果F<Sub>F<Super>的子类型,则F协变的(子类型闯“穿过”);
  • 如果F<Super>F<Sub>的子类型,则F逆变的(子类型“反转”);
  • 其他情况(即子类型之间没有关系),则F不变的。

如果F具有多个类型参数,我们可以说单个变性,例如,F <T,U>T上是协变的,而在U上是不变的。

牢记协变从实际上是变性,这一点非常有用。几乎所有关于变性的考虑都是根据某事物是协变还是不变的。实际上,在Rust中,逆变是非常少见的,尽管实际上确实存在。

以下是重要的变性表,本节的其余部分将专门解释:

'aTU
*&'a T协变协变
*&'a mut T协变不变
*Box<T>协变
Vec<T>协变
*UnsafeCell<T>不变
Cell<T>不变
*fn(T) -> U协变
*const T协变
*mut T不变

带 * 的类型是我们将重点关注的类型,因为它们在某种意义上是“基本的”。可以通过与其他类似的方式来理解所有其他类型:

  • Vec<T> 以及所有其他拥有的指针和集合遵循与 Box<T> 相同的逻辑;
  • Cell<T> 以及其他所有的内部可变类型遵循与 UnsafeCell<T> 相同的逻辑;
  • *const T 遵循与 &T 相同的逻辑;
  • *mut T 遵循 &mut T(或者 UnsafeCell<T>) 的逻辑;

注意:语言中逆变的唯一来源是函数的参数,这就是为什么它实际上在实践中并没有太大作用的原因。 调用逆变涉及使用函数指针进行高阶编程,这些函数指针采用具有特定生命周期的引用 (与通常的“任意生命周期”相对,后者进入较高级别的生命周期,与子类型无关)。

好的,类型的理论已经够了!让我们尝试将变性的概念应用于Rust,并看一些示例。

首先,让我们回顾一下喵喵叫的狗的例子:

fn evil_feeder(pet: &mut Animal) {
    let spike: Dog = ...;

    // `pet` is an Animal, and Dog is a subtype of Animal,
    // so this should be fine, right..?
    *pet = spike;
}

fn main() {
    let mut mr_snuggles: Cat = ...;
    evil_feeder(&mut mr_snuggles);  // Replaces mr_snuggles with a Dog
    mr_snuggles.meow();             // OH NO, MEOWING DOG!
}

如果我们看一下变性表,就会发现 &mut TT不变。事实证明,这完全可以解决问题!因为不变,Cat是Animal的子类型这一事实无关紧要;&mut Cat仍然不会是&mut Animal的子类型。静态类型检查器将正确阻止我们将Cat传递给evil_feeder

子类型的合理性是基于这样的想法,即可以忘记不必要的细节。但是有了引用,总会有人记住那些细节:被引用的值。该值期望这些细节保持真实,并且如果违反其期望,则行为可能不正确。

使&mut T关于T协变的问题在于,当我们不记得所有约束时,它使我们能够修改原始值。因此,当他们确定自己还有 Cat 时,我们可以让他们有 Dog。

建立了这个后,我们可以很容易地理解为什么 &T 关于 T 协变合理的:它不允许你修改值,而只能查看它。没有任何可变的方法,就没有办法让我们弄乱任何细节。我们还可以看到为什么UnsafeCell和所有其他内部可变性类型必须具有不变性:它们使&T&mut T一样工作!

现在,引用的生命周期是怎样的?为什么两种引用在它们的生命周期中都可以协变?好吧,这是一个两方面的论点:

首先,基于生命周期的子类型化引用是Rust中子类型化的整个重点。我们拥有子类型化的唯一原因是,我们可以在预期寿命短的地方传递寿命长的东西。这样更好地工作!

其次,更重要的是,生命周期只是引用本身的一部分。引用对象的类型是共享知识,这就是为什么仅在一个地方(引用)调整该类型会导致问题的原因。但是,如果你在将引用交给某人是将引用的生命周期缩减,则该生命周期信息不会以任何方式共享。现在有两个具有独立生命周期的独立引用,使用另一种方法无法破坏原始引用的生命周期。

或者更确切地说,弄乱某人生命周期的唯一方法是构建一只喵喵叫的狗。但是,一旦你尝试构建一只喵喵狗,就应该将生命周期固定为不变类型,以防止生命周期缩短。为了更好地理解这一点,让我们将猫叫问题移植到真正的Rust上。

在喵喵叫的狗问题中,我们采用子类型(Cat),将其转换为超类型(Animal),然后使用该事实将满足父类型但不满足子类型(Dog)约束的值覆盖子类型。

因此,对于生命周期,我们希望将其寿命长的东西转换成短寿命的东西,然后用它来写一些寿命不长的东西到期望寿命长的地方。

也就是:

fn evil_feeder<T>(input: &mut T, val: T) {
    *input = val;
}

fn main() {
    let mut mr_snuggles: &'static str = "meow! :3";  // mr. snuggles 一直存活!!
    {
        let spike = String::from("bark! >:V");
        let spike_str: &str = &spike;                // 只在代码块内存活
        evil_feeder(&mut mr_snuggles, spike_str);    // EVIL!
    }
    println!("{}", mr_snuggles);                     // 释放后使用?
}

当我们运行这个程序时,我们会得到什么?

error[E0597]: `spike` does not live long enough
  --> src/main.rs:9:32
   |
9  |         let spike_str: &str = &spike;
   |                                ^^^^^ borrowed value does not live long enough
10 |         evil_feeder(&mut mr_snuggles, spike_str);
11 |     }
   |     - borrowed value only lives until here
   |
   = note: borrowed value must be valid for the static lifetime...

好,它无法编译!让我们详细分解这里发生的事情。

首先让我们看一下新的evil_feeder函数:


#![allow(unused)]
fn main() {
fn evil_feeder<T>(input: &mut T, val: T) {
    *input = val;
}
}

它所做的只是获取一个可变的引用和一个值,并用它覆盖引用对象。此函数重要的是它创建了一个类型相等约束。它清楚地在签名中指出了所指对象和值必须是完全相同的类型。

同时,在调用方中,我们传递 &mut &'static str&'spike_str str

因为&mut T相对于T是不变的,所以编译器得出结论,它不能对第一个参数应用任何子类型,因此T必须精确地是&'static str

另一个参数只是一个&'a str,它对于a协变的。因此,编译器采用了一个约束:&'spike_str str必须是&'static str(包括)的子类型,这反过来意味着'spike_str必须是'static(包括)的子类型。 这就是说,'spike_str必须包含'static。但是只有一个东西包含'static──'static本身!

这就是为什么当我们尝试将&spike分配给spike_str时出现错误的原因。编译器进行了倒退的工作,以得出spike_str必须永远存在的结论,而&spike不能存活那么久。

因此,即使引用在其整个生命周期中都是协变的,但只要将它们置于上下文中,它们都可以“继承”不变性,这可能会对此造成不良影响。在这种情况下,只要将引用放入&mut T中,我们就继承了不变性。

事实证明,Box(以及Vec,Hashmap等)为什么可以协变的论点与为什么生命周期可以协变的论点非常相似:只要你尝试将它们塞入诸如可变引用之类的东西,它们继承不变性,并且可以防止你做任何不好的事情。

但是,Box使我们更容易专注于引用的按值方面,这部分我们通常是掩盖。

与许多语言允许随时对值进行自由别名不同,Rust有一个非常严格的规则:如果你允许对值进行可变或移动,则可以保证你可以唯一访问它。

考虑以下代码:

let mr_snuggles: Box<Cat> = ..;
let spike: Box<Dog> = ..;

let mut pet: Box<Animal>;
pet = mr_snuggles;
pet = spike;

我们已经忘记了mr_snuggles是一只 Cat,或者我们用 Dog 将它覆盖了,这根本没有问题,因为一旦我们将 mr_snuggles 移到只知道他是Animal的变量上,我们摧毁了宇宙中唯一记得它是 Cat 的东西

与关于不可变引用由于它们不允许你进行任何更改而听起来是协变的说法相反,拥有的值可以是协变量的,因为它们使你改变一切。旧位置和新位置之间没有连接。应用按值子类型化是知识破坏的不可逆转的行为,并且没有任何关于过去情况的记忆,任何人都不会被诱使对这些旧信息采取行动!

只剩下一件事要解释了:函数指针。

要了解为什么fn(T) -> U应该相对于U是协变的,请考虑以下签名:

fn get_animal() -> Animal;

此函数声称可产生 Animal。 因此,为函数提供以下签名是完全有效的:

fn get_animal() -> Cat;

毕竟,Cat是Animal,因此始终生产Cat是生产Animal的完全有效的方法。或将其与真实的Rust相关联:如果我们需要一个函数来产生可以生存的'short的东西,那么它可以产生可以生存'long的东西。我们不在乎,我们可以忘记这一事实。

但是,相同的逻辑不适用于参数变量。 考虑尝试满足:

fn handle_animal(Animal);

使用

fn handle_animal(Cat);

第一个函数可以接受 Dogs,但是第二个函数绝对不能。协变性在这里不起作用。但是,如果我们将其翻转,它实际上确实有效!如果我们需要一个可以处理Cats的函数,那么一个可以处理任何 Animal 的函数肯定可以正常工作。或者将其与真正的Rust关联起来:如果我们需要一个能够处理至少'long存活期的函数,那么它能够处理至少'short生存期的函数。

这就是为什么与语言中的任何其他不同的是,函数类型对于参数是逆变的。

至此,对于标准库提供的类型来说,这一切都很好,那么自己定义的类型又如何确定变性呢?简单点说,结构体会继承它的成员的变性。如果结构体MyType有一个成员a,它使用了结构体的泛型参数A,那么MyType对于A的变性就等于a对于A的变性。

可如果A被用在了多个成员中:

  • 如果所有用到A的成员都是协变的,那么MyType对于A就是协变的
  • 如果所有用到A的成员都是逆变的,那么MyType对于A也是逆变的
  • 其他的情况,MyType对于A是不变的

#![allow(unused)]
fn main() {
use std::cell::Cell;

struct MyType<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed> {
    a: &'a A,     // 对于'a和A协变
    b: &'b mut B, // 对于'b协变,对于B不变

    c: *const C,  // 对于C协变
    d: *mut D,    // 对于D不变

    e: E,         // 对于E协变
    f: Vec<F>,    // 对于F协变
    g: Cell<G>,   // 对于G不变

    h1: H,        // 对于H本该是可变的,但是……
    h2: Cell<H>,  // 其实对H是不变的,发生变性冲突的都是不变的

    i: fn(In) -> Out,       // 对于In逆变,对于Out协变

    k1: fn(Mixed) -> usize, // 对于Mix本该是逆变的,但是……
    k2: Mixed,              // 其实对Mixed是不变的,发生变性冲突的都是不变的
}
}