为什么 Rust 不明白不再借用引用?



在Rust中,当我借用一个值时,编译器会注意到,但当我替换它时,编译器不会注意到,并发出E0597错误。

给定一个包含引用x的可变变量。当我用对局部变量的引用替换它的内容时,在局部变量超出范围之前,我将其替换回原来的内容。

这里有一个代码显示了这一点:

struct X {payload : i32}
fn main() {
let pl = X{payload : 44};
{
let mut x = &pl;
{
let inner = X{payload : 30};
let tmp = std::mem::replace(&mut x, &inner);
println! ("data ={:?}", x.payload);
let _f = std::mem::replace(&mut x, &tmp);
}
println! ("data ={:?}", x.payload);
}
}

错误为:

error[E0597]: `inner` does not live long enough
--> src/main.rs:9:49
|
9  |             let tmp = std::mem::replace(&mut x, &inner);
|                                                 ^^^^^^ borrowed value does not live long enough
...
12 |         }
|         - `inner` dropped here while still borrowed
13 |         println! ("data ={:?}", x.payload);
|                                 --------- borrow later used here
For more information about this error, try `rustc --explain E0597`.

当我将inner的引用分配给x时,编译器会注意到这一点,但忽略了这样一个事实,即当inner仍然存在时,我会再次将此引用替换为对pl的原始引用。

预期输出应为:

data =30
data =44

我做错了什么?

我已经解决了它。不幸的是,这是一个编译器错误或限制。

语义上等效的代码,代码的信用属于发布了部分答案的人,但随后将其删除。

// This one will yields error E0597
struct X {payload : i32}
fn main() {
let pl = X{payload : 44};
{
let mut x = &pl;
println! ("data ={:?}", x.payload);
{
let inner = X{payload : 30};
let tmp : &X = x;
x = &inner;
println! ("data ={:?}", x.payload);
x = tmp;
}
println! ("data ={:?}", x.payload);
}
}

此代码将产生相同的错误。

然而,一个小的调整将使它编译时没有错误或警告。

// Compiles without errors/warnings.
struct X {payload : i32}
fn main() {
let pl = X{payload : 44};
{
let mut x = &pl;
println! ("data ={:?}", x.payload);
{
let inner = X{payload : 30};
x = &inner;
println! ("data ={:?}", x.payload);
x = &pl;
}
println! ("data ={:?}", x.payload);
}
}

这让我相信存在编译器错误。因为现在编译器发现inner的生存期与x的生存期解耦。

唉。当您将内部块放入一个单独的函数中时,问题又回来了。所以这只是一个例子,Rust编译器有一些优化代码路径,它捕捉到了角落的情况。

// Yields error E0597 again.
struct X {payload : i32}
fn inner_func(x : &mut &X) {
let inner = X{payload : 30};
let tmp : &X = *x;
*x = &inner;
println! ("data ={:?}", (*x).payload);
*x = &tmp;
}
fn main() {
let pl = X{payload : 44};
{
let mut x : &X = &pl;
inner_func(&mut &mut x);
println! ("data ={:?}", x.payload);
}
}

Frederico提供了一种方法,将无尽的寿命硬塞进inner的寿命,使其更大,即使这意味着使用不安全的方法。

// This one compiles without errors/warnings.
struct X {payload : i32}
fn inner_func(x : &mut &X) {
let inner = X{payload : 30};
let tmp : &X = *x;
unsafe {
*x = std::mem::transmute::<_, &'static X>(&inner);
}
println! ("data ={:?}", (*x).payload);
*x = &tmp;
}
fn main() {
let pl = X{payload : 44};
{
let mut x : &X = &pl;
inner_func(&mut &mut x);
println! ("data ={:?}", x.payload);
}
}

您似乎忽略了引用的生存期是其类型的一部分,而不是一些动态跟踪的元数据。因此,在原始代码中,与x相关联的生存期必须停止在嵌套块的末尾,因为与之相关联的生命期必须"0";合并";inner的寿命到此为止。你把它放回原处并不重要,因为这不会改变类型。

现在来解决在自我回答中发现的细微差别:

然而,一个小的调整将使其编译时没有错误或警告。

// Compiles without errors/warnings.
struct X {payload : i32}
fn main() {
let pl = X{payload : 44};
{
let mut x = &pl;
println! ("data ={:?}", x.payload);
{
let inner = X{payload : 30};
x = &inner;
println! ("data ={:?}", x.payload);
x = &pl;
}
println! ("data ={:?}", x.payload);
}
}

这让我相信存在编译器错误。因为现在编译器发现内部的生存期与x的生存期解耦。

唉。当您将内部块放入一个单独的函数中时,问题又回来了。所以这只是一个例子,Rust编译器有一些优化代码路径,它捕捉到了角落的情况。

推理某些示例有效而某些示例无效的关键知识很大程度上是由于借用检查器不会在当前函数体之外推理的原则

这是双向的:

  • 如果使用std::mem::replace:

    借用检查器不知道std::mem::replace做什么。它只看到它是一个接收&mut TT并返回T的函数。因此,您可以看到它将如何拒绝此代码,因为它无法推断x具有其原始值。借用检查器不会查看replace是如何实现的,从而推断本地代码。

  • 如果你把它分成一个单独的函数:

    fn inner_func(x: &mut &X) {
    let inner = X { payload: 30 };
    let tmp: &X = *x;
    *x = &inner;
    println!("data ={:?}", x.payload);
    *x = &tmp;
    }
    

    然后借用检查器发现这显然是错误的:x的构造寿命超出了inner_func的范围,因此任何局部变量的寿命都会更小,并且是不兼容的。在目前的形式中,这肯定是不合理的,因为println!可能会恐慌,调用者可能会抓住它,并最终得到一个悬空引用。同样,借用检查器不查看main正在做什么来推断本地代码。

    它的其余部分与使用tmp变量的其他示例类似地失败。借用检查器不会推断这将恢复原始生存期。如果你愿意,可以称之为错失的机会,但编译器没有理由以特殊的方式处理这一系列更改。如果你想缩短在嵌套范围内使用的寿命,你可以简单地重新调用。

因此,总的来说,这不是一个编译器错误,它只是借用检查器的实现方式和设计工作方式。事实上,工作的例子是丑小鸭,借用检查器只允许它,因为它知道x的整个生命周期和交互,因此可以手动挥手拿走我在答案开头写的内容。

我做错了什么?

如果在实际用例中有这样的代码,就没有理由重用x,只需在嵌套范围中进行新的引用:

struct X { payload: i32 }
fn main() {
let pl = X { payload: 44 };
let x = &pl;
{
let mut x = x; // reborrow so that the new x can have a smaller scope
let inner = X { payload: 30 };
let tmp = std::mem::replace(&mut x, &inner);
println!("data ={:?}", x.payload);
let _f = std::mem::replace(&mut x, &tmp);
}
println!("data ={:?}", x.payload); // uses original x
}

在分析Rust代码时,编译器是保守的,会拒绝无法确定是否存在内存错误的程序。

特别是,当编译器检查代码中的let tmp = ...时,它会注意到您正在将指向短命实例(inner)的引用存储到引用(pl)中,该引用在inner超出范围后使用。编译器在这里停止并报告一个错误,表示您可能创建了一个悬空引用。在恢复原始pl之后,它没有立即考虑到这一点。当前用于分析的生存期抽象不够精确。

在Rust中,你必须构建你的代码结构,这样编译器就可以很容易地检查它。例如,这意味着你必须在外部范围中声明inner,或者你不使用引用,而是使用Rc之类的类型,用库中执行的运行时检查取代编译时检查。

如果真的想要使用不安全的代码,可以按照以下步骤操作,但强烈建议不要这样做。这取决于您手动检查代码是否没有未定义的行为,这很难做到,因为目前还没有关于什么是未定义行为的完整描述。来自Rust参考:

警告:以下列表并非详尽无遗。对于不安全代码中允许和不允许的内容,没有Rust语义的正式模型,因此可能会有更多的行为被认为是不安全的。下面的列表正是我们确信的未定义行为。在编写不安全代码之前,请阅读Rustonomicon。

struct X {payload : i32}
fn main() {
let pl = X{payload : 44};
{
let mut x = &pl;
// SAFETY: ...(motivate why the following is ok)
unsafe {
let inner = X{payload : 30};
x = std::mem::transmute::<_, &'static X>(&inner);
println! ("data ={:?}", x.payload);
x = &pl;
}
println! ("data ={:?}", x.payload);
}
}

最新更新