为什么 Rust 的借用检查器会拒绝这段代码?

10

我从 Rust 借用检查器那里得到了一个编译错误,但我不明白为什么会出错。可能是我没有完全理解生命周期方面的知识。

我已将其简化为一个短小的代码示例。在主函数中,我想要执行以下操作:

fn main() {
    let codeToScan = "40 + 2";
    let mut scanner = Scanner::new(codeToScan);
    let first_token = scanner.consume_till(|c| { ! c.is_digit ()});
    println!("first token is: {}", first_token);
    // scanner.consume_till(|c| { c.is_whitespace ()}); // WHY DOES THIS LINE FAIL?
}
尝试第二次调用scanner.consume_till会导致如下错误:
example.rs:64:5: 64:12 error: cannot borrow `scanner` as mutable more than once at a time
example.rs:64     scanner.consume_till(|c| { c.is_whitespace ()}); // WHY DOES THIS LINE FAIL?
                  ^~~~~~~
example.rs:62:23: 62:30 note: previous borrow of `scanner` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `scanner` until the borrow ends
example.rs:62     let first_token = scanner.consume_till(|c| { ! c.is_digit ()});
                                    ^~~~~~~
example.rs:65:2: 65:2 note: previous borrow ends here
example.rs:59 fn main() {
...
example.rs:65 }

基本上,我创建了类似于自己的迭代器,并且相当于“next”方法需要使用&mut self。因此,在同一作用域中不能多次使用该方法。

但是,Rust标准库有一个迭代器可以在同一作用域中使用多次,并且它也需要一个&mut self参数。

let test = "this is a string";
let mut iterator = test.chars();
iterator.next();
iterator.next(); // This is PERFECTLY LEGAL

为什么 Rust 标准库的代码能编译,而我的不能?(我确定生命周期注释是问题的根源,但我的生命期理解并没有让我预料到会出现问题。)

这是我的完整代码(只有 60 行,为了这个问题缩短了):

 use std::str::{Chars};
use std::iter::{Enumerate};

#[deriving(Show)]
struct ConsumeResult<'lt> {
     value: &'lt str,
     startIndex: uint,
     endIndex: uint,
}

struct Scanner<'lt> {
    code: &'lt str,
    char_iterator: Enumerate<Chars<'lt>>,
    isEof: bool,
}

impl<'lt> Scanner<'lt> {
    fn new<'lt>(code: &'lt str) -> Scanner<'lt> {
        Scanner{code: code, char_iterator: code.chars().enumerate(), isEof: false}
    }

    fn assert_not_eof<'lt>(&'lt self) {
        if self.isEof {fail!("Scanner is at EOF."); }
    }

    fn next(&mut self) -> Option<(uint, char)> {
        self.assert_not_eof();
        let result = self.char_iterator.next();
        if result == None { self.isEof = true; }
        return result;
    }

    fn consume_till<'lt>(&'lt mut self, quit: |char| -> bool) -> ConsumeResult<'lt> {
        self.assert_not_eof();
        let mut startIndex: Option<uint> = None;
        let mut endIndex: Option<uint> = None;

        loop {
            let should_quit = match self.next() {
                None => {
                    endIndex = Some(endIndex.unwrap() + 1);
                    true
                },
                Some((i, ch)) => {
                    if startIndex == None { startIndex = Some(i);}
                    endIndex = Some(i);
                    quit (ch)
                }
            };

            if should_quit {
                return ConsumeResult{ value: self.code.slice(startIndex.unwrap(), endIndex.unwrap()),
                                      startIndex:startIndex.unwrap(), endIndex: endIndex.unwrap() };
            }
        }
    }
}

fn main() {
    let codeToScan = "40 + 2";
    let mut scanner = Scanner::new(codeToScan);
    let first_token = scanner.consume_till(|c| { ! c.is_digit ()});
    println!("first token is: {}", first_token);
    // scanner.consume_till(|c| { c.is_whitespace ()}); // WHY DOES THIS LINE FAIL?
}
2个回答

22

这里有一个更简单的例子:

struct Scanner<'a> {
    s: &'a str
}

impl<'a> Scanner<'a> {
    fn step_by_3_bytes<'a>(&'a mut self) -> &'a str {
        let return_value = self.s.slice_to(3);
        self.s = self.s.slice_from(3);
        return_value
    }
}

fn main() {
    let mut scan = Scanner { s: "123456" };

    let a = scan.step_by_3_bytes();
    println!("{}", a);

    let b = scan.step_by_3_bytes();
    println!("{}", b);
}
如果你编译这段代码中的错误一样。
<anon>:19:13: 19:17 error: cannot borrow `scan` as mutable more than once at a time
<anon>:19     let b = scan.step_by_3_bytes();
                      ^~~~
