【问题标题】:How can you use an immutable Option by reference that contains a mutable reference?如何通过引用使用包含可变引用的不可变选项?
【发布时间】:2019-07-19 22:44:26
【问题描述】:

这是Thing

struct Thing(i32);

impl Thing {
    pub fn increment_self(&mut self) {
        self.0 += 1;
        println!("incremented: {}", self.0);
    }
}

下面的函数会尝试改变 Thing 并返回真或假,具体取决于 Thing 是否可用:

fn try_increment(handle: Option<&mut Thing>) -> bool {
    if let Some(t) = handle {
        t.increment_self();
        true
    } else {
        println!("warning: increment failed");
        false
    }
}

这是一个使用示例:

fn main() {
    try_increment(None);

    let mut thing = Thing(0);
    try_increment(Some(&mut thing));
    try_increment(Some(&mut thing));

    try_increment(None);
}

如上所述,it works just fine (link to Rust playground)。下面的输出:

warning: increment failed
incremented: 1
incremented: 2
warning: increment failed

当我想编写一个对Thing 进行两次变异的函数时,问题就出现了。例如,以下内容不起作用:

fn try_increment_twice(handle: Option<&mut Thing>) {
    try_increment(handle);
    try_increment(handle);
}

fn main() {
    try_increment_twice(None);

    let mut thing = Thing(0);
    try_increment_twice(Some(&mut thing));

    try_increment_twice(None);
}

这个错误很有意义。第一次调用try_increment(handle) 将拥有handle 的所有权,因此第二次调用是非法的。通常情况下,Rust 编译器会产生一个合理的错误消息:

   |
24 |     try_increment(handle);
   |                   ------ value moved here
25 |     try_increment(handle);
   |                   ^^^^^^ value used here after move
   |

为了解决这个问题,我认为通过引用传递handle 是有意义的。请注意,它应该是一个 不可变 引用,因为我不希望 try_increment 能够更改 handle 本身(例如,将 None 分配给它)只是能够根据它的值调用突变。

我的问题是我不知道该怎么做。

这里is the closest working version that I could get

struct Thing(i32);

impl Thing {
    pub fn increment_self(&mut self) {
        self.0 += 1;
        println!("incremented: {}", self.0);
    }
}

fn try_increment(handle: &mut Option<&mut Thing>) -> bool {
    // PROBLEM: this line is allowed!
    // (*handle) = None;

    if let Some(ref mut t) = handle {
        t.increment_self();
        true
    } else {
        println!("warning: increment failed");
        false
    }
}

fn try_increment_twice(mut handle: Option<&mut Thing>) {
    try_increment(&mut handle);
    try_increment(&mut handle);
}

fn main() {
    try_increment_twice(None);

    let mut thing = Thing(0);
    try_increment_twice(Some(&mut thing));

    try_increment_twice(None);
}

此代码按预期运行,但 Option 现在由 可变 引用传递,这不是我想要的:

  • 我可以通过重新分配None 来改变Option,从而破坏所有后续的突变。 (例如取消注释第 12 行 ((*handle) = None;)。)
  • 一团糟:有很多无关的&amp;mut 在撒谎。
  • 真是一团糟:天知道为什么我必须在 if let 语句中使用 ref mut,而惯例是在其他任何地方使用 &amp;mut
  • 它违背了在编译器中设置复杂的借用检查和可变性检查规则的目的。

有什么方法可以真正实现我想要的:通过引用传递一个不可变的Option,并且实际上能够使用它的内容?

【问题讨论】:

  • "你如何使用一个不可变的 Option,通过引用,它包含一个可变引用?"你不能像编译器解释的那样“t 是一个&amp; 引用,所以它引用的数据不能被借用为可变的”
  • @Stargateur - .Line 12 是 (*handle) = None;
  • @Stargateur - 不过,我不会改变选项,因为它包含一个引用,我没有更改引用。在我的突变之后,引用仍然指向同一件事。我正在改变那东西内部的一些东西。
  • @Stargateur - 此外,使用 RefCell 无法实现我想要的模式,因为我可以更改 RefCell 以完全包含不同的 Thing - 与重新分配 @ 基本相同987654355@ 无。我可以在内部重写Thing 以使用RefCell,但这意味着我必须重组我的整个库并在任何地方产生运行时借用检查的成本。

标签: reference rust immutability mutability


【解决方案1】:

您无法从不可变引用中提取可变引用,即使是对其内部的引用。这就是重点!不可变引用的多个别名是允许的,因此,如果 Rust 允许您这样做,您可能会遇到两段代码能够同时改变相同数据的情况。

Rust 为interior mutability 提供了几个逃生舱口,例如RefCell

use std::cell::RefCell;

fn try_increment(handle: &Option<RefCell<Thing>>) -> bool {
    if let Some(t) = handle {
        t.borrow_mut().increment_self();
        true
    } else {
        println!("warning: increment failed");
        false
    }
}

