子类型和变异

源:subtyping.md   Commit: a805a667ba8534b78b9587ba7644dac53ce0ab98

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

Rust中的子类型与其他语言的子类型略有不同。 这使得提供简单示例变得更加困难,这是一个问题,因为子类型,尤其是变异,已经很难正确理解。 甚至是编译器编写者也会搞乱。

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

所以这是我们的简单扩展,Objective Rust,有三种新类型:

trait Animal { fn snuggle(&self); fn eat(&mut self); } trait Cat: Animal { fn meow(&self); } trait Dog: Animal { fn bark(&self); }

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

现在,假设我们有一个非常简单的函数,它接受一个Animal,就像这样:

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

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

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

Snuggles先生是猫,猫不是动物,所以我们不能爱他!

这很烦人,因为猫是动物。它们支持动物支持的每一项操作,所以如果我们将它传递给Cat,直觉上爱情就不应该关心。我们应该能够忘记我们猫的非动物部分,因为它们没有必要去爱它。

这正是子类型要修复的问题。因为猫只是动物和更多,我们说猫是动物的亚型(因为猫是所有动物的子集)。同样,我们说动物是猫的超级类型。使用子类型,我们可以通过一个简单的规则调整我们过于严格的静态类型系统:在预期类型为T的值的任何地方,我们也将接受作为T的子类型的值。

或者更具体地说:在任何期望动物的地方,猫或狗也会起作用。

正如我们将在本节的其余部分中看到的那样,子类型比这更复杂和微妙,但这个简单的规则是符合的99%直觉。除非您编写不安全的代码,编译器将自动为您处理所有角落情况。

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

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

当我们以完全天真的"查找和替换"方式应用子类型时,这是一个简单的例子。

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! }

显然,我们需要一个比“查找和替换”更强大的系统。 该系统是变异,这是一组规则如何构成子类型的规则。 最重要的是,变异定义了应禁用子类型的情况。

但在我们开始变异之前,让我们快速看看Rust中实际存在的子类型:生命周期!

注意:生命周期的类型是一种相当随意的结构,有些人不同意。 然而,它简化了我们的分析,以统一处理生命周期和类型。

生命周期只是代码区域,区域可以部分地与包含(outlives)关系一起排序。生命周期的子类型就是这种关系:如果'big: 'small("big包含small"或"大比小存活的长"),那么bigsmall的子类型。这是一个很大的混乱来源,因为它看起来逆直觉很多:较大的区域是较小区域的子类型。但是,如果你考虑我们的动物例子,它是有道理的:猫是一个动物和更多,就像bigsmall和更多。

换句话说,如果有人想要一个"small"的引用,那么他们实际上意味着他们想要的引用至少是"small"。他们实际上并不关心生命周期是否完全匹配。因此,我们应该忘记一些东西是big的生命周期而且只需记得small的生命周期。

喵喵狗问题的生命周期将导致我们能够将一个短命的参考存储在一个期望生命周期更长的地方,创造一个悬垂的引用并让我们在释放后使用。

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

尽管如此,我们仍然不知道如何实际使用生命周期的子类型,因为没有谁有个类型'a。生命周期只发生在某些较大类型的一部分,如'a u32IterMut <'a,u32>。要应用生命周期子类型,我们需要知道如何编写子类型。再一次,我们需要变异。

变异

变异是事情变得复杂的地方。

变异是类型构造函数关于其参数的属性。 Rust中的类型构造函数是具有无界参数的任何泛型类型。 例如,Vec是一个类型构造函数,它接受一个类型T并返回Vec <T>&mut是带有两个输入的类型构造函数:生命周期和指向的类型。

注意:为方便起见,我们经常将F <T>称为类型构造函数,以便我们可以轻松地讨论T.希望这在上下文中是明确的。

类型构造函数F的变异是其输入的子类型如何影响其输出的子类型。 Rust中有三种变异。 给定SubSuper两种类型,其中SubSuper的子类型:

  • 如果当F<Sub>F<Super>的子类型时,则F协变的(子类型"正向")
  • 如果当F<Super>F<Sub>的子类型时,则F逆变的 (子类型是"倒")
  • 其他情况F不变的(没有子类型关系存在)

如果F有多个类型参数,我们可以通过讨论个体差异,例如,F <T,U>T上是协变的而在U上是不变的,可以讨论各个变异。

记住协变实际上是"变异"是非常有用的。 几乎所有对变异的考虑都取决于某些事物是否应该是协变的或不变的。 实际上,在Rust中遇见逆变是相当困难的,尽管它确实存在。

以下是本节其余部分将用于解释变异的重要差异表:

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

*的类型是我们将关注的类型,因为它们在某种意义上是"基础"。 所有其他类型都可以通过类比来理解:

  • Vec和所有其他的拥有指针类型集合遵循与Box相同的逻辑
  • Cell和所有其他的内部可变性类型遵循与UnsafeCell相同的逻辑
  • * const遵循&T的逻辑
  • * mut遵循&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 T对T不变。事实证明,这完全解决了这个问题!由于不变性,Cat是动物亚型的事实并不重要; &mut Cat仍然不会是&mut Animal的子类型。然后,静态类型检查器将正确阻止我们将Cat传递给evil_feeder

子类型的健全性基于这样的想法,即忘记不必要的细节是可以的。但是通过引用,总会有人记住这些细节:被引用的值。该值预计这些细节将保持正确,并且如果违反预期,则可能表现不正确。

T上进行&mut T协变的问题在于,当我们不记得所有约束时,它赋予我们修改原始值的能力。因此,当他们确定他们还有一只猫时,我们可以让某人拥有一只狗。

有了这个,我们可以很容易地看出为什么&TT的协变是合理的:它不会让你修改价值,只看它。没有任何改变的方法,我们没有办法搞砸任何细节。我们还可以看到为什么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 forever!! { let spike = String::from("bark! >:V"); let spike_str: &str = &spike; // Only lives for the block evil_feeder(&mut mr_snuggles, spike_str); // EVIL! } println!("{}", mr_snuggles); // Use after free? }

当我们运行它时我们会得到什么?

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函数:

所有这一切都需要一个可变的引用和一个值,并用它覆盖指示对象。这个函数的重要之处在于它创建了一个类型相等约束。它在其签名中清楚地表明指示物和价值必须是完全相同的类型。

同时,在调用者中我们传入&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_str分配&spike时出错的原因。编译器已经向后工作以得出结论spike_str必须永远存在,并且&spike根本不能活得那么久。

因此,即使引用在其生命周期中是协变的,但只要它们被置于可能对此做坏事的上下文中,它们就“继承”不变性。在这种情况下,只要我们将引用放在&mut T中,我们就会继承不变性。

事实证明,为什么Box(以及VecHashmap等)可以协变的论证非常类似于为什么生命周期都可以协变的论证:只要你试着填充它们类似于可变引用的东西,它们继承了不变性并且你被阻止做任何坏事。

然而Box更容易关注我们部分掩盖的引用的价值方面。

与许多允许值始终自由别名的语言不同,Rust有一个非常严格的规则:如果允许变异或移动值,则保证您是唯一可以访问它的人。

请考虑以下代码:

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

因为我们忘记了mr_snuggles是一只猫,或者我们用狗覆盖了他,所以我们没有任何问题,因为只要我们将mr_snuggles移动到一个只知道他是动物的变量,我们就摧毁了唯一的 在宇宙中记得他是猫的东西!

与不可变引用完全协变的论点相反,因为它们不允许你改变任何东西,所拥有的值可以是协变的,因为它们会让你改变一切。 旧位置和新位置之间没有连接。 应用按值的子类型是一种不可逆转的知识破坏行为,如果没有任何记忆,那么就不会有任何人被这些旧信息欺骗!

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

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

fn get_animal() -> Animal;

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

fn get_animal() -> Cat;

毕竟,猫是动物,因此生产猫是一种完全有效的生产动物的方法。 或者将它与真正的Rust联系起来:如果我们需要一个能够产生“短暂”生命的功能,那么生产能够长期存在的东西就完全没问题了。 我们不在乎,我们可以忘记这一事实。

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

fn handle_animal(Animal); fn handle_animal(Cat);

第一个函数可以接受Dogs,但第二个函数绝对不能。 协变在这里不起作用。 但是,如果我们翻转它,它确实有效! 如果我们需要一个可以处理Cats的函数,可以处理任何Animal的函数肯定会正常工作。 或者将它与真正的Rust联系起来:如果我们需要一个能够处理任何至少长寿命的函数,那么它能够处理任何至少短暂存在的东西都是完美的。

这就是为什么函数类型与语言中的其他任何东西不同,它们的参数都是逆变的。

现在,这对于标准库提供的类型来说都很好,但是如何确定您定义的类型的变异? 非正式地说,结构继承了其字段的变异。 如果结构MyType具有在字段a中使用的泛型参数A,那么MyTypeA上的变异恰好是A的变异。

但是,如果在多个字段中使用A

  • 如果所有用到A的成员都是协变的,那么Foo对于A就是协变的
  • 如果所有用到A的成员都是逆变的,那么Foo对于A也是逆变的
  • 其他的情况,Foo对于A是不变的
use std::cell::Cell; struct Foo<'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是不变的,发生变性冲突的都是不变的 }