<anon>:16:13: 16:17 note: previous borrow of `scan` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `scan` until the borrow ends
<anon>:16     let a = scan.step_by_3_bytes();
                      ^~~~
<anon>:21:2: 21:2 note: previous borrow ends here
<anon>:13 fn main() {
...
<anon>:21 }
          ^

现在,需要做的第一件事是避免生命周期的阴影效应:也就是说,这段代码有两个称为'a的生命周期,step_by_3_bytes中所有'a都指向那里声明的'a,实际上它们都没有引用到Scanner<'a>中的'a。我将重命名内部生命周期,以清楚地说明发生了什么。

impl<'a> Scanner<'a> {
    fn step_by_3_bytes<'b>(&'b mut self) -> &'b str {

这里的问题在于'bself对象与str返回值连接起来。编译器不得不假设调用step_by_3_bytes可能会进行任意修改,包括使之前的返回值失效,当从外部查看step_by_3_bytes的定义时(这就是编译器工作的方式,类型检查纯粹基于被调用的事物的类型签名,没有内省)。也就是说,它可能被定义为:

struct Scanner<'a> {
    s: &'a str,
    other: String,
    count: uint
}

impl<'a> Scanner<'a> {
    fn step_by_3_bytes<'b>(&'b mut self) -> &'b str {
        self.other.push_str(self.s);
        // return a reference into data we own
        self.other.as_slice()
    }
}

现在,每次调用 step_by_3_bytes 都会开始修改先前返回值所来自的对象。例如,它可能会导致 String 重新分配内存,从而移动内存中的任何其他 &str 返回值作为悬空指针。Rust 通过跟踪这些引用并禁止变异,以防止发生此类灾难性事件。回到我们的实际代码:编译器通过查看 step_by_3_bytes/consume_till 的类型签名来检查 main 的类型,因此它只能假设最坏的情况(即我刚刚给出的示例)。


我们该如何解决这个问题?

让我们退一步:就好像我们刚刚开始,并不知道返回值需要哪些生命周期一样,因此我们只需将它们留为匿名的(实际上无效的 Rust):

impl<'a> Scanner<'a> {
    fn step_by_3_bytes<'b>(&'_ mut self) -> &'_ str {

现在,我们来问一个有趣的问题:我们想在哪里使用哪些生命周期?

通常最好注明最长有效的生命周期,并且我们知道我们的返回值的生命周期为'a(因为它直接来自s字段,而&str对于'a是有效的)。也就是说,

impl<'a> Scanner<'a> {
    fn step_by_3_bytes<'b>(&'_ mut self) -> &'a str {

对于其他的'_',我们实际上并不关心:作为API设计者,我们没有任何特别的愿望或需要将self借用与任何其他引用连接起来(与返回值不同,在返回值中,我们希望/需要表达它来自哪个内存)。因此,我们可以将其省略。

