这个不安全的用法是否可以轻松安全?

15

我遇到了一个Rust借用检查器错误,我相信这是非词法生命周期当前实现的限制。我想要编写的代码大致如下:

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。看起来我可以通过“洗涤”指针来解决这个问题:

if (*ref_to_value % 2) == 0 {
    return unsafe {
        &*(ref_to_value as *const i32)
    }
}
第二个问题:这个是不是非常安全?我以前从来没有使用过unsafe,所以我很紧张。 我想第三个问题是:有没有一种安全的Rust重写方法?限制是在不可变路径上只调用一次'value'。
我猜还有第三个问题:是否有一种安全的Rust重写方式?限制是在不可变路径上仅调用一次'value'。

1
我会说编译器在这里有问题。因为通过强制额外的作用域,引用应该被删除。但它没有被删除。https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e57622a76132661649f6ddf4991a1077 - Netwave
2
请查看非词法生命周期的RFC。第三个示例与您的类似,第四个示例中的解决方法可以进行调整。 - starblue
8
这并不是编译器的错误,而是已知的限制。我知道我们有几个类似的问题,但我现在找不到一个。我会继续查找。 - Sven Marnach
4
我想指出的是,使用Polonius编译器,这段代码已经可以通过编译 - https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=20905567d6dcec8c495be475b279fa20。 - Cerberus
显示剩余6条评论
2个回答

2
编译器不允许你的代码的原因是,为了返回它,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
}

-4
可能是安全的,但我还没有仔细查看过。另一方面,你可以将函数重写为:
fn increment_if_odd(thing: &mut Thing) -> &i32 {
    if (*thing.value() % 2) != 0 {
        thing.increment();
    }
    thing.value()
}

谢谢!不过这会调用 value 两次,我想避免这种情况。在我的代码中,value 是昂贵的。 - James Fennell
@JamesFennell 嗯,好的。问题在于 increment 中的 &mut self 表示对编译器来说,self 的任何数据/内存都可能发生变化,并使 value() 返回的引用后面的数据无效。如果您知道不会使其无效,则可以使用unsafe关键字,但是这种做法强烈不建议,因为这将使未来更改行为变得容易导致代码崩溃。 - Ben
如果 increment 改变与 value() 返回的不同的数据,那我建议将 Thing 结构拆分为两个不同的结构。这样你可以更改一个部分而不会使得另一个部分的不可变引用无效。否则,也许可以创建一个 value_mut(),然后让 increment 接收 &mut i32 而不是 &mut self...?可能需要更多上下文才能提出更好的建议 :) - Ben
关键点在于有两个分支,而且变异不能沿着返回引用的分支发生,所以代码是正确的。这是借用检查器过于保守了。 - James Fennell
我不知道那种语言,但为什么不用 x & 1 == 1 呢? - Patrick Beynio

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