rust中的reborrow和NLL

2023-09-06 13:52:13 浏览数 (1)

reborrow

我们看下面这段代码

代码语言:javascript复制
fn main() {
    let mut num = 123;
    let ref1 = &mut num;     // 可变引用

    add(ref1);               // 传递给 add 函数

    println!("{}", ref1);    // 再次使用ref1
}

fn add(num: &mut i32) {
    println!("{}", *num);
}

我们知道可变引用是没有实现Copy trait的,因此,当ref1传递给add函数之后,其所有权应该被转移到add函数内,之后应该无法使用ref1,但是上面这段代码是可以编译,运行的。这是为什么呢?

经过辛苦的寻找,在github上找到了相关的pull request以及rust核心开发者nikomatsakis在这篇文档中提到的reborrow。原文如下:

代码语言:javascript复制
One of the less obvious but more important coercions is what I call
*reborrowing*, though it's really a special case of autoborrow. The
idea here is that when we see a parameter of type `&'a T` or `&'a mut
T` we always "reborrow" it, effectively converting to `&'b T` or `&'b
mut T`.  While both are borrowed pointers, the reborrowed version has
a different (generally shorter) lifetime. Let me give an example where
this becomes important:

    fn update(x: &mut int) {
        *x  = 1;
    }

    fn update_twice(x: &mut int) {
        update(x);
        update(x);
    }
    
In fact, thanks to auto-borrowing, the second function is implicitly
transformed to:

    fn update_twice(x: &mut int) {
        update(&mut *x);
        update(&mut *x);
    }

This is needed because `&mut` pointers are *affine*, meaning that
otherwise the first call to `update(x)` would move the pointer `x`
into the callee, leading to an error during the second call. The
reborrowing however means that we are in fact not moving `x` but
rather a temporary pointer (let's call it `y`). So long as `y` exists,
access to `x` is disabled, so this is very similar to giving `x` away.
However, lifetime inference will find that the lifetime of this
temporary pointer `y` is limited to the first call to `update` itself,
and so after the call access to `x` will be restored. The borrow
checker rules permit reborrowing under the same conditions in which a
move would be allowed, so this transformation never introduces errors.

对应的译文(来自chatgpt3.5的翻译,非常棒)如下:

代码语言:javascript复制
这段文本描述了Rust中的 "reborrowing" 概念,即重新借用引用的特性,它实际上是 "autoborrow" 的
一种特殊情况。"reborrowing" 的核心思想是,当我们遇到一个类型为 &'a T 或 &'a mut T 的参数时,
我们总是会对它进行 "reborrow",实际上将其转换为 &'b T 或 &'b mut T。虽然这两者都是借用指针,
但 "reborrowed" 版本具有不同(通常更短)的生命周期。
下面通过一个示例来说明 "reborrowing" 为何重要:

fn update(x: &mut i32) {
    *x  = 1;
}

fn update_twice(x: &mut i32) {
    update(x);
    update(x);
}
实际上,由于 "auto-borrowing",第二个函数会被隐式转换为:

fn update_twice(x: &mut i32) {
    update(&mut *x);
    update(&mut *x);
}
这是因为 &mut 指针是 "affine" 的,这意味着否则第一次调用 update(x) 会将指针 x 移动到被调用的
函数内部,导致第二次调用时发生错误。但是,"reborrowing" 意味着我们实际上并没有移动 x,而是
移动了一个临时指针(我们称之为 y)。只要 y 存在,对 x 的访问就会被禁用,因此这与将 x 移动
出去非常相似。然而,生命周期推断将发现,临时指针 y 的生命周期仅限于第一次调用 update 本身,
因此在调用后访问 x 将会被恢复。借用检查规则允许在允许移动的情况下进行 "reborrowing",
因此此转换永远不会引入错3误。

综上所述,"reborrowing" 是 Rust 中的一个重要借用模式,它有助于确保代码的安全性和正确性,
同时允许对数据进行操作而不引入潜在的错误。
这种机制是 Rust 借用系统的一部分,有助于编写安全且高效的代码。

总结一下,对于上面的代码而言:

代码语言:javascript复制
// 下面这两行是等价的
add(ref1);
add(&mut *ref1);


// 对于不可变引用而言也是一样的,但是由于不可变引用实现了Copy trait,通常在不可变引用身上不常见。

let num2 = 456;
let ref2 = &num2;

// 下面这两行是等价的
my_print(ref2);
my_print(&*ref2);

fn my_print(num: &i32) {
    println!("{}", num);
}

至于为什么大量的文档和资料没有提到reborrow这个问题,可能得归结于此。在pull request中看到了核心开发者认为正式化reborrow时机不对。

NLL

在Rust的早期版本中,生命周期推断基于词法分析(Lexical analysis),而为了解决这个问题,Rust引入了非词法生命周期(Non-Lexical Lifetime),从而提高了编译器对生命周期的理解和推断。

Rust在1.31版本后提供的NLL(Non-Lexical Lifetime)生命周期简化规则。变量的生命周期跟它的作用域等同,而现在,变量的生命周期结束于它最后一次被使用的位置。

代码语言:javascript复制
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // 新编译器中,r1,r2作用域在这里结束

    let r3 = &mut s;
    println!("{}", r3);
}   // 老编译器中,r1、r2、r3作用域在这里结束
    // 新编译器中,r3作用域在这里结束

在现在版本的rust编译器上,上面的代码可以通过正确编译,运行。有了NLL,大大增加了在rust中编写代码的灵活性。

0 人点赞