V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
KlesaOff
V2EX  ›  Rust

关于 Rust 所有权,如果对 mut 变量进行嵌套 mut 引用该怎么理解?

  •  1
     
  •   KlesaOff · 2 天前 · 864 次点击

    最近在自学 Rust ,因为之前粗略看过一些博客教程,所以这次直接看Programming Rust这本书的翻译版,在第 5 章“引用”中遇到一个例子,感觉不能理解。

    首先看书中图 5-9 的所有权树,讲了对于可变和不可变引用时的访问关系,我以为自己理解了,可是后面看到这个例子,结果又看不明白,代码是这样的:

    ...
    也可以再次借用一个可变引用:
    let mut v = (136, 139);
    let m = &mut v;
    let m0 = &mut m.0;  // ok:从可变引用重新借用可变引用
    *m0 = 137;
    let r1 = &m.1;      // ok:从可变引用重新借用共享引用,并且和 m0 没有重叠部分
    v.1;		        // error:通过其他路经访问仍然是禁止的
    println!("{}", r1); // r1 在这里使用
    
    

    其实从第一个 ok 注释那里我就没搞明白,不是说变量只能同时存在一个可变引用吗,为什么有可变引用(“引用”为名词)m 的情况下还能从 m 那再可变引用(“引用”为动词)一个 m0 ?

    然后我问 gpt ,它是这么回答的:

    ...

    嵌套 mut 引用的场景

    假设你有一个可变变量,并希望对其创建嵌套的可变引用。以下 是一些常见的情况及其分析。

    情况 1:对整个变量的可变引用,然后尝试对部分值的可变引用

    fn main() {
        let mut x = (1, 2);
    
        let r1 = &mut x;       // 对整个变量 `x` 的可变引用
        let r2 = &mut r1.0;    // 尝试对 `x` 的一部分创建可变引用
    
        *r2 = 42;              // 修改部分值
        r1.1 = 99;             // 修改另一部分
    }
    

    错误原因: Rust 编译器会报错,提示 不能同时借用 x 的多个可变引用。这是因为:

    r1 是对整个 x 的可变引用,它控制了 x 的独占访问权。 当你尝试对 r1.0 ( x 的某个字段)创建可变引用时,r1 的作用域依然活跃,Rust 无法区分你是否会同时操作 x 的其他部分。

    错误示例的编译器消息(可能类似于):

    error[E0499]: cannot borrow `r1.0` as mutable more than once at a time
    

    ...

    但事实上,我尝试了类似代码,并不会报错:

    fn main() {
        let mut v = (136, 139);
        let m = &mut v;
        let m0 = &mut m.0;
        *m0 = 137;
        m.1 = 42;
        println!("{:?}", v)
    }
    

    其运行结果是:

    (137, 42)
    

    很多资料都没有讲解关于这种嵌套的引用会发生什么结果,也不知道什么情况是可以什么是不行,我没看过这例子时还以为压根不能创建 m0 呢,所以想着找 GPT 问问,结果告诉的内容是错的。

    请问该如何解释上面书中的代码,如何更好地理解 Rust 在这种嵌套情况下创建引用的做法是否成功?请各位赐教

    18 条回复    2025-01-17 22:16:12 +08:00
    boxrq
        1
    boxrq  
       2 天前
    m0 不是对 v 的直接引用,而是对 m.0 的引用。
    关于"只能同时存在一个可变引用"的规则:
    这个规则其实更准确的表述应该是:对于同一块内存区域,在同一时间只能有一个可变引用。
    想象一下现实生活中的场景:
    1. 你有一间房子(原始值 v )
    2. 你把整个房子的管理权交给物业(可变引用 m )
    3. 物业可以再把某个房间的使用权分配给他人(重借用 m0 )
    w568w
        2
    w568w  
       2 天前
    Check: https://doc.rust-lang.org/nomicon/borrow-splitting.html

    我记得这个行为有一些严格的 references ,不过找不到了。翻一下 Language references 吧。

    简单来说:借用检查器理解一些基本的东西,它确实充分理解 struct ,知道可以同时借用 struct 的不相交字段。

    至于最后一个例子,Rust 的作用域是语义的,也就是说:

    fn main() {
    let mut v = (136, 139);
    let m = &mut v;
    let m0 = &mut m.0;
    *m0 = 137; // <-- 从这一行开始,不再使用 m0 ,因此可以理解为 m0 在这里生命结束
    m.1 = 42;
    println!("{:?}", v)
    }
    aloxaf
        3
    aloxaf  
       2 天前
    rust 的借用检查器一直在改进

    它早期是基于词法作用域的,所以会出现你书里说的那种情况,你在 godbolt 里测试早期的 rust 版本就能看到报错了。当时写 rust 程序常常需要把一些创建临时引用的代码用大括号另起一个作用域,看起来莫名其妙的,就是为了规避这个问题。

    后面引入了 non-lexical lifetimes (NLL) 版本,会智能分析这个引用和其他引用之间是否存在冲突,而不是粗暴地看作用域,大部分情况下都能给出复合直觉的结果。少量 edge case 可能要等下一代检查器 polonius 了,不过这几年都没啥动静。
    w568w
        4
    w568w  
       2 天前
    发现上面说的有点离题了。然后打了一大段字被 V2EX 吞掉了……

    为了说明你的问题和 struct 其实没有关系,这是一个简化的例子:

    fn main() {
    let mut a = 42;

    let mut_a = &mut a;
    let another_mut_a = &mut *mut_a;
    *another_mut_a = 12345;
    *mut_a = 42;

    println!("{}", a);
    }
    openmynet
        5
    openmynet  
       2 天前
    rust 的生命周期和现实世界高度类似,一个东西(在生命周期内)永远只会在一个人手里,如果你看到两个一样的东西其他一个必定是复制品(.clone())

    ```js
    let r1 = &m.1; // 139 已经被 r1 拿在在手里了
    v.1; // error:v.1 取回 139
    println!("{}", r1); // r1 现在手里没有 139 , 你现在非要让 r1 给你拿出来,r1 拿不出来,rust 编译器向上裁定 v.1 不允许拿 139 。
    ```js
    调整一下获取 139 的顺序

    ```js
    let r1 = &m.1; // 139 已经被 r1 拿在在手里了
    println!("{}", r1); // r1 现在手里有 139 , 你现在让 r1 给你拿出来,139 被成功打印。
    v.1; // v.1 拿回 139

    ```
    要么你在 139 被借出去之前弄一个复制品.clone()

    ```js
    let mut v = (136, 139);
    let v1 = v.1.clone();
    let m = &mut v;
    let m0 = &mut m.0;
    *m0 = 137;

    let r1 = &m.1;
    v1;
    println!("{}", r1);
    ```
    wjx0912
        6
    wjx0912  
       2 天前
    楼上说的都太复杂了。如果编译器扫描到后面没有再使用变量,就对借用的对象进行归还。
    书上好像有 3 个原则来着,记住它就行了。

    ```rust
    fn main() {
    let mut v = (136, 139);
    let m = &mut v; // v 拥有所有权,m 借用了它。所以后面 v 不可用,m 可用
    let m0 = &mut m.0; // rust 没有局部借用。到这里,m,v 都不可用了。只有借用者 m0 可用
    *m0 = 137; // 编译器发现后面没有使用 m0 ,就归还了。又回到了‘v 不可用,m 可用’
    m.1 = 42; // 归还 m ,之后 v 可用
    println!("{:?}", v)
    }
    ```
    KlesaOff
        7
    KlesaOff  
    OP
       2 天前 via Android
    @w568w
    看了你的例子,我开始以为关键是生命周期:another_mut_a 的生命周期被 mut_a 包括在内,而 mut_a 又被 a 包括,所以代码不出错。
    我刚刚把我主楼最后代码的变量创建换个位置,试了试下面这个代码,也能正常运行不报错
    ```rust
    fn main() {
    let mut v = (136, 139);
    let m = &mut v;
    let m1 = &mut m.1;
    m.0 = 137;
    *m1 = 42;
    println!("{:?}", v)
    }
    ```
    我本来以为在使用 m1 之前访问了 m.0 ,相当于 m1 和 m 的生命周期重叠了一部分,所以应该报错。
    但事实没有,而如果我在 m.0 和*m1 之间加上一句`println!("{:?}", m);`,就会报错。
    我想,按照书中的概念,v 是所有权树的父节点,v.0 和 v.1 是其子节点,有了父节点的&mut 后,可以在此基础上&mut 子节点;使用时只要使用&mut 子节点时,若使用的子节点之间生命周期没有重叠,就能够正常用(就像本楼例子和主楼最后);但如果在&mut 子节点生命周期没结束时直接使用&mut 父节点,就会因为生命周期重叠导致同时有两个方式访问同一个&mut ,所以会报错。
    你那个例子的感觉像是生命周期正好层层被包住所以没有冲突,不知道我这样理解对不对?
    visper
        8
    visper  
       2 天前
    我理解的,rust 最终的目的,是想在编译阶段确保你不可能同时通过不同的变量来修改到同一内存。为了这个目的,他设置了一些规则,宁可杀错不可放过。但是最开始的时候,可能算法想得不够清,规则定义的过死,误杀的情况多。造成写代码的时候限制过大。然后随着慢慢改进,编译器越来越聪明,误杀的情况越来越少。所以,有时候不要想太多,你看一下觉得是没有冲突问题,编译器也认为没有。那就行了。如果有时候你觉得肯定没问题,但是编译器认为有,那就是编译器还不会处理这种情况,要绕过它。
    KlesaOff
        9
    KlesaOff  
    OP
       2 天前 via Android
    @wjx0912
    你好,可以看我 7 楼发的,你注释里说 let m0 时 v 、m 都不可用看来是错的,这时 m.1 应该是可以用的( v.1 确实不行),哪怕 m0 的使用还没结束。
    ke1e
        10
    ke1e  
       2 天前
    我的经验你就写,按你理解的写,然后看 cargo check 报红,然后改,改多了就理解了
    PTLin
        11
    PTLin  
       2 天前
    这个叫重借用 reborrow ,确实很少有资料讲过,为此以前我写过笔记整理了一下 https://alabaster-linen-4dc.notion.site/Rust-86f927bca1794b3b95e3b5ab5f81b9c4
    KlesaOff
        12
    KlesaOff  
    OP
       2 天前 via Android
    @PTLin 老实说你写的我也看不是太明白
    KlesaOff
        13
    KlesaOff  
    OP
       2 天前 via Android
    @PTLin 我本想业余学 rust ,然后自己实现一些本地小项目满足个人学习和使用,然后学下 bevy 弄点小玩意加深语言理解并保持驱动力,但目前确实出乎意料,因为之前一次看教程时都没有注意到这次的问题
    tedzhou1221
        14
    tedzhou1221  
       1 天前 via Android
    KlesaOff
        15
    KlesaOff  
    OP
       1 天前 via Android
    @tedzhou1221 写得挺实在的,我之后按照这个思路来判断相关场景
    这文章是你写的吗?确实能补充很多书/教程不涉及的内容
    tedzhou1221
        16
    tedzhou1221  
       22 小时 44 分钟前
    #15 肯定不是啊。我也是在入门。《 rust 圣经》是入门必读。
    tedzhou1221
        17
    tedzhou1221  
       22 小时 43 分钟前
    我看错了,我以为我发的是《 rust 圣经》
    w568w
        18
    w568w  
       14 小时 58 分钟前
    @KlesaOff #12 根据楼上仁兄的介绍,我也查到了 Rust 仓库关于 reborrow 的讨论: https://github.com/rust-lang/reference/issues/788

    总之这部分目前确实是没有比较详细的官方文档的,内部实现可能也不是特别完备,一般初学不用过深了解,简单理解上面说的「同一时间、同一块内存只能使用一个可变引用」的规则就可以判断代码合法性了。如果编译器不够聪明(例如借用数组切片),就用 unsafe 帮他体面。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2692 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 05:14 · PVG 13:14 · LAX 21:14 · JFK 00:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.