使用处理动作队列的结构满足借用检查器的要求

Satisfying the borrow checker with a struct that processes a queue of actions

我正在尝试编写一个结构,它在 Vec 中拥有一些数据(或者可能包含可变引用的 Vec - 哪个并不重要),并且可以处理“动作”队列,其中每个动作是某种计算,它改变了这个 Vec 的元素。这是我到目前为止所写内容的一个最小示例:

// some arbitrary data - may be large, so should not be cloned or copied
#[derive(PartialEq)]
struct T(i32, &'static str);

struct S(Vec<T>);
impl S {
    fn get_mut(&mut self, t: &T) -> &mut T {
        self.0.iter_mut().find(|a| *a == t).unwrap()
    }
    fn process_actions(&mut self, queue: ActionQueue) {
        // some arbitrary calculation on the elements of self.0
        for a in queue.actions {
            let t1 = self.get_mut(a.t1);
            t1.0 += a.t2.0;
        }
    }
}

#[derive(Debug)]
enum Error {
    ActionError,
    ActionQueueError,
}

struct Action<'a> {
    s: &'a S,
    t1: &'a T,
    t2: &'a T,
}
impl<'a> Action<'a> {
    pub fn new(s: &'a S, t1: &'a T, t2: &'a T) -> Result<Action<'a>, Error> {
        if s.0.contains(&t1) && s.0.contains(&t2) {
            Ok(Action { s, t1, t2 })
        } else {
            Err(Error::ActionError)
        }
    }
}

struct ActionQueue<'a> {
    s: &'a S,
    actions: Vec<Action<'a>>,
}
impl<'a> ActionQueue<'a> {
    pub fn new(s: &'a S, actions: Vec<Action<'a>>) -> Result<ActionQueue<'a>, Error> {
        if actions.iter().all(|a| std::ptr::eq(a.s, s)) {
            Ok(ActionQueue { s, actions })
        } else {
            Err(Error::ActionQueueError)
        }
    }
}

fn main() -> Result<(), Error> {
    let t1 = T(1, "a");
    let t2 = T(2, "b");
    let mut s = S(vec![t1, t2]);

    let a = Action::new(&s, &t1, &t2)?; // error: borrow of moved value: `t1`
    let q = ActionQueue::new(&s, vec![a])?;
    s.process_actions(q); // cannot borrow `s` as mutable because it is also borrowed as immutable

    Ok(())
}

这有一些问题:

  1. 我无法创建操作,因为 t1 和 t2 已经被移动。
  2. 即使我可以,我也无法处理动作队列,因为 s 已经在动作中被不变地借用了。我希望 Action(和 ActionQueue)包含对 s 的引用的原因是,据我所知,使用类型可以防止创建无效数据,例如引用未包含在 s 中的数据的操作(由 s 处理)。
  3. S 的 get_mut 功能看起来有点怪怪的,好像我不应该有这样的功能。

我明白错误发生的原因及其含义,但我看不出有什么方法可以解决这个问题,因为为了定义任何 Action,我需要参考 s.0 的元素我不允许这样做。所以我的问题是,应该如何重写这段代码才能真正编译?设计是否完全不同并不重要,只要它达到相同的目标(即允许排队等待稍后处理的动作)。

我通过以下方式获得了所需的行为:

  1. 我将 T 中的引用更改为 usize 索引,它们引用与以前相同的数据,但现在仅在执行操作时引用,因此在新的范围内引用。这确实需要稍微多一些考虑,如果您将 T 项目移动到位(在 S 向量中),将会更加昂贵,因为您需要找到项目的新索引 -可能会阻止它(我没有测试过,但不明白为什么除了简单的迭代搜索之外还需要什么)

2.limiting借用的次数。我删除了 Action 中的 S 引用,这使得这成为可能,尽管它牺牲了一些可靠性(稍后会详细介绍)它无论如何都是多余的。

  1. usize 索引搜索解决了get_mut() 的问题,但也意味着现在要使用数据,您必须引用T 的各个字段。 get_mut() 之所以看起来如此 hacky,是因为它本质上是一种超压缩迭代搜索。如果您将 T 移到 S 中,那将非常有帮助,因此您可能希望将其保留在您的源代码中,但在本示例中不需要它。
// some arbitrary data - may be large, so should not be cloned or copied
use std::marker::PhantomData;
struct T(i32, &'static str);
struct S(Vec<T>);
impl S {
    fn process_actions(&mut self, changes: [usize;2]) {
        let mut a = self.0[changes[0]].0;
        a += self.0[changes[1]].0;
        println!("the total is {}",a);
    }
}

#[derive(Debug)]
enum Error {
    ActionQueueError,
}

struct Action<'a> {
    t1: usize,
    t2: usize,
    phantom_lifetime: PhantomData<&'a u8>,
}
impl<'a> Action<'a> {
    pub fn new(ts: [usize;2]) -> Action<'a> {
        Action {
            t1: ts[0],
            t2: ts[1],
            phantom_lifetime: PhantomData,
        }
    }
}
struct ActionQueue<'a> {
    s: S,
    actions: Vec<Action<'a>>,
}
impl<'a> ActionQueue<'a> {
    pub fn new(s: S, actions: Vec<Action<'a>>) -> Result<ActionQueue<'a>, Error> {
        if actions.iter().all(|a| s.0.len() > a.t1 && s.0.len() > a.t2) {
            Ok(ActionQueue { s, actions })
        } else {
            Err(Error::ActionQueueError)
        }
    }
    pub fn process_many_actions(&mut self) {
        for gos in self.actions.iter_mut() {
            self.s.process_actions([gos.t1,gos.t2])
        }
    }
}

fn main() -> Result<(), Error> {
    let t1 = T(1, "a");
    let t2 = T(2, "b");
    let s = S(vec![t1, t2]);
    let a = Action::new([0,1]);
    let mut q = ActionQueue::new(s, vec![a])?;
    q.process_many_actions();
    Ok(())
}

出于安全考虑,您可能注意到我从问题中删除了两项检查。第一个检查是一个相当简单的替换,唯一真正的变化是现在它在移动到 ActionQueue 时进行检查 立即,它检查索引是否会找到 something,而不是它 110% 是该索引中与您放入时相同的元素。第二次检查已过时,因为S 被移动到 ActionQueue 然后改变了一个方法 至 ActionQueue。虽然调试起来可能有点困难,但这至少会使函数调用更简单、更安全。

你把自己搞得一团糟!将来,为了让调试更容易(或者至少让回答者花费更少的时间,并吸引更多的人......这个网站上还有其他人,我认为《?》),记住 rust 借用的规则 如你所写。这不会消除错误,甚至不会消除错误,但应该让您远离曾经遇到的大麻烦。