开始

以下知识点会比较杂乱,看Rust的一些书籍文档很多时候都是简单带过,可以理解Rust在努力简化一些概念看起来容易学习,但是有些点一深入就会发现可能没有想象中简单,如果理解不够透切阅读代码会有很多的障碍,所以汇总一下自己的理解。

先说Copy,Clone和Move

image.png
在rust大部分类型都实现了copy trait都会使用memcpy直接复制一份内存出来。
而对于没有实现copy trait的类型,默认都是所有权移动的行为,但是实际在内存角度看仍然是memcpy,只是复制完成后就把原有内存释放掉;所以我们才在变量所有权移动后就不能再使用原有变量了;而所有权这个特性,也保证了变量绑定内存永远都是独一份的。

image.png

无论引用,还是胖指针或者基本类型在栈上面都是一小块的内存。

Sized和Unsized

Szied是对于编译期可以确定内存大小的类型,它们可以分配在栈上也可以分配在堆上;但是对于编译期无法知道大小的类型它们只能分配在堆上,而我们也无法在代码上直接声明这种类型的变量;只能通过引用或者胖指针来间接引用这块不定大小的内存。
这些Unsized类型大致有三类:

  1. str // 字符串
  2. [T] // slice
  3. dyn Write // trait object

不可变引用和可变引用,生命周期

对于引用,一般情况下每当声明一个变量伴随着声明一个新的生命周期,包括调用函数时声明的函数参数也会产生一个临时局部的生命周期持续到函数调用结束
image.png
image.png
lifetime是也是一种类型,也就是一个Rust变量的实际类型是Type + Lifetime,所以可以理解为什么lifetime会放到类型参数上

但是不一样的是一般我们的变量类型声明后就不会变化,哪怕赋值给其他变量其他变量也不会改变类型(除非自己强行转换),但是lifetime就不一样
每次绑定新的变量时(这个变量的lifetime类型不一致的时候)就会产生一个新的lifetime类型,而这个类型时原来的lifetime类型的父类型(就是父类型不是子类型)

不可变引用都实现了copy trait;可变引用都是move语义

一般情况下引用的生命周期都是会不停缩短,
但是也可以通过赋值外部变量来达到延长生命周期

let x = 0;
let z;
{
    let y = &x;
    z = y;
}
println!("{}", z)

但是本质上这个是因为不可变引用都是实现了copy trait;而且z的生命周期也小于x的生命周期,
所以赋值就直接从y变量的内存copy了一份内存到z变量给人一种错觉好像生命周期延长了;

而可变引用则是move语义的,只要赋值就会发生移动,后面再想使用原来的变量就会出问题

let mut x = 0;
let mut z;
{
    let y = &mut x;
    z = y;
    print!("{}", y); // error: value borrowed here after move
}
println!("{}", z);

可变引用的reborrow

虽然刚刚提到可变引用都是所有权移动,但是有些场景会看起来很迷惑:

fn test(z: &mut i32) {
    println!("{}", z);
}

fn main()  {
  let mut x = 10;
  let y = &mut x;
  test(y);
  println!("{}", y)
}

明显从代码上看y已经move到test函数内,但是现在实际y仍然可以在函数外部可用,这跟move语义
是不符合的

但是test方法实际调用时产生reborrow
test(&mut *y)
先解引用再产生新的借用,然后这个新的借用才会被move到函数里面

而这种隐含的rebrrow可以产生在任何编译器已经知道目标类型是可变引用类型的情况下:
let z: &mut i32 = y;

test(z: &mut i32)

但是
fn foo<T>(_: T) {}
这个泛型方法不会触发reborrow,因为T没有明确表明是可变应用类型

fn bar<T>(_a: T, _b: T) {}

fn main() {
    let mut i = 42;
    let mut j = 43;
    let x = &mut i;
    let y = &mut j;
    bar(x, y);   // Moves x, but reborrows y.
    let _z = x;  // error[E0382]: use of moved value: `x`
    let _t = y;  // Works fine. 
}

而这个方法因为第一个参数编译器还不能推断是否是可变引用会直接使用move,但是第二个参数编译器可以知道是可变引用
就会使用reborrow

