如何在嵌套循环中访问可变迭代对象

5

时不时地,我在循环中遇到了关于借用(或不借用)可变变量的相同问题,最终我花时间编写了一个最小化示例。因此,代码有点愚蠢,但这是我能想到的突出问题的最短版本:

struct Association {
    used: bool,
    key: usize,
    value: String,
}

impl Association {
    fn new(key: usize, value: &str) -> Self {
        Association{used: false, key: key, value: value.to_string()}
    }
}

fn find_unused<'a>(data: &'a mut Vec<Association>) -> Option<&'a String> {
    for k in 0.. {
        for a in data {
            if a.key == k && !a.used {
                a.used = true;
                return Some(&a.value);
            }
        }
    }
    None
}

fn main() {
    let mut assoc = vec![
        Association::new(7, "Hello"),
        Association::new(9, "World")
    ];

    println!("{}", find_unused(&mut assoc).unwrap());
    println!("{}", find_unused(&mut assoc).unwrap());
}

由于data 被移动了,所以将导致错误。如果我改为借用它,则会失败,因为它已经被借用了。我希望确切地理解发生了什么并如何一般地解决它. 特别的是,即使代码结构非常傻,我也不想更改它。我也不想实现变通方法,因为这只是一个最简示例:请假定循环的嵌套方式是“正确”的,即使在这种情况下它非常愚蠢, 当然它肯定是。
我只想知道如何告诉借用检查器实际上发生的事情是可以接受的。我知道一种方法可以做到这一点:
fn find_unused<'a>(data: &'a mut Vec<Association>) -> Option<&'a String> {
    for k in 0.. {
        for j in 0..data.len() {
            if data[j].key == k && !data[j].used {
                data[j].used = true;
                return Some(&data[j].value);
            }
        }
    }
    None
}

这段代码编译没有错误并且按照预期运行。据我的初步了解,应该有一种使用迭代器而不是索引来表达上述内容的方法,我想知道具体实现方式。


这个回答解决了你的问题吗?从HashMap或Vec返回一个引用会导致借用超出其所在的范围吗? - Stargateur
2
亲爱的@Stargateur,请注意我的免责声明,并假设代码重构不是解决方案,即使它在最小示例中绝对有效。请理解这实际上不是我正在编写的代码,以这种方式进行重构并不总是可行的。我确实尝试了启用夜间通道上的非词法生命周期支持,但至少我也无法使其与该功能启用一起工作。 - Jesko Hüttenhain
你已经在使用NLL了,再读一遍shepmaster的回答,这是当前NLL实现的一个限制。尝试使用-Zpolonius选项。至于我提供的解决方案,只是为了帮助你,还有一种使用迭代器的方法,如果你没有创建一个真正展示你使用情况的[MCVE],那么你不能抱怨。我不是魔术师。 - Stargateur
@JeskoHüttenhain 我不确定是否可以发布答案,但我最近遇到了类似的情况,并偶然发现:https://github.com/rust-lang/rust/issues/51526,它与我的代码相当匹配。看起来有条件地从循环中返回借用是具体问题,它不知道如何结束循环的借用。(我猜索引版本之所以有效是因为数据没有被借用。)但是,最终我不太确定NLL。 - GManNickG
1个回答

0

如果您想要的行为是可能的,您需要:

保持其余代码不变,专注于此部分,但将return注释掉:

fn find_unused<'a>(data: &'a mut Vec<Association>) -> Option<&'a String> {
    for k in 0.. {
        for a in data { // Error - but rustc says what to do here
            if a.key == k && !a.used {
                a.used = true;
                // return Some(&a.value);
            }
        }
    }
    None
}

来自rustc的错误消息说明了该怎么做以及为什么。参数中的可变借用被调用into_iter()for a in data中使用(用完)。

rustc建议进行可变重新借用。这样我们就不会尝试借用已经移动的东西。进行这种更改(现在保持return的注释),以下内容现在可以通过类型检查:

fn find_unused<'a>(data: &'a mut Vec<Association>) -> Option<&'a String> {
    for k in 0.. {
        for a in &mut *data { // we took rustc's suggestion
            if a.key == k && !a.used {
                a.used = true;
                // return Some(&a.value);
            }
        }
    }
    None
}

如果我们取消注释return,就会出现错误,指出我们返回的值的生命周期与'a不匹配。
但是如果我们使用cargo +nightly rustc -- -Z polonius运行,代码将通过类型检查。

@stargateur建议在评论中尝试polonius

这是我猜测Polonius有所帮助的原因:
  • 对于当前的rustc借用检查器,函数签名中的生命周期在函数体内以相对粗略和简单的方式处理。我认为这里发生的情况是,'a 应该跨越整个函数体,但是 a 的新鲜可变借用仅跨越函数体的一部分,因此 Rust 不会将它们统一起来。
  • Polonius 在类型中跟踪借用的来源。因此,它知道 &a.value 的生命周期来自于 a,而 a 又来自于 data,其生命周期为 'a,因此 Polonius 知道 return 是可以的。

我基于The Polonius Talk 进行了推断,但 The Book 可能更为最新。


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