Android程序员初学Rust-借用

111 阅读4分钟

1.png

正如我们之前所见,调用函数时,你不必转移所有权,而是可以让函数借用该值:

#[derive(Debug)]
struct Point(i32, i32);

fn add(p1: &Point, p2: &Point) -> Point {
    Point(p1.0 + p2.0, p1.1 + p2.1)
}

fn main() {
    let p1 = Point(3, 4);
    let p2 = Point(10, 20);
    let p3 = add(&p1, &p2);
    println!("{p1:?} + {p2:?} = {p3:?}");
}

// Output
// Point(3, 4) + Point(10, 20) = Point(13, 24)

add 函数借用两个 Point 并返回一个新的 Point。 调用者 main 保留输入的所有权。

借用检查

2.jpg

Rust 的借用检查器对借用值的方式施加了限制。我们已经了解到,引用的生命周期不能超过它所借用的值:

fn main() {
    let x_ref = {
        let x = 10;
        &x // compile error: borrowed value does not live long enough
    };
    dbg!(x_ref);
}

借用检查器还会执行第二条主要规则:别名规则。对于给定的值,在任何时候:

  • 可以有多个对该值的共享引用。
  • 有且仅有一个对该值的独占引用。
fn main() {
    let mut a: i32 = 10;
    let b: &i32 = &a;

    {
        let c: &mut i32 = &mut a;
        *c = 20;
    }

    dbg!(a);
    dbg!(b);
}

// compile error : cannot borrow `a` as mutable because it is also borrowed as immutable

“生存期长于”规则在我们初次学习引用时已有演示。在此回顾该规则。

上述代码无法编译,因为变量 a 同时被可变借用(通过 c )和不可变借用(通过 b )。

请注意,这里的要求是,在同一时刻不能存在冲突的引用。引用在何处解引用并不重要。尝试注释掉 *c = 20,可以看到即使从未使用 c,编译错误依然会出现。

还要注意,中间引用 c 并非引发借用冲突的必要因素。将 c 替换为对 a 的直接可变操作,并展示这样做也会产生类似的错误。

这是因为对一个值的直接可变操作实际上会创建一个临时的可变引用。

bdbg! 语句移到引入 c 的作用域之前,代码就能编译通过。

做出这一更改后,编译器可以意识到,在通过 ca 进行新的可变借用之前,b 已经完成了他的使命。这是借用检查器的一个特性,称为“非词法生存期”。

借用错误

3.png

为了具体展示这些借用规则是如何防止内存错误的,我们来考虑一下在存在对集合元素的引用时修改集合的情况:

fn main() {
    let mut vec = vec![1, 2, 3, 4, 5];
    let elem = &vec[2];
    vec.push(6);
    dbg!(elem);
}

// comile error: cannot borrow `vec` as mutable because it is also borrowed as immutable

同样,想想迭代器失效的情况:

fn main() {
    let mut vec = vec![1, 2, 3, 4, 5];
    for elem in &vec {
        vec.push(elem * 2);
    }
}

// comile error: cannot borrow `vec` as mutable because it is also borrowed as immutable

以上两种情况下,如果添加元素集合必须重新分配内存,那么通过向集合中插入新元素来修改集合,都可能使现有的集合元素引用失效。

尤其是迭代器失效的情况,大家在 Kotlin/Java 中一定干过迭代列表删除元素的事情,这个时候,Java 一定会抛出一个运行时异常。

内部可变

4.jpg

在某些情况下,有必要在共享(只读)引用后修改数据。例如,一个共享的数据结构可能有一个内部缓存,并希望在只读方法中更新该缓存。

“内部可变性”模式允许在共享引用后进行独占(可变)访问。标准库提供了几种实现这一点的方法,同时仍然确保内存安全,通常是通过运行时检查来实现(即不会在编译器检查,执行时如果违反规则会 panic)。

Cell

Cell 封装了一个值,并允许仅通过对 Cell 的共享引用获取或设置该值。不过,它不允许对内部值有任何引用。由于不存在引用,借用规则就不会被破坏。

use std::cell::Cell;

fn main() {
    // 注意 cell 没有被声明为可变的。
    let cell = Cell::new(5);

    cell.set(123);
    dbg!(cell.get());
}

// Output
// [src/main.rs:8:5] cell.get() = 123

Cell 是一种确保安全的简单方式:它有一个接收 &selfset 方法。这不需要运行时检查,但需要移动值,这本身可能会有一定开销。

RefCell

RefCell 允许通过提供替代类型 RefRefMut 来访问和修改封装的值,这些类型模拟 &T / &mut T,但实际上不是 Rust 引用。

这些类型使用 RefCell 中的计数器执行动态检查,以防止 RefMut 与其他 Ref / RefMut 同时存在。

通过实现 Deref(以及 RefMutDerefMut),这些类型允许在内部值上调用方法,同时防止引用逃逸。

use std::cell::RefCell;

fn main() {
    
    let cell = RefCell::new(5);

    {
        let mut cell_ref = cell.borrow_mut();
        *cell_ref = 123;

        // 下面的代码会引发运行时 panic
        // let other = cell.borrow();
        // println!("{}", *other);
    }

    println!("{cell:?}");
}

// Output
// RefCell { value: 123 }

RefCell 通过运行时检查来执行 Rust 通常的借用规则(要么有多个共享引用,要么有一个唯一的可变引用)。在这种情况下,所有的借用时间都非常短,而且从不重叠,所以这些检查总是会成功。