在match表达式中借用可变的self的Rust实现

4

我在 match 表达式中使用 self 时遇到了问题:

fn add_once(&'t mut self, p_name: &'t str) -> Box<Element> {
    match self.get(p_name) {
        Some(x) => Box::new(*x),
        None => self.add(p_name),
    }
}

get()add() 函数的签名如下:

fn get(&self, p_name: &str) -> Option<&Element>
fn add(&'t mut self, p_name: &'t str) -> Box<Element>

编译器拒绝了这段代码:
error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/main.rs:38:21
   |
36 |         match self.get(p_name) {
   |               ---- immutable borrow occurs here
37 |             Some(x) => Box::new(*x),
38 |             None => self.add(p_name),
   |                     ^^^^ mutable borrow occurs here
39 |         }
40 |     }
   |     - immutable borrow ends here

我理解,但我不知道如何重写match表达式。

相关问题中,通过让match返回一个值并调用函数来解决了这个问题。然而,在这里不起作用,因为条件语句的含义不是选择一个值而是有选择地执行一个操作。

下面是完整的代码示例:

struct Element<'e> {
    name: &'e str, 
}

impl<'e> Element<'e> {
    fn new(p_name: &str) -> Element {
        Element { name: p_name }
    }
}

struct Top<'t> {
    list: Vec<Element<'t>>,
}

impl<'t> Top<'t> {
    fn new() -> Top<'t> {
        Top { list: vec![] }
    }

    fn get(&self, p_name: &str) -> Option<&Element> {
        for element in self.list.iter() {
            if element.name == p_name {
                return Some(element);
            }
        }
        None
    }

    fn add(&'t mut self, p_name: &'t str) -> Box<Element> {
        let new_element = Box::new(Element::new(p_name));
        self.list.push(*new_element);
        return new_element;
    }

    fn add_once(&'t mut self, p_name: &'t str) -> Box<Element> {
        match self.get(p_name) {
            Some(x) => Box::new(*x),
            None => self.add(p_name),
        }
    }
}

fn main() {
    let mut t = Top::new();
    let plop1 = t.add_once("plop1");
    let plop2 = t.add_once("plop1");
}

即使没有可变错误,您仍然会遇到类型统一问题。match的两个分支应该返回相同的类型。同样,我很惊讶add编译通过,因为您首先将内容推入向量,然后再返回;但是在我的看法中,将内容推入向量应该使盒子的内容无效(移动)... add不应该返回&Element,而add_once的结果应该是&Element吗? - Matthieu M.
@MatthieuM.,它能够工作是因为ElementCopy,所以*new_element返回元素的副本。 - Vladimir Matveev
@VladimirMatveev:啊,是的,我忘记了Copy还没有选择加入。不过,对我来说,这似乎表明了一个设计缺陷:Top看起来应该是通用的,并且适用于非Copy类型。 - Matthieu M.
我对Rust非常陌生,所以可能存在设计错误。Top确实应该适用于非“Copy”类型。 - Tifauv'
@MatthieuM。最初,我想让addadd_once返回&Element,但无法使其正常工作。 - Tifauv'
1个回答

3

首先,让我们解决设计问题。主要问题是生命周期混淆:

struct Top<'t> {
    list: Vec<Element<'t>>,
}

impl<'t> Top<'t> {
    fn add(&'t mut self, p_name: &'t str) -> Box<Element>;
    fn add_once(&'t mut self, p_name: &'t str) -> Box<Element>;
}

你认为self应该至少与包含它的引用的生存期't相同。但实际上,你需要的是 self 活得比't更短,这样任何&'t都能够在self消失之前继续存在。
如果我们进行改变,就可以轻松地返回对Element的引用:
impl<'t> Top<'t> {
    fn add<'a>(&'a mut self, p_name: &'t str) -> &'a Element<'t>;
    fn add_once<'a>(&'a mut self, p_name: &'t str) -> &'a Element<'t>;
}

请注意,对于 Element 的引用的生命周期 'a 与其包含的引用的生命周期 't 不同(实际上要更短)。
解决了这个问题之后,这些函数应该就可以正常工作了。
fn position(&self, p_name: &str) -> Option<usize> {
    self.list.iter().position(|e| e.name == p_name)
}

fn add<'a>(&'a mut self, p_name: &'t str) -> &'a Element<'t> {
    self.list.push(Element::new(p_name));
    &self.list[self.list.len() - 1]
}

fn add_once<'a>(&'a mut self, p_name: &'t str) -> &'a Element<'t> {
    if let Some(p) = self.position(p_name) {
        return &self.list[p];
    }
    self.add(p_name)
}

position可以被重复使用来进行get操作:

fn get<'a>(&'a self, p_name: &str) -> Option<&'a Element<'t>> {
    self.position(p_name).map(|pos| &self.list[pos])
}

在理想情况下,我期望以下内容能够正常工作:

fn add_once<'a>(&'a mut self, p_name: &'t str) -> &'a Element<'t> {
    match self.get(p_name) {
        Some(x) => x,
        None => self.add(p_name)
    }
}

然而,我记得有一个讨论,认为借用检查器不够宽松:由self.get引起的借用范围被计算为整个match表达式,即使在None分支中,临时变量也无法访问。

一旦Rust中纳入了“非词法生命周期”,就应该解决这个问题,这是正在进行中的工作。


你在“完美世界”评论中是完全正确的。这不起作用,因为借用是词法的,但它们可能会在未来变得非词法,从而允许这种模式。据我所记,这是在libstd中的映射中引入Entry API的主要原因之一(当然还有减少查找次数的愿望)。 - Vladimir Matveev
@MatthieuM。谢谢,我现在对生命周期有了更深的理解。实际上,addadd_once返回的引用与元素本身具有不同的生命周期。需要一些脑力训练,但看起来很好。然而,编译器在fn<'a>上抱怨,难道它不应该是fn add<'a>吗?还是这是一种新的语法?此外,在v0.12中,if let语法是实验性的。使用match的语法看起来非常干净。 - Tifauv'
@Tifauv':抱歉,fn<'a>的生命周期确实在函数名之后。我同意match语法更清晰,但是正如我在注释中所解释的那样,它目前无法工作。 - Matthieu M.
如果 pushindex 之间存在竞争条件,那不是可能发生吗? 如果 Vec.push 返回插入的元素,将更加简单。 - Tifauv'
@MatthieuM。好的,我忘记了&mut T。Rust有一些独特的概念需要一些时间来正确理解。我尝试了你提出的修改,但它仍然无法编译。首先,在add中,Vec.index获取索引的引用,而不是索引本身,因此我首先创建let idx = self.list.len() - 1;,然后调用self.list.index(&idx)。然而,编译器仍然拒绝使用消息“由于冲突要求,无法推断自动引用的适当生命周期”的if let Some(x) = self.get(p_name) {。也许它可以在夜间构建中工作,而不是0.12? - Tifauv'
显示剩余4条评论

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