【问题标题】:Is this use of unsafe trivially safe?这种使用不安全的琐碎安全吗?
【发布时间】:2021-12-09 07:03:53
【问题描述】:

我遇到了一个 Rust 借用检查器错误,我认为这是 non-lexical lifetimes 当前实现的限制。我想写的代码是这样的:

struct Thing {
    value: i32
}

impl Thing {
    fn value(&self) -> &i32 {
        &self.value
    }
    fn increment(&mut self) {
        self.value += 1;
    }
}

/// Increments the value of `thing` if it is odd, and returns a reference to the value.
fn increment_if_odd(thing: &mut Thing) -> &i32 {
    let ref_to_value = thing.value();
    if (*ref_to_value % 2) == 0 {
        return ref_to_value;
    }
    thing.increment();  // fails to compile because the immutable borrow `ref_to_value` is still alive
    thing.value()
}

Rust Playground.

第一个问题:我是否认为这段代码是 100% 安全的,而借用检查器过于保守?返回ref_to_value 的分支不会改变thing,因此保证引用是有效的,而另一个分支根本不使用ref_to_value。 (我知道如果我用return thing.value(); 替换return ref_to_value; 它将编译,但在我的实际代码中value 方法很昂贵。)

看来我可以通过指针“清洗”引用来解决这个问题:

if (*ref_to_value % 2) == 0 {
    return unsafe {
        &*(ref_to_value as *const i32)
    }
}

第二个问题:这很安全吗?我以前从来没有用过 unsafe 所以我很紧张。

我猜第三个问题:有没有办法用安全的 Rust 重写它?约束是 value 只能在非变异路径上调用一次。

【问题讨论】:

  • 我想说编译器在这里有问题。因为通过强制执行额外的 escope,应该删除引用。但我不是。 play.rust-lang.org/…
  • 即使显式删除也不起作用:play.rust-lang.org/…
  • 看看the RFC for non-lexical lifetimes。第三个例子和你的差不多,第四个的变通方法可以修改。
  • 这不是编译器错误,而是已知限制。我知道我们有这个问题的多个重复项,但我现在无法找到一个。我会继续挖掘。
  • 我想用 Polonius 补充一下,有问题的代码已经编译了 - play.rust-lang.org/…

标签: rust unsafe borrow-checker


【解决方案1】:

编译器不允许您的代码的原因是因为 ref_to_value 的生命周期必须至少与 increment_if_odd 的生命周期一样长才能返回。

如果您在省略的生命周期中添加回来,则 ref_to_value 必须具有生命周期 'a.我的理解是编译器不能改变引用的生命周期。编写安全 rust 来解决此问题的一种方法是使 ref_to_value 可变,并修改 Thing::increment。

您的不安全代码所做的是允许编译器为 ref_to_value 提供更短的生命周期,以及通过强制转换指针创建的新引用生命周期 'a.我认为你的不安全代码是“安全的”,因为没有任何 rust 的借用规则被破坏,而且我们知道新的引用不会超过数据。

struct Thing {
    value: i32
}

impl Thing {
    fn value(&self) -> &i32 {
        &self.value
    }
    fn mut_value(&mut self) -> &mut i32{
        &mut self.value
    }
    fn increment(val: &mut i32) {
        *val += 1;
    }
}

/// Increments the value of `thing` if it is odd, and returns a reference to the value.
fn increment_if_odd<'a>(thing: &'a mut Thing) -> &'a i32 {
    
    let ref_to_value : &'a mut i32 = thing.mut_value();
    if (*ref_to_value % 2) != 0 {
        Thing::increment(ref_to_value);
    }
    ref_to_value
}

【讨论】:

    【解决方案2】:

    可能是安全的,没有仔细观察它。另一方面,您可以将函数重写为:

    fn increment_if_odd(thing: &mut Thing) -> &i32 {
        if (*thing.value() % 2) != 0 {
            thing.increment();
        }
        thing.value()
    }
    

    【讨论】:

    • 谢谢!这会调用value 两次,但我想避免这种情况。在我的代码中,value 很昂贵。
    • @JamesFennell 啊,好吧。问题是 increment 中的 &amp;mut self 对编译器意味着 self 的任何数据/内存都可以更改并使 value() 返回的引用后面的数据无效,如果您知道不会使其无效,则 unsafe 很好但强烈建议不要这样做,因为如果您将来改变行为,它很容易破坏。
    • 如果increment 将不同的数据更改为value() 返回的数据,那么我建议将Thing 结构拆分为两个单独的结构。然后你可以改变一侧而不会使另一侧的不可变引用无效。否则可能创建一个value_mut(),然后让increment 采用&amp;mut i32 而不是&amp;mut self...?可能需要更多上下文来提供更好的建议:)
    • 关键点是有两个分支,并且不会沿着返回引用的分支发生突变,所以代码没问题。借用检查器过于保守。
    • 我不知道那个语言,但为什么不知道x &amp; 1 == 1
    猜你喜欢
    • 1970-01-01
    • 2011-06-19
    • 2012-09-01
    • 2011-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多