impl<'a> WriteBufView<'a> {
    fn set_u8(&'a mut self, value: u8) -> Result<(), BufError> {}
}

而这种情况虽然已经明确类型是可变引用,明确生命周期也被指定的情况下,就不会产生reborrow

另外一种场景

#[derive(Debug)]
struct Foo;

impl Foo {
    fn mutate_and_share(&mut self) -> &Self { &*self }
    fn share(&self) {}
}

fn main() {
    let mut foo = Foo;
    let loan = foo.mutate_and_share();
    foo.share(); // error
    println!("{:?}", loan);
}

这种场景下也不会产生reborrow,因为等效于:

struct Foo;

impl Foo {
    fn mutate_and_share<'a>(&'a mut self) -> &'a Self { &'a *self }
    fn share<'a>(&'a self) {}
}

fn main() {
    'b: {
        let mut foo: Foo = Foo;
        'c: {
            let loan: &'c Foo = Foo::mutate_and_share::<'c>(&'c mut foo);
            'd: {
                Foo::share::<'d>(&'d foo);
            }
            println!("{:?}", loan);
        }
    }
}

目前rust还没有文档明确reborrow这个行为触发的场景,基本还是看编译器实际推断如何触发

引用在结构体上

可变引用
#[derive(Debug)]
struct Bar {
    fieldC: i32
}
#[derive(Debug)]
struct Foo {
    fieldA: i32,
    fieldB: i32,
    bar: Bar
}
fn main() {
    let mut foo = Foo{
        fieldA: 0, 
        fieldB: 1, 
        bar: Bar{ 
            fieldC: 2 
        }
    };
    let bar = &mut foo.bar;
    let x = &mut foo.bar.fieldC;
    let b = &mut foo.fieldA;
    //let f = &mut foo; // error, 因为foo.bar的引用隐含借用foo,所以foo不能再同时产生mut引用
    //let f = &foo; // errorm, 理由同上
    // *x = 6;
    *b = 7;
    // println!("{:?}", bar); // error, 会与foo.bar.fieldC可变引用冲突
    println!("{:?}", foo);
}

image.png

总结

  1. 当结构字段被可变引用时,它的父节点都会隐含借用,但是其他没有关联的字段例如:fieldA,仍然可以产生可变引用
  2. 当结构字段被可变引用时,例如:bar,如果它的子字段例如:fieldC也要产生可变引用,这两个引用的生命周期不能重叠
不可变引用
#[derive(Debug)]
struct Bar {
    fieldC: i32
}
#[derive(Debug)]
struct Foo {
    fieldA: i32,
    fieldB: i32,
    bar: Bar
}
fn main() {
    let mut foo = Foo{
        fieldA: 0, 
        fieldB: 1, 
        bar: Bar{ 
            fieldC: 2 
        }
    };
     // let f = &foo; // error, 跟foo.fieldA的可变引用冲突
    let bar = &foo.bar; 
    let b = &mut foo.fieldA; // ok
    // let f = &mut foo; // error,跟foo.bar的不可变引用冲突
    *b = 7;
    println!("{:?}", bar);
    // println!("{:?}", f);
}

image.png
总结

  1. 当结构字段被不可变引用时,它的父节点都会隐含借用,但是其他没有关联的字段例如:fieldA,仍然可以产生可变引用。

最后汇总如果只引用结构的一部分时,借用关系就会往上传递,就像加锁一样,防止被其他地方操作影响,冻结整个关系链;但是其他不关联的节点仍然不受影响

生命周期在结构体中

struct Entity<'a> {
    part: &'a str,
}

意味着Entity的实例不能够outlive超过part字段的生命周期

生命周期在泛型类型参数中

当声明一个返回引用的函数,rust就会要求我们明确标明输入参数的生命周期类型参数
和对应返回引用的生命周期

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

因为rust需要从返回引用的生命周期来继续函数调用后的借用检查, 而实际刚刚声明的函数等效于

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { // 那么返回引用是'a还是'b,后面借用检查无法继续分析
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

在方法中或者函数参数的生命周期我们称作输入生命周期,而返回值的生命周期我们称作输出生命周期

但是当我们按照编译修改函数后

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这里同样的'a生命周期参数意味着输出生命周期就是x,y输入生命周期中最小的一个。

总结

Rust的引用和生命周期感觉并不如文档那样小清新,其实隐藏很多小细节,有些地方文档也没有明确说明,只能凭着感觉总结。

引用

Do mutable references have move semantics?
better documentation of reborrowing
mutably borrow fields from a mutably borrowed struct
Why is the mutable reference not moved here?
【初学者】Rust这个引用问题怎么解决?
生命周期的局限


tain335
576 声望196 粉丝

Keep it simple.


« 上一篇
Yjs代码简析