• rust中的reborrow和NLL


    reborrow

    我们看下面这段代码

    fn main() {
        let mut num = 123;
        let ref1 = &mut num;     // 可变引用
    
        add(ref1);               // 传递给 add 函数
    
        println!("{}", ref1);    // 再次使用ref1
    }
    
    fn add(num: &mut i32) {
        println!("{}", *num);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

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

    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.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

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

    这段文本描述了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 借用系统的一部分,有助于编写安全且高效的代码。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

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

    // 下面这两行是等价的
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    NLL

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

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

    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作用域在这里结束
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

  • 相关阅读:
    [自研开源] MyData 数据集成之数据过滤 v0.7.2
    大数据:Shell的操作
    〖Python 数据库开发实战 - Redis篇⑥〗- Redis常用数据结构 - 字符串类型
    修改一下测试用例的生成方式 算法学习笔记3
    基于桶的排序之基数排序以及排序方法总结
    排序算法-基数排序法(RadixSort)
    python办公之使用xlrd读取excel文件
    【JSS-22双延时时间继电器】
    错误方法修改用户名后,开机提示无法登录到你的账户的正确解决方法
    TCP的粘包问题及解决办法
  • 原文地址:https://blog.csdn.net/zy010101/article/details/132658480