【问题标题】:Borrowing mutable references in closure to sort vector在闭包中借用可变引用对向量进行排序
【发布时间】:2021-05-01 18:20:50
【问题描述】:

尽管我了解 Rust 的内存安全方法、所有权概念,并且对一般引用没有任何问题,但我仍在努力弄清楚 Rust 想要我如何解决一个看似微不足道的问题。

此代码是一个最小示例。

一个结构:

pub struct S {
    value: Option<i8>,
}

使用第一次计算一个值,然后返回存储的值的 getter:

use rand::Rng;
impl S {
    fn get_value(&mut self) -> i8 {
        if let Some(v) = self.value {
            return v;
        }

        let v = rand::thread_rng().gen::<i8>();
        self.value = Some(v);
        v
    }
}

当然,get_value() 需要一个可变引用 &amp;mut self,因为它修改了 self

当我想根据get_value() 的结果对S 的引用向量进行排序时,这种可变性是我苦苦挣扎的根源。因为要按get_value()排序,所以sort_by使用的比较函数需要可变引用。

我的第一次尝试:

fn main() {
    let mut a = S {value: None};
    let mut b = S {value: None};
    let mut c = S {value: None};

    let mut v = vec![&mut a, &mut b, &mut c];

    v.sort_by( |a, b| a.get_value().cmp(&b.get_value()) );
}

投掷:

error[E0596]: cannot borrow `**a` as mutable, as it is behind a `&` reference
  --> src/main.rs:27:20
   |
27 |     v.sort_by( |a, b| a.get_v().cmp(&b.get_v()) );
   |                 -     ^ `a` is a `&` reference, so the data it refers to cannot be borrowed as mutable
   |                 |
   |                 help: consider changing this to be a mutable reference: `&mut &mut S`

我最初的想法是,首先拥有一个可变引用向量将允许比较函数使用可变引用。然而,我的 getter 函数借用了可变引用的可变引用,这就是错误显示 cannot borrow '**a' 的原因。

帮助建议更改|a,b|,使其成为&amp;mut &amp;mut S 引用。

&amp;mut &amp;mut&amp;&amp;mut 一样吗? 这是否意味着我应该将其更改为|mut a:&amp;&amp;mut S, mut b:&amp;&amp;mut S|

【问题讨论】:

  • &amp;&amp;mut x 不能被变异,就好像它可以一样,您可以拆分可变引用,从而导致竞争条件。可悲的是,排序仅在 Rust 中是不可变的引用,因为如果值在排序期间发生变化,它可能会扰乱整个过程。您可能需要考虑使用 std::cell::Cell 之类的东西来实现内部可变性,因为您的案例是完全安全的(一旦获得一个值,它就永远不会改变)。
  • 如果您的值在被访问之前未创建,然后没有更改,请考虑使用OnceCell

标签: rust reference closures borrow-checker borrowing


【解决方案1】:

假设您的示例已简化,并且在您的真实代码中实际上需要可变性,这可能是内部可变性的一个很好的例子,例如Cell:

use std::cell::Cell;

pub struct S {
    value: Cell<Option<i8>>,
}

这意味着您可以更新您的 getter 方法,使其不需要可变的自引用:

impl S {
    fn get_value(&self) -> i8 {
        if let Some(v) = self.value.get() {
            return v;
        }

        let v = rand::thread_rng().gen::<i8>();
        self.value.set(Some(v));
        v
    }
}

您也可以摆脱其他一些可变引用:

fn main() {
    let a = S { value: Cell::default() };
    let b = S { value: Cell::default() };
    let c = S { value: Cell::default() };

    let mut v = vec![&a, &b, &c];

    v.sort_by(|a, b| a.get_value().cmp(&b.get_value()));
}

【讨论】:

  • 我现在明白了,谢谢你的例子。看来我停止阅读有点太早了。包装器在实践中似乎比我准备接受的更重要。
【解决方案2】:

sort_by(&lt;closure&gt;) 中的闭包具有签名FnMut(&amp;T, &amp;T) -&gt; Ordering,因为项目在排序期间必须是不可变的。如果可以在排序期间改变项目,那么您将如何知道集合何时实际排序?无法验证排序的正确性!

此外,sort_by 是基于比较的排序,这意味着它将至少访问集合中的每个项目一次,因此 每个 项目将在整个排序过程中被初始化,所以你不会在这种情况下,通过使用惰性初始化 getter 确实可以获得任何好处。

如果出于某种原因您想坚持使用惰性初始化 getter,那么我的建议是在对所有项目进行排序之前对其进行初始化:

use rand::Rng;

pub struct S {
    value: Option<i8>,
}

impl S {
    fn get_value(&mut self) -> i8 {
        if let Some(v) = self.value {
            return v;
        }

        let v = rand::thread_rng().gen::<i8>();
        self.value = Some(v);
        v
    }
}

fn main() {
    let mut a = S { value: None };
    let mut b = S { value: None };
    let mut c = S { value: None };

    let mut v = vec![&mut a, &mut b, &mut c];

    // initialize all items, it's what would
    // happen in the sort below if it allowed
    // mutating items mid-sort anyway
    v.iter_mut().for_each(|i| {
        i.get_value();
    });

    // sort initialized items without mutating them
    v.sort_by(|a, b| a.value.cmp(&b.value));
}

playground

【讨论】:

  • 你说得对,在这种情况下我不会通过延迟初始化获得任何收益。我仍然不知道该怎么做这件事让我很困扰。
猜你喜欢
  • 1970-01-01
  • 2015-12-31
  • 2021-12-19
  • 2021-05-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-01
  • 1970-01-01
相关资源
最近更新 更多