为什么Rust不理解引用已经不再被借用了?

4
在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

我做错了什么?


1
请查看此playground进行更深入的分析,尽管我无法弄清楚。 - cafce25
3个回答

1
我已经解决了这个问题。不幸的是,这是编译器的一个错误或限制。
语义上等价的代码,感谢某个发布了部分答案但随后删除了它的人提供的代码。
// 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提供的方法,通过使用unsafe技术,可以将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);
    }
}

0

看起来你忽略了引用的生命周期是它们类型的一部分,而不是一些动态跟踪的元数据。因此,在原始代码中,与 x 相关联的生命周期必须在嵌套块的末尾停止,因为与之相关联的生命周期必须“合并”到 inner 的生命周期中,后者在那里结束。将其放回去并没有改变类型,所以这样做没有任何影响。

现在来处理在self-answer中发现的细微差别:

However a small tweak will make it compile without errors or warnings.

// 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);
    }
}

This makes me believe that there is a compiler bug. Because now the compiler catches that the lifetime of inner decouples from the lifetime of x.

ALAS. When you put the inner block into a separated function the problem comes back. So it was just a case that the Rust compiler has some optimization code-path that was catching the corner case.

理解为什么某些示例有效而其他示例无效的关键知识主要归因于原则:借用检查器不会超出当前函数体进行推理。

这可以从两个方面看出:

  • 如果你使用 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! 可能会 panic,而调用者可能会捕获它并最终得到一个悬空引用。同样,借用检查器不会查看 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
}

0

在分析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);
    }
}

Rc使用堆,所以它并不相同。如果编译器过于保守而且没有充分的理由,那么你能提供一个不安全的方法来实现相同的结果,仅使用栈分配吗? 在“inner”消失时,生命周期违规应该在块关闭时检查,而不是在它赋值时。 - E. Timotei
1
你使用transmute的想法非常好,但你举的例子即使没有它也会意外地起作用,请看我的完整答案。 - E. Timotei

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接