08|所有权:值的借用是如何工作的?

117 阅读5分钟

正式开始

上一小节回顾

  1. 当我们进行变量赋值、传参和函数返回时,如果涉及的数据结构没有实现 Copy trait,就会默认使用 Move 语义转移值的所有权,失去所有权的变量将无法继续访问原来的数据;
  2. 如果数据结构实现了 Copy trait,就会使用 Copy 语义,自动把值复制一份,原有的变量还能继续访问

Borrow 语义

  1. Borrow 语义允许一个值的所有权,在不发生转移的情况下,被其它上下文使用
  2. Borrow 语义通过引用语法(& 或者 &mut)来实现
  3. 默认情况下,Rust 的借用都是只读的

其他语言引用与Rust借用的区别

  1. 在其他语言中,引用是一种别名,多个引用拥有对值的无差别的访问权限,本质上是共享了所有权
  2. 在 Rust 下,所有的引用都只是借用了“临时使用权”,它并不破坏值的单一所有权约束

只读借用 / 引用

  1. 本质上,引用是一个受控的指针,指向某个特定的类型
  2. Rust 所有的参数传递都是传值,不管是 Copy 还是 Move
  3. 在 Rust 中,你必须显式地把某个数据的引用,传给另一个函数

Rust 的引用实现了 Copy trait,这个引用会被复制一份交给要调用的函数。

对这个函数来说,它并不拥有数据本身,数据只是临时借给它使用,所有权还在原来的拥有者那里

fn main() {
    let data = vec![1, 2, 3, 4];
    let data1 = &data;
    // 值的地址是什么?引用的地址又是什么?
    // addr of value: 0x7ff7bf4deb68(0x7ff7bf4deb68), addr of data 0x7f7bcef05bb0, data1: 0x7ff7bf4deb80
    println!(
        "addr of value: {:p}({:p}), addr of data {:p}, data1: {:p}",
        &data, data1, &*data, &data1
    );
    // sum of data1: 10
    println!("sum of data1: {}", sum(data1));

    // 堆上数据的地址是什么?
    // addr of items: [0x7f7bcef05bb0, 0x7f9fe6f05bb4, 0x7f9fe6f05bb8, 0x7f9fe6f05bbc]
    println!(
        "addr of items: [{:p}, {:p}, {:p}, {:p}]",
        &data[0], &data[1], &data[2], &data[3]
    );
}

fn sum(data: &[u32]) -> u32 {
    // 值的地址会改变么?引用的地址会改变么?
    // addr of value: 0x7f9fe6f05bb0, addr of ref: 0x7ff7bf4de9b8
    println!("addr of value: {:p}, addr of ref: {:p}", data, &data);
    data.iter().sum()
}

image.png

  1. data1、&data 和传到 sum() 里的 data1’ 都指向 data 本身,这个值的地址是固定的。但是它们引用的地址都是不同的
  2. 只读引用实现了 Copy trait,也就意味着引用的赋值、传参都会产生新的浅拷贝
  3. 值的任意多个引用并不会影响所有权的唯一性

借用的生命周期及其约束

借用不能超过(outlive)值的生存期

举个例子

在堆内存中,使用栈内存的引用

  1. 编译不通过:

生命周期更长的 main() 函数变量 r ,引用了生命周期更短的 local_ref() 函数里的局部变量

fn main() {
    let r = local_ref();
    println!("r: {:p}", r);
}

fn local_ref<'a>() -> &'a i32 {
    let a = 42;
    &a
}
  1. 编译通过
fn main() {
    let mut data: Vec<&u32> = Vec::new();
    let v = 42;
    data.push(&v);
    println!("data: {:?}", data);
}
  1. 编译不通过
fn main() {
    let mut data: Vec<&u32> = Vec::new();
    push_local_ref(&mut data);
    println!("data: {:?}", data);
}

fn push_local_ref(data: &mut Vec<&u32>) {
    let v = 42;
    data.push(&v);
}

  1. 一个核心要素“在一个作用域下,同一时刻,一个值只能有一个所有者”
  2. 核心只需要关心调用栈的生命周期

image.png

可变借用 / 引用

Rust 的限制

  1. 在一个作用域内,仅允许一个活跃的可变引用。 所谓活跃,就是真正被使用来修改数据的可变引用,如果只是定义了,却没有使用或者当作只读引用使用,不算活跃。
  2. 在一个作用域内,活跃的可变引用(写)和只读引用(读)是互斥的,不能同时存在

再理解一下第一性原理

image.png

小结

  1. 一个值在同一时刻只有一个所有者。

    a. 当所有者离开作用域,其拥有的值会被丢弃。

    b. 赋值或者传参会导致值 Move,所有权被转移,一旦所有权转移,之前的变量就不能访问。

  2. 如果值实现了 Copy trait,那么赋值或传参会使用 Copy 语义,相应的值会被按位拷贝,产生新的值。

  3. 一个值可以有多个只读引用。

  4. 一个值可以有唯一一个活跃的可变引用。可变引用(写)和只读引用(读)是互斥的关系。

  5. 引用的生命周期不能超出值的生命周期。

image.png

新鲜知识

  1. 编译器在作用域结束之前判断不再使用的引用的能力被称为非词法作用域生命周期(Non-Lexical Lifetimes,简称NLL)

精彩链接

  1. manishearth.github.io/blog/2015/0…

精选问答

  1. 希望有一段内存的生命周期是由业务逻辑决定,在rust中要如何实现呢?

    a. 可以用 Box::leak/Box:into_raw/ManuallyDrop 让堆内存完全脱离自动管理。按照需求,可以使用 ManuallyDrop

    b. ManuallyDrop 使用Demo

  2. 有且仅有一个活跃的可变引用存在,怎么理解? a. 「活跃的」这个定语是 Rust 编译器做的一个优化,可以让我们不用添加不必要的作用域 b. 可以简单这么认为:在撰写代码的时候,如果你在某处使用了一个可变引用之后就再也没用了,那么这处之后的地方这个可变引用就不是活跃的了