impl<'a> Scanner<'a> {
    fn step_by_3_bytes<'b>(&mut self) -> &'a str {

'b 没有使用,可以删除,这样我们就得到了

impl<'a> Scanner<'a> {
    fn step_by_3_bytes(&mut self) -> &'a str {

这表示Scanner引用了至少有效期为'a的某些内存,然后返回该内存的引用。 self对象本质上只是一个代理,用于操作这些视图:一旦您拥有它返回的引用,就可以丢弃Scanner(或调用更多方法)。

简而言之,完整可工作的代码如下:

struct Scanner<'a> {
    s: &'a str
}

impl<'a> Scanner<'a> {
    fn step_by_3_bytes(&mut self) -> &'a str {
        let return_value = self.s.slice_to(3);
        self.s = self.s.slice_from(3);
        return_value
    }
}

fn main() {
    let mut scan = Scanner { s: "123456" };

    let a = scan.step_by_3_bytes();
    println!("{}", a);

    let b = scan.step_by_3_bytes();
    println!("{}", b);
}

fn consume_till(&mut self, quit: |char| -> bool) -> ConsumeResult<'lt> { 为什么 Rust 标准库的代码可以编译,但我的却不行?(我确信生命周期注解是问题的根源,但我对生命周期的理解并没有让我期望出现问题)。

这里有一个轻微(但不是很大)的区别:Chars 只返回一个 char,即返回值中没有生命周期。而next方法(本质上)具有如下签名:
impl<'a> Chars<'a> {
    fn next(&mut self) -> Option<char> {

这实际上是在一个 Iterator 特质的 impl 中,但这并不重要。

这里的情况类似于编写:

impl<'a> Chars<'a> {
    fn next(&'a mut self) -> Option<char> {

(在“生命周期链接不正确”的方面类似,但细节有所不同。)


1
太棒了!我已经让它工作了,更重要的是,我学到了很多。谢谢你和@ChrisMorgan。有没有任何资源可以解释这些生命周期特性?(见下一条评论) - Charlie Flowers
例如,我之前不知道:(1)生命周期可以遮蔽其他生命周期;(2)像consume_till这样的函数可以在其返回类型中引用一个生命周期,而该生命周期不是函数类型的一部分(在函数名称后面的<和>中);(3)结构体生命周期在某种意义上是“全局”的,因为其他代码可以单独引用它;(4)在&self&mut self参数上的生命周期注释实际上可以使借用更长寿;(5)甚至是什么决定了&self&mut self参数的借用长度?现在深入编译器源码是唯一的方法吗? - Charlie Flowers
1
(1)有点被[#11658](https://github.com/rust-lang/rust/issues/11658)覆盖,(2)和(3)只是“类型变量作用域”,即生命周期/类型变量在它们声明的`fn`和`impl`内可见。 (4)和(5):这是“使”借用活得更长,因为它声明返回值固定在它上面,即self在创建借用的范围内(或最近的封闭语句,如果它不是立即分配给一个变量)中被借用(更容易将其视为“借用持续的时间与保留返回值的时间一样长”)。 - huon
1
@CharlieFlowers,语句的前半部分(大部分)是准确的,因为引用源自 self(即它说你不能返回对局部变量的引用)。第二部分不太准确,正如你所说。(第一部分忽略了事实,即你可以通过 static X: Foo = ...; &X 返回一个 &'static Foo。) - huon
1
抱歉,如果返回值和&引用之间没有连接,则借用只是该表达式:“x.takes_mut_self() == x.takes_mut_self()" 是可以的(如果函数返回,例如 uint),尽管在单个语句中可变地“borrowing” 了 x 两次。如果有引用,借用将人为地延长到语句中(因为替代方案非常麻烦:需要很多临时变量)。 - huon
显示剩余10条评论

5
让我们看一下consume_till。它需要&'lt mut self并返回ConsumeResult<'lt>。这意味着,输入参数self的借用期限'lt将成为输出参数(返回值)的生命周期。
换句话说,在调用consume_till之后,您不能再次使用self,直到它的结果超出作用域。该结果被放入first_token中,并且first_token仍在您的最后一行的作用域内。
为了解决这个问题,您必须使first_token超出作用域; 在其周围插入新块可以实现此目的:
fn main() {
    let code_to_scan = "40 + 2";
    let mut scanner = Scanner::new(code_to_scan);
    {
        let first_token = scanner.consume_till(|c| !c.is_digit());
        println!("first token is: {}", first_token);
    }
    scanner.consume_till(|c| c.is_whitespace());
}

这一切都是有道理的:当您在 Scanner 内部引用某些内容时,不安全让您修改它,以免该引用失效。这就是 Rust 提供的内存安全性。


2
我不太确定这是否正确,@ChrisMorgan。我这么说的原因是你的回答意味着生命周期注释实际上延长了&mut self借用的寿命,但我认为生命周期注释仅仅是对返回值寿命的编译器声明。如果&mut self借用在main函数整个作用域中存在,那么为什么Rust chars()迭代器可以连续调用两次(它也需要&mut self)?我需要深入了解这里实际发生了什么,否则我只是瞎猜。谢谢。 - Charlie Flowers
chars() 接受的是 &self 而不是 &mut self,同时具有多个不可变借用在同一时间是可以的——只是你不能这样做可变借用。这是否清楚,还需要我进一步解释吗? - Chris Morgan
顺便说一下,@ChrisMorgan,请不要误解我的意思。我非常感激你提供的任何帮助。我已经被Rust的承诺所吸引,并且非常渴望深入了解。生命周期指南是一个很好的开始,但它并不足以给我所有需要的帮助。如果你或任何人能够提供帮助,我非常感激。 - Charlie Flowers
1
但这不是我上面所做的相同的事情吗?我的代码从来没有计划更改“code”字符串,因此我的意图是除了不可变的引用之外什么都没有。我之所以使用 &mut self,是因为 rustc 坚持要求这样做,因为我在字符串中更新了迭代器位置。 - Charlie Flowers
1
@CharlieFlowers 如果你移除 <'lt> 声明和 &mut self 中的 'lt(都在 consume_till 函数中),它可能会正常工作。 - huon
显示剩余4条评论

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