RUST 所有权移动问题

2023-10-01 03:46:06 +08:00
 caobug

刚随手写了一个示例,尝试探测编译器检测机制。以下代码在 JAVA/C/C++等众多语言中没有任何问题,即使按照 RUST 的所有权原则,也应该正常。但实际上编译失败了。

struct Dog {
    name: String,
}

impl Dog {
    fn release(self) {
        println!("{}", self.name)
    }
}

struct Person {
    dog: Dog,
    name: String,
}

fn main() {
    let mut person = Person {
        dog: Dog {
            name: String::from("Hamel"),
        },
        name: String::from("Zoe"),
    };

    person.dog.release();

    for _ in 0..1 {
        person.dog = Dog {
            name: String::from("Hogge"),
        };
    }

    println!("{} got a new dog: {}", person.name, person.dog.name);
}

以上代码我先通过 Person 关联了 Dog ,随后释放 Person 中的 Dog ,到此 Dog 不再有效,若后续直接调用会编译失败。

我跟着创建一个百分比会执行的 for 循环重新赋值,按理说指针已发生了变更,后续在循环外使用的 Dog 是新建的对象不会有问题。

但事上上编译失败。Rust 的检测器似乎没有认真评估 for 中的条件,认为 loop 至少执行一次,而 for 却不确定。当我把 for 改成 loop ,它确实正常了。

loop {
    person.dog = Dog {
        name: String::from("Hogge"),
    };
    break
}

我接着在 loop 中加一个百分比会执行的条件判断,最终仍然失败了。

loop {
    if true {
        person.dog = Dog {
            name: String::from("Hogge"),
        };
    }
    break
}

如果说写 C/C++满脑袋都是寄存器、堆、栈、指针...,那么 RUST 必然是所有权检查器。