fn try_increment_twice(handle: Option<RefCell<Thing>>) {
    try_increment(&handle);
    try_increment(&handle);
}

fn main() {
    let mut thing = RefCell::new(Thing(0));
    try_increment_twice(Some(thing));
    try_increment_twice(None);
}

【讨论】:

  • RefCell 用于将可变性检查推迟到运行时。我想要在编译时进行可变性检查,应该没有理由不能这样做,因为我不想改变Option&lt;&gt;。我没有尝试更改选项值,因为在我的突变之后,引用本身没有改变,只有被引用对象的内部状态。
  • 此外,使用 RefCell 无法实现我想要的模式,因为我可以将 RefCell 更改为完全包含不同的事物——本质上与将选项重新分配为无相同。我可以在内部重写 Thing 以使用 RefCell,但这意味着我必须重组整个库并在任何地方产生运行时借用检查的成本。
  • @Xharlie 您也可以通过&amp;mut 引用重新分配“完全不同的事物”。
  • 在这种情况下,与使用可变 Option's 相比,使用 RefCell 并没有获得任何好处。
  • @Xharlie 正确。可变选项可能会更好,因为它们准确地编码正在发生的事情并让类型检查器来做这件事。我建议内部可变性只是因为您特别要求具有不可变外部类型的解决方案。
【解决方案2】:

TL;DR:答案是,我不能。

在与@Peter Hall 和@Stargateur 讨论之后,我开始明白为什么我需要在任何地方使用&amp;mut Option&lt;&amp;mut Thing&gt;RefCell&lt;&gt; 也是一个可行的解决方法,但它并不整洁,也没有真正实现我最初寻求实现的模式。

问题是这样的:如果一个人被允许改变一个只有一个不可变Option&lt;&amp;mut T&gt;的引用的对象,那么一个人可以使用这个权力来完全打破借用规则。具体来说,本质上,您可以有许多对同一对象的可变引用,因为您可以有许多这样的不可变引用。

知道只有一个可变Thing(由Option&lt;&gt;拥有)的引用,但是,一旦我开始引用@ 987654327@,编译器不再知道这些数量不多。

模式的最佳版本如下:

fn try_increment(handle: &mut Option<&mut Thing>) -> bool {
    if let Some(ref mut t) = handle {
        t.increment_self();
        true
    }
    else {
        println!("warning: increment failed");
        false
    }
}

fn try_increment_twice(mut handle: Option<&mut Thing>) {
    try_increment(&mut handle);
    try_increment(&mut handle);
}

fn main() {
    try_increment_twice(None);

    let mut thing = Thing(0);
    try_increment_twice(Some(&mut thing));

    try_increment_twice(None);
}

注意事项:

  1. Option&lt;&gt; 拥有对Thing 的唯一现存可变引用
  2. try_increment_twice() 拥有Option&lt;&gt; 的所有权
  3. try_increment() 必须Option&lt;&gt; 当作 &amp;mut,以便编译器知道它在调用期间拥有对 Option&lt;&gt; 的唯一可变引用
  4. 如果编译器知道try_increment() 具有对Option&lt;&gt; 的唯一可变引用,而Option&lt;&gt; 拥有对Thing 的唯一可变引用,则编译器知道借用规则没有被违反。

另一个实验

Option&lt;&gt; 的可变性问题仍然存在,因为可以调用 take() 等。在一个可变的Option&lt;&gt; 上,破坏了后面的一切。

为了实现我想要的模式,我需要一个类似于 Option&lt;&gt; 的东西,但是,即使它是可变,它也不能被改变。像这样的:

struct Handle<'a> {
    value: Option<&'a mut Thing>,
}

impl<'a> Handle<'a> {
    fn new(value: &'a mut Thing) -> Self {
        Self {
            value: Some(value),
        }
    }

    fn empty() -> Self {
        Self {
            value: None,
        }
    }

    fn try_mutate<T, F: Fn(&mut Thing) -> T>(&mut self, mutation: F) -> Option<T> {
        if let Some(ref mut v) = self.value {
            Some(mutation(v))
        }
        else {
            None
        }
    }
}

现在,我想,我可以整天传递&amp;mut Handle,并且知道拥有Handle 的人只能改变其内容,而不是句柄本身。 (See Playground)

不幸的是,即使这样也没有任何好处,因为如果你有一个可变引用,你总是可以用解引用运算符重新分配它:

fn try_increment(handle: &mut Handle) -> bool {
    if let Some(_) = handle.try_mutate(|t| { t.increment_self() }) {
        // This breaks future calls:
        (*handle) = Handle::empty();

        true
    }
    else {
        println!("warning: increment failed");
        false
    }
}

这一切都很好。

底线结论:只需使用&amp;mut Option&lt;&amp;mut T&gt;

【讨论】:

    猜你喜欢
    • 2021-12-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-21
    • 2020-12-08
    相关资源
    最近更新 更多