【问题标题】:Why does Rust borrow checker reject this code?为什么 Rust 借用检查器会拒绝此代码?
【发布时间】:2014-09-10 22:07:06
【问题描述】:

我从借用检查器中得到一个 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?
}

【问题讨论】:

    标签: rust


    【解决方案1】:

    我们来看看consume_till

    它接受&amp;'lt mut self 并返回ConsumeResult&lt;'lt&gt;。这意味着生命周期'lt,即输入参数self的借用时间,将是输出参数,即返回值的生命周期。

    换一种说法,在调用consume_till之后,你不能再次使用self,直到它的结果超出范围。

    该结果被放入first_tokenfirst_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 提供的内存安全。

    【讨论】:

    • 我不太确定这是否正确,@ChrisMorgan。我这么说的原因是你的回应意味着生命周期注释实际上延长了&amp;mut self借用的生命周期,但我认为生命周期注释只是对编译器关于 return 生命周期的断言i> 价值。如果 &amp;mut self 借用了 main 的整个范围,那么为什么可以连续调用 Rust chars() 迭代器两次(它也需要 &amp;mut self)?我需要更深入地了解这里实际发生的事情,否则我只是在黑暗中猜测。谢谢。
    • chars() takes &amp;self, not &amp;mut self,同时拥有多个不可变的借用是可以的——它只是可变的借用,你不能这样做。这是否清楚,还是您需要我进一步解释?
    • 这是来自 github 的代码 github.com/rust-lang/rust/blob/master/src/libcore/str.rs impl&lt;'a&gt; Iterator&lt;char&gt; for Chars&lt;'a&gt; { #[inline] fn next(&amp;mut self) -&gt; Option&lt;char&gt; {
    • 但这不就是我在上面做的事情吗?我的任何代码都没有计划更改“代码”字符串,所以我的意图是只拥有不可变的引用。我选择&amp;mut self 的唯一原因是因为 rustc 坚持使用它,因为我更新了字符串中的迭代器位置。
    • @CharlieFlowers 如果您从&amp;mut self 中删除&lt;'lt&gt; 声明和'lt(两者都在consume_till),它可能会起作用。
    【解决方案2】:

    这是一个更简单的例子:

    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);
    }
    

    如果you compile that,你会得到类似问题中代码的错误:

    <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 }
              ^
    

    现在,要做的第一件事是避免遮蔽生命周期:也就是说,这段代码有两个生命周期,称为 'astep_by_3_bytes 中的所有 'as 都引用了 'a 声明,没有一个它们实际上指的是Scanner&lt;'a&gt; 中的'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 重新分配并因此在内存中移动,而将任何其他&amp;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 字段,并且 &amp;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(或调用更多方法)。

    综上所述,the full, working code

    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);
    }
    

    Applying this change 到您的代码只是调整consume_till 的定义。

    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 trait impl 中,但这并不重要。)

    你这里的情况和写的差不多

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

    (类似“错误的生命周期链接”,细节不同。)

    【讨论】:

    • 太棒了!我有它的工作,更重要的是,我学到了很多。谢谢你和@ChrisMorgan。是否有任何资源可以解释生命的这些特征? (见下一条评论)
    • 例如,我不知道:(1)生命周期可能会影响其他生命周期; (2) 像 consume_till 这样的 fn 可以在其返回类型中引用一个生命周期,该类型不是 fn 类型的一部分(在 fn 名称之后的 中); (3) 结构生命周期是“全局的”,因为其他代码可以单独引用它; (4) &amp;self&amp;mut self 参数上的生命周期注释实际上可以使借用寿命更长; (5) 首先是什么决定了&amp;self&amp;mut self 参数的借用长度? 到目前为止,研究编译器 src 是唯一的方法吗?
    • (1) 被#11658 覆盖(有点),(2) 和(3) 只是“类型变量作用域”,即生命周期/类型变量在fns 中可见和impls 声明它们。 (4)和(5):它“使”借用的寿命更长,因为它表明返回值被固定在它上面,即self被借用用于范围(或最近的封闭语句,如果它没有立即分配给变量),其中创建了借用(更容易将其视为“只要保留返回值,借用就会持续”)。
    • @CharlieFlowers,声明的前半部分(大部分)是准确的,因为引用来自self(也就是说,你不能返回一个引用局部变量)。下半场没有你说的那么准确。 (前半部分忽略了您可以通过static X: Foo = ...; &amp;X 返回&amp;'static Foo。)
    • 对不起,如果返回值和&amp;引用之间没有联系,那么借用就是那个表达式:x.takes_mut_self() == x.takes_mut_self()很好(如果函数返回,例如uint)尽管在单个语句中“借用”x 可变两次。如果有引用,则人为将借用扩展到语句(因为替代方案使用起来很烦人:很多临时变量)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-22
    • 2018-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多