2188 次点击
所在节点    Rust
24 条回复
DianQK
2023-10-01 06:06:54 +08:00
(似乎是 bug ,感觉可以提个 issue
binhb
2023-10-01 06:48:40 +08:00
因为是静态所有权检查,条件不确定,可能不被执行
qdwang
2023-10-01 09:02:57 +08:00
rust 编译过程中,除非是特殊指定在编译器运算的代码(比如 const fn ),其他代码都是不会运算的,只能根据一些特殊情况做一些处理。比如 loop 内非条件语句下认定为会执行到。for 里不一定必然执行。还有例如有些循环情况算必然执行多次,这样就不可以用 FnOnce 这样。
Leviathann
2023-10-01 11:36:15 +08:00
所有权检查实际上是一种证明
你要做的是向编译器提供证据
rrfeng
2023-10-01 12:06:06 +08:00
这跟不定长数组越界访问一样啊,编译器不会检查条件的。
lance6716
2023-10-01 19:28:31 +08:00
只是有个度不想继续细化了,毕竟停机问题
Kaiv2
2023-10-02 09:48:45 +08:00
loop {
if true {
person.dog = Dog {
name: String::from("Hogge"),
};
break;
}
}
编译成功
caobug
2023-10-02 13:26:26 +08:00
@Kaiv2 RUST 编译检查默认会检查完整的分支,即 if 和 else
swordcoming9527
2023-10-03 20:00:01 +08:00
硬要解决这个问题只能等待以后编译器在各种 edge case 优化的更好,但实践中可能没啥意义。
这个问题,是 People 里面的 dog 可能被 Move ,dog 被 Move 时,People 其实是处于一种“不可用”的状态,在 dog 被填充回来之前,对 People 的其他操作都是不允许的。如果在真实的实践中,只需要提前设计好 dog 被 Move 的状态,典型的就是 dog: Option<Dog>。
caobug
2023-10-03 23:49:03 +08:00
@swordcoming9527 确实如此。不过编译器应该要检查 in 0..1 才对,甚至编译后应该直接展开这种硬编码。
buxiuxi
2023-10-04 13:16:35 +08:00
为啥这里 release 函数会释放 dog?
owtotwo
2023-10-08 15:00:36 +08:00
直接简化问题,此问题本质上,与下面代码无法编译通过的原因是一致的:


**即在编译期 Borrow Checker 是不进行具体值计算的。**

如上面的 if 分支,在生命周期检查期间,并不知道 1 == 0 总为 false 且永不执行此代码块。故而在使用 name 时,无法得知所有权是否已被转移。
甚至将 1 == 0 直接换成 false ,此期间 Borrow Checker 依旧不知道 false 总不执行代码块,即认为是**有可能**调用 drop(name)触发所有权转移的。
owtotwo
2023-10-08 15:30:23 +08:00
回到原问题,person.dog 的各种赋值写法:


0. 对于 Code 0 ,即上一条回复提到的本质问题,此处尽管写着 if true ,然而 Borrow Checker 并不默认此代码总是执行,而是认为可能执行也可能不执行。(当然,当 build release 编译优化阶段时,if true 就会被优化掉了,而所有权检查阶段并不会,或许是太耗时了)

1. 对于 Code 1 ,与 Code 0 本质一样,Borrow Checker 会认为此 for 语句可能不会执行,所以“Rust 的检测器似乎没有认真评估 for 中的条件”确实是对的,并不会在此阶段评估。

2. 对于 Code 2 ,因为有 else 分支且调用 unreachable!(),所以理论上后续使用 person 时必然已经经过 if 部分的赋值语句(因为 else 部分会 panic ,即不会执行后面代码)
owtotwo
2023-10-08 15:40:44 +08:00
续上

3. 对于 Code 3 ,即楼主的第一次修改尝试,实际上等价于将 loop 和 break 去掉(因为此控制流总是执行),所以必然会执行赋值语句,故而编译通过(Ok)。

4. 对于 Code 4 ,即楼主的第二次修改尝试,依然可以将 loop 和 break 去掉,此时情况等价于 Code 0 ,所以一样是编译不通过(Error)。

5. 对于 Code 5 ,即 @Kaiv2 提到的编译成功的写法,因为 break 进去 if 里了,情况就不一样了。此时控制流的逻辑只有两种情况:情况一,若不进入 if 语句里,则无限循环,那么就不会执行后面的使用 person 及 person.dog 的代码了,就没问题;情况二,进入 if 语句,则成功执行赋值,且最后必定 break 出去,执行后面的代码,依然没问题。所以这种不涉及对具体求值有依赖的控制流是能编译通过的。
owtotwo
2023-10-08 15:52:55 +08:00
综上

6. 楼主的代码符合 Rust 的所有权规则吗?符合的,因为问题并不在所有权转移上,而是在编译期所有权检查时是否会进行具体求值的判断上。

7. @DianQK 所以**目前**而言不是 bug ,或许以后编译器更聪明效率更高了就支持此优化了。

8. @binhb @qdwang @rrfeng 的说法是对的。

9. 为啥这里 release 函数会释放 dog ?因为 fn release(self)的参数是 self ,跟 std::mem::drop()一样,调用时会获取其所有权,并在此函数结束后 drop 掉。
owtotwo
2023-10-08 15:54:39 +08:00
FIX: @buxiuxi 第 9 项忘 at 了
DianQK
2023-10-08 17:24:22 +08:00
kerwincsc
2023-10-09 13:14:23 +08:00
@buxiuxi 因为 release 方法签名是 self
PTLin
2023-10-09 13:49:02 +08:00
```
loop {
if true {
person.dog = Dog {
name: String::from("Hogge"),
};
break;
}
}
```
你这把 break 写在 if 里面不就可以了吗,这种 edge case 也算是满脑子都是的东西?
owtotwo
2023-10-10 15:19:35 +08:00
@DianQK #17
或许有些区别
简单点概况,NLL(Non-Lexical Lifetimes)的迭代进化(即下一代的 Polonius)应该依然并不能解决此问题。

因为楼主这问题本质上是 rustc 编译流程的限制。如图: https://blog.rust-lang.org/images/2016-04-MIR/flow.svg
根据**目前**的编译流程,Borrow Checking 发生在 MIR 阶段,此刻的 CFG(Control-Flow Graph)仅将`if true {}`识别为`if some_cond {}`。
故而`if true { <code> }`无法等价于`loop { <code>; break }`或`{ <code> }`,因为它们的 CFG 是不一致的。(参考引用 数据流分析中的 CFG )
最后将类似`if false {}`这样的死代码消除的优化行为,是在 LLVM 的 codegen 优化阶段进行的,所以此前 borrowck 并不认识"true"或"false"。

在 Rust 2018(Rust 1.31)引入 NLL 后,生命周期的推断精度更高了,而下一代的 Polonius 会支持更复杂的控制流(Control Flow)。但是如上所述的原由,依然不能在此阶段进行条件求值,所以问题依在。

* NLL: https://blog.rust-lang.org/2018/12/06/Rust-1.31-and-rust-2018.html#non-lexical-lifetimes
* 编译流程中的 MIR: https://blog.rust-lang.org/2016/04/19/MIR.html
* 数据流分析中的 CFG: https://github.com/rust-lang/rustc-dev-guide/blob/master/src/appendix/background.md#what-is-a-dataflow-analysis
* rustc 概览(包含各编译阶段): https://rustc-dev-guide.rust-lang.org/overview.html


我希望能由浅入深解释问题,但无法太深。前面的回答并无涉及到更多的编译器部分的具体内容,是因太冗长容易导致阅读阻力大,很少人愿意认真看(完)。(但即使现在这长度,似乎大家也习惯 tl;dr 了)

希望我有解释清楚。 : )

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/978382

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX