刚随手写了一个示例,尝试探测编译器检测机制。以下代码在 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 必然是所有权检查器。
1
DianQK 2023-10-01 06:06:54 +08:00 via Android
(似乎是 bug ,感觉可以提个 issue
|
2
binhb 2023-10-01 06:48:40 +08:00
因为是静态所有权检查,条件不确定,可能不被执行
|
3
qdwang 2023-10-01 09:02:57 +08:00 via iPad
rust 编译过程中,除非是特殊指定在编译器运算的代码(比如 const fn ),其他代码都是不会运算的,只能根据一些特殊情况做一些处理。比如 loop 内非条件语句下认定为会执行到。for 里不一定必然执行。还有例如有些循环情况算必然执行多次,这样就不可以用 FnOnce 这样。
|
4
Leviathann 2023-10-01 11:36:15 +08:00
所有权检查实际上是一种证明
你要做的是向编译器提供证据 |
5
rrfeng 2023-10-01 12:06:06 +08:00 via Android
这跟不定长数组越界访问一样啊,编译器不会检查条件的。
|
6
lance6716 2023-10-01 19:28:31 +08:00 via Android
只是有个度不想继续细化了,毕竟停机问题
|
7
Kaiv2 2023-10-02 09:48:45 +08:00
loop {
if true { person.dog = Dog { name: String::from("Hogge"), }; break; } } 编译成功 |
9
swordcoming9527 2023-10-03 20:00:01 +08:00
硬要解决这个问题只能等待以后编译器在各种 edge case 优化的更好,但实践中可能没啥意义。
这个问题,是 People 里面的 dog 可能被 Move ,dog 被 Move 时,People 其实是处于一种“不可用”的状态,在 dog 被填充回来之前,对 People 的其他操作都是不允许的。如果在真实的实践中,只需要提前设计好 dog 被 Move 的状态,典型的就是 dog: Option<Dog>。 |
10
caobug OP @swordcoming9527 确实如此。不过编译器应该要检查 in 0..1 才对,甚至编译后应该直接展开这种硬编码。
|
11
buxiuxi 2023-10-04 13:16:35 +08:00
为啥这里 release 函数会释放 dog?
|
12
owtotwo 2023-10-08 15:00:36 +08:00
|
13
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 ,即不会执行后面代码) |
14
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 出去,执行后面的代码,依然没问题。所以这种不涉及对具体求值有依赖的控制流是能编译通过的。 |
15
owtotwo 2023-10-08 15:52:55 +08:00 2
|
17
DianQK 2023-10-08 17:24:22 +08:00 via Android
|
19
PTLin 2023-10-09 13:49:02 +08:00
```
loop { if true { person.dog = Dog { name: String::from("Hogge"), }; break; } } ``` 你这把 break 写在 if 里面不就可以了吗,这种 edge case 也算是满脑子都是的东西? |
20
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 了) 希望我有解释清楚。 : ) |
21
DianQK 2023-10-10 22:29:39 +08:00 via Android
@owtotwo #20
除了 RFC 和具体代码我还没看到,剩下的我应该基本了解。如果先做一些常量传播,再做借用检查就可以了(但是常量传播可能也需要先有一个合格的借用检查?) |
23
owtotwo 2023-10-11 15:07:30 +08:00 1
@DianQK #21
嗯呐 这个确实就是关键所在 若我没记错的话 似乎 rustc 的编译流程会有两次的常量折叠/传播 一次是在前端的 MIR 中 另一次是在后端如 LLVM 中 (好像是前端优化一次能降低给后端的 IR 代码复杂度) MIR 中支持常量传播应该是比较早前的事了(或许有相关公告) 似乎是支持控制流的(代码可能在 mir 部分的 const_prop.rs ?文件名应该长得差不多) 但并不知道是否支持“消掉 if const_expr”的行为 (我不知道这种分支优化的术语应该是什么 死代码消除 Dead Code Elim ?或者是叫 Sparse Cond Const Prop ?中文可能是 稀疏条件常量传播 之类的 或许也不准确) 但比较尴尬的是 常量传播是在 MIR 的优化阶段进行的 而 borrowck 是在 mir-opt 之前进行的(如果我没记错的话) 所以正如老哥你所说的 常量传播时应该已经有借用检查了 (以及我感觉理论上应该确实是能在借用检查前算 const 的 就是不知道最终会不会增加 MIR 部分编译的总耗时) 编译流程层面的改动影响对 rustc 而言还是挺大的(如 Polonius 也只是 borrowck 部分的平替) 所以短期内可能不会有相应优化了(个人感觉 不知道目前有没有人提对应的 RFC ) 以上的话并不严谨 我也暂时没能去进行校验 或许会有些错漏或过时(记忆有点旧了) 有条件的朋友或许可以补充下相关链接~ |
24
owtotwo 2023-10-11 15:37:51 +08:00 1
@caobug #22
有讲清楚了就好 能对别人有帮助还是很开心的 : ) Rust 文章的话 不太好写 写起来时很难兼顾到不同熟练度的 Rust 小伙伴(主要还是我自己能力有限) 举个例子 就像一开始其实我想**直接**用 `while true {}`和`loop {}`为什么不一样 来解释这问题的(前者迭代 0+次 后者 1+次) 但是这样会引入题目中没提到的 while 语句 以及它们另外的差异( loop-break <value>能返回值而 while 恒为`()`之类的) 最后还得再迁移到 for 语句来解释 这样就有可能将问题复杂化了 而如果跳过中间例子直接说“const_expr 在 borrowck 阶段不求值” 不熟悉的小伙伴有可能一下子转不过来 且问题涉及 if 语句 所以最后决定用`if false {}`作例子来渐进地解释 比较好理解 Rust 文章同理 讲一个点 从多浅讲到多深 我就有点犯难了 有的知识点实在涉及太多 比如 Pin 感觉没十几页纸实在讲不清楚 一想就头都大了 0.o |