RUST 所有权移动问题

273 天前
 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 必然是所有权检查器。

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


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

如上面的 if 分支,在生命周期检查期间,并不知道 1 == 0 总为 false 且永不执行此代码块。故而在使用 name 时,无法得知所有权是否已被转移。
甚至将 1 == 0 直接换成 false ,此期间 Borrow Checker 依旧不知道 false 总不执行代码块,即认为是**有可能**调用 drop(name)触发所有权转移的。
owtotwo
265 天前
回到原问题,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
265 天前
续上

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
265 天前
综上

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

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

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

9. 为啥这里 release 函数会释放 dog ?因为 fn release(self)的参数是 self ,跟 std::mem::drop()一样,调用时会获取其所有权,并在此函数结束后 drop 掉。
owtotwo
265 天前
FIX: @buxiuxi 第 9 项忘 at 了
DianQK
265 天前
kerwincsc
265 天前
@buxiuxi 因为 release 方法签名是 self
PTLin
264 天前
```
loop {
if true {
person.dog = Dog {
name: String::from("Hogge"),
};
break;
}
}
```
你这把 break 写在 if 里面不就可以了吗,这种 edge case 也算是满脑子都是的东西?
owtotwo
263 天前
@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