Rust 中的型变

原文 https://zhuanlan.zhihu.com/p/372283729

协变在Rust中是一个今人迷惑的话题。对于新手可能感知不到这些概念。本篇内容将给出一个更通俗的理解。

首先, 有两个重要的点我们要关注一下。

  1. 变性是一个与通用参数T有关的概念
  2. Rust的生命周期参数('a)具有子类型化的设计

?关于子类型是什么,如果你学习过其它高级语言,就很容易理解。

?关于生命周期,你可以理解为一个变量在栈上什么时候有效,没被释放。更通俗的来说。 就是 一个作用域代码块

fn main()
{    // 这{}括号包起来的,就是一个作用域,叫行生命周期范围也行。
     // 在括号内的局部栈变量只在这个括号内有效,

}    // ---------------- 结束

Rust中的型变就是允许对一个生命周期参数进行如何的变化:

寿命变短:协变 (子类型向基类进化。缩减作用域。)
寿命变长:逆变 (基类向子类型进化,听进来很荒谬,扩大生命周期,这时危险的)
没有关系:不变 (不允许协变与逆变,只允许替换成为相同的类型)

注意:变性这些概念实际上是我们需要做的理论假设, 这样我们才能确保我们的实现是正确的。不然, 程序会炸,出现各种无效指针。

在Rust中无论你在哪里看到变性,默认情况下,它表示协变。

现在这里有一个经典的例子。

先试着猜测输出结果:

struct MyCell<T> {
    value: T
}

impl<T> MyCell<T> {
    fn new(value: T) -> Self {
        MyCell { value }
    }

    fn get(&self) -> &T {
        &self.value
    }

    fn set(&self, new_value: T) {
        // 注意这里,方法获取的是一个不可变的实例(&self)不是&mut self。如果你改成了。那就不会
        // 有问题了。 编译时会提示new_value生命周期不足
        // 我们需要使用不安全的内存写入方法,制造这个'BUG'
        unsafe { 
               std::ptr::write(&self.value as *const _ as *mut T, new_value); 
        }
    }
}

fn set_value(source: &MyCell<&i32>) {
    println!("旧的值是:{}", source.get());
    let bugvar = 100;
    source.set(&bugvar);
}

fn main() {
    let a= MyCell::new(&10);
    set_value(&a);
    println!("现在的值是:{}", a.get());
}

// 最终输出:
// 旧的值是:10
// 现在的值是:-65374792

Rust忽然变的内存不安全了。 造成这种原因的。就是今天所说的 变性。

我们展开一下类似编译的MIR风格代码解析一下问题所在。

'a: {
    let a = MyCell::new(&'a 10);
    'b: {
        let bugvar: i32 = 100; // <-- 这个BUG变量创建在这里
        'c: {
           'd {
                source.set(&'d bugvar);
            }
        }
       // <-- bufvar 释放在这里
    }
    println!("end value: {}", cell.value);
}

问题的根源在于,我们允许 改变 了MyCell里包裹的val。

(重点:这种关系,MyCell对于val来说。是协变*)

我们把生命周期只有'b范围的变量bugvar,强行塞进了生命周期比它更长的'a范围里。那么一但较短的那个离开了作用域。那么更长的变量却保存着他。 结果就是: 指向了一个无效的地址。所以,这就相当于, 我借了100万给你, 1个月后。 你竟然说是我欠了你100万。你说会不会炸? (你不经我同意单方面修改了内部的值)

这段代码的生命周期关系:a > b > c > d

  • 划重点,如果要让MyCell包裹的值有效,那么必须裹入一个至少同样>=a的作用域

那如何告诉编译器,对MyCell做一些限制呢。

先参考Cell源码,核心在于多了一条#[lang = "unsafe_cell"] 编译器会认为这是一个不安全的块,限制你传入的参数生命周期必须和Cell一样或更长,否则 编译报错。不过 #[lang = "unsafe_cell"] 这条属性并不对外开放,语言内部使用, 你只有通过其它方式标记我们的MyCell了

pub struct Cell<T: ?Sized> {
    value: UnsafeCell<T>,
}

#[lang = "unsafe_cell"] // --> 只是比我们写的MyCell多了一条这个属性
pub struct UnsafeCell<T: ?Sized> {
    value: T,
}

在标准库中,有一个叫幽灵数据PhantomData<T> ,它用于模拟另一个类型,Rust在编译时会检查这个标志,让MyCell模拟一个特性

struct MyCell<T> {
    value: T,
    _pd: std::marker::PhantomData<std::cell::UnsafeCell<T>>
}

这里MyCell模拟了UnsafeCell。变相加上了 #[lang = "unsafe_cell"]这条属性让MyCell的变性保持不变。到此。你编译时就得到一个生命周期不足的错误,自己动手试试吧。

变性 整体上来说。 就是一个容器与被包裹值的一种关系

变性只是一个概念,不要过度理解造成负担,你可以想成其实啥也不是,就是一种我们假设的理论性规范, 遵守它, 程序才能正确的运行。不遵守。程序就是一个字: 炸

变性还有逆变不变,之后有机会将继续