• 规则

    强制转换和子类型

    Rust 有一系列规则,允许一个类型被强制转换为另一个类型。尽管强制转换和子类型很相似,但是能够区分它们也很重要。关键的不同在于,子类型没有改变底层的值,但是强制转换改变了。具体来讲,编译器在强制转换的位置插入额外的代码以执行某些底层转换,而子类型只是一个编译器检查。因为这些额外的代码对开发者是不可见的,并且强制转换和子类型看起来很相似,因为二者看起来都像这样:

    let b: B;
    ...
    let a: A = b;

    强制转换和子类型放一起:

    // 这是强制转换(This is coercion):
    let values: [u325] = [12345];
    let slice: &[u32] = &values;

    // 这是子类型(This is subtyping):
    let val1 = 42;
    let val2 = 24;
    'x: {
        let ref1 = &'x val1;
        'y: {
            let mut ref2 = &'y val2;
            ref2 = ref1;
        }
    }

    这段代码能够工作,因为'x'y的子类型,而且也因此,&'x也是&'y的子类型。

    通过学习一些最常见的强制转换,很容易就能区分二者,剩下的一些不常见的,见 Rustonomicon

    你可能想知道为什么'x'y的子类型这件事能够推导出&'x也是&'y的子类型?要回答这个问题,我们需要讨论 Variance。

    变型(Variance)

    基于前面的内容,我已经可以很容易区分生命周期'longer是否是生命周期'shorter的子类型。你甚至可以直观地理解为什么&'longer T&'shorter T的子类型。但是,你能够区分&'a mut &'longer T是否是&'a mut &'shorter T的子类型嘛?实际上做不到,要知道为什么,我们需要 Variance 规则。

    正如我们之前所说,生命周期能够对那些生命周期参数化的类型上进行有限的子类型化。变型 是类型构造器(type-constructor)的一个属性, 类型构造器是一个带有参数的类型,比如Vec<T>或者&mut T。更具体的,变型决定了参数的子类型化如何影响结果类型的子类型化。如果类型构造器有多个参数,比如F<'a, T, U>或者&'b mut V,那么变型就针对每个参数单独计算。

    有三种类型的变型:

    当类型构造器有多个参数时,我们这样来讨论单个的变型,例如,F<'a, T>'a的协变并且是T的不变。而且,还有第四种类型的变型-二变体,但它是一个特定的编译器实现细节,这里我们不需要了解。

    下面是一张针对最常见的类型构造器的变型表格:

    协变基本上是一个传递规则。逆变很少见,并且只发生在当我们传递指针到一个使用了更高级别 trait 约束的函数时才会发生,不变是最重要的,当我们开始组合变型时,我们会看到它的动机。

    变型运算(Variance arithmetic)

    现在我们知道&'a mut TVec<T>的子类型和超类型是什么了,但是我们知道&'a mut Vec<T>Vec<&'a mut T>的子类型和超类型是什么嘛?要回答这个问题,我们需要知道如何组合类型构造器的 variance。

    组合变型有两种数学运算:Transform 和最大下确界(greatest lower bound, GLB )。Transform 用于类型组合,而 GLB 用于所有的聚合体:结构体、元组、枚举以及联合体。让我们分别用 0、+、和 - 来表示不变,协变和逆变。然后 Transform(X)和 GLB(^)可以用下面两张表来表示:

    示例

    假定,我们想要知道Box<&'longer bool>是否是Box<&'shorter bool>的一个子类型。换句话说,也就是我们想要知道Box<&'a bool>关于'a的协变。&'a bool是关于'a的协变,Box<T>是关于T的协变。因为它是一个组合,所以我们需要应用 Transform(X): 协变(+) x 协变(+) = 协变(+),这意味着我们可以把Box<&'longer bool> 赋予 Box<&'shorter bool>

    类似的,Cell<&'longer bool>不能被赋给Cell<&'shorter bool>,因为 协变 (+) x 不变 (0) = 不变 (0)

    示例

    下面来自 Rustonomicon 的示例解释了为什么在一些类型构造器上我们需要不变性(invariant)。它试图编写一段代码,这段代码使用了一个被释放后的对象。

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

    Rust 编译器不会编译这段代码。要理解其原因,我们首先要对代码进行脱糖:

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

    fn main() {
        let mut mr_snuggles: &'static str = "meow! :3";
        {
            let spike = String::from("bark! >:V");
            'x: {
                let spike_str: &'x str = &'x spike;
                'y: {
                    evil_feeder(&’y mut mr_snuggles, spike_str);
                    }
                }
        }
        println!("{}", mr_snuggles);
    }

    在编译期间,编译器试图找到满足约束的参数T。回想一下,编译器会采用最小的生命周期,所以它会尝试为T使用&'x str。现在,evil_feeder的第一个参数是&'y mut &'x str,而我们试图传递&'y &'static str。这样会有效么?

    为了使其有效,&'y mut &'z str应该是'z的协变,因为'static'y的子类型。回顾一下,&'y mut T是关于T的不变,&'z T是关于'z的协变。&'y mut &'z str是关于'z,因为 协变(+) x 不变 (0) = 不变 (0)。因此,它将无法编译。
    有趣的是,这段代码如果用 C++来写就可以编译通过。

    结合结构体的示例

    关于结构体,我们需要使用 GLB 而不是 Transform,这只在我们使用函数指针涉及到协变时才有意义。下面是一个无法编译的示例,因为结构体Owner是关于生命周期参数'a的不变,编译器给出的错误信息也有表明:

    type annotation requires that `spike` is borrowed for `'static`

    不变性从本质上禁用了子类型化,也因此,spike的生命周期准确匹配mr_sunggles

    struct Owner<'a:'c'b:'c'c> {
            pub dog: &'a &'c str,
            pub cat: &'b mut &'c str,
    }

    fn main() {
        let mut mr_snuggles: &'static str = "meow! :3";
        let spike = String::from("bark! >:V");
        let spike_str: &str = &spike;
        let alice = Owner { dog: &spike_str, cat: &mut mr_snuggles };
    }

    结尾

    要记住所有的规则是非常困难的 ,并且我们也不想每次在 Rust 中遇到困难的情况都去搜索这些规则。培养直觉的最好方式是理解和记住这些规则所阻止的不安全的情况。

    每一个版本的发布,Rust 的可用性和友好性都在改善,然而,生命周期是一个核心概念,仍然需要深挖。这篇文章集合了各种资源的信息,让你做一次深入研究而不必多次:)。

    参考资料

    01-07 07:56