繁体   English   中英

为什么Rust不明白不再借用一个reference?

[英]Why doesn't Rust understand that a reference is no longer borrowed?

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

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

这是显示此内容的代码:

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

预期的 output 应该是:

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的生命周期分离。

唉。 当您将内部块放入单独的 function 时,问题又回来了。 所以这只是 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的生命周期,使其变得更大,即使这意味着使用 unsafe。

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

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

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

在 Rust 中,您必须构建代码,以便编译器可以轻松检查它。 这意味着,例如,您必须在外部 scope 中声明inner ,或者不使用引用,而是使用Rc之类的类型,它将编译时检查替换为在库中执行的运行时检查。

如果你真的想使用不安全的代码,你可以这样做,但强烈建议不要这样做。 由你手动检查代码是否有未定义的行为,这很难做到,因为目前还没有关于什么是未定义行为的完整描述。 来自Rust 参考资料

警告:以下列表并不详尽。 对于不安全代码中允许和不允许的内容,Rust 的语义没有正式的 model,因此可能会有更多的行为被认为是不安全的。 下面的列表正是我们确定的未定义行为。 请在编写不安全代码之前阅读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);
    }
}

您似乎错过了引用的生命周期是它们的一部分type ,而不是一些动态跟踪的元数据。 因此,在原始代码中,与x关联的生命周期必须在嵌套块的末尾停止,因为与其关联的生命周期必须与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); } }

这让我相信存在编译器错误。 因为现在编译器发现 inner 的生命周期与 x 的生命周期分离。

唉。 当您将内部块放入单独的 function 时,问题又回来了。 所以这只是 Rust 编译器有一些优化代码路径的情况,它正在捕捉极端情况。

推理为什么有些例子有效而有些无效的关键知识主要是由于借用检查器不在当前 function 主体之外进行推理的原则。

这可以从两个方面看:

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

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

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

     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 的inner_func之外,因此任何局部变量的生命周期都将更短并且不兼容。 在目前的形式下,这绝对是自println! 可能会恐慌,调用者可能会发现并最终得到一个悬垂的引用。 同样,借用检查器不会查看main正在做什么来推理本地代码。

    它的 rest 失败与使用tmp变量的其他示例类似。 借用检查器并不认为这会恢复原来的生命周期。 如果您愿意,可以称之为错失良机,但编译器没有理由以特殊方式处理这一系列更改。 如果您想缩短在嵌套 scope 中使用的生命周期,您可以简单地重新借用。

所以总而言之,这不是编译器错误,它只是借用检查器的实现方式和设计工作原理。 事实上,工作示例是丑小鸭,借用检查器只允许它,因为它知道x的整个生命周期和交互,因此可以挥手去掉我在答案开头写的内容。

我究竟做错了什么?

如果您在实际用例中有此类代码,则没有理由重用x ,只需在嵌套的 scope 中创建一个新引用即可:

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
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM