Rust 中的生命周期如何影响可变性?

AC-*_*C-5 5 immutability lifetime mutability rust borrow-checker

我正在通过显式注释函数签名来测试我对 Rust 生命周期的理解,并且我创建了一个我不确定我是否理解的示例。

在这个例子中,我模拟了共享一本书并在其中翻页的概念。为此,我使用了一个可变引用,我将其传递给一个borrow_and_read更新结构curr_page字段的Book函数。我的Book结构和main函数看起来像:

#[derive(Debug)]
pub struct Book<'a> {
    pub title: &'a str,
    pub curr_page: Option<i32>,
    pub page_count: i32,
}

fn borrow_and_read<'a>(a_book: &'a mut Book<'a>) {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}

fn main() {
    let mut the_book: Book = Book {
        title: "The Book",
        curr_page: None,
        page_count: 104,
    };

    let a_book: &mut Book = &mut the_book;

    borrow_and_read(a_book);
    borrow_and_read(a_book);

    observe_book(&*a_book);
}

pub fn observe_book<'a>(a_book: &'a Book<'a>) {
    println!("Observing: {:?}", a_book);
}
Run Code Online (Sandbox Code Playgroud)

游乐场

对于该borrow_and_read函数的第一次实现,我让编译器添加注释并编译所有内容:

fn borrow_and_read(a_book: &mut Book) {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}
Run Code Online (Sandbox Code Playgroud)

然后我尝试添加一个生命周期注释,指定引用和Book自身实例的生命周期:

fn borrow_and_read<'a>(a_book: &'a mut Book<'a>) {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}
Run Code Online (Sandbox Code Playgroud)

这产生了以下错误:

error[E0499]: cannot borrow `*a_book` as mutable more than once at a time
  --> src/main.rs:25:21
   |
24 |     borrow_and_read(a_book);
   |                     ------ first mutable borrow occurs here
25 |     borrow_and_read(a_book);
   |                     ^^^^^^
   |                     |
   |                     second mutable borrow occurs here
   |                     first borrow later used here

error[E0502]: cannot borrow `*a_book` as immutable because it is also borrowed as mutable
  --> src/main.rs:27:18
   |
24 |     borrow_and_read(a_book);
   |                     ------ mutable borrow occurs here
...
27 |     observe_book(&*a_book);
   |                  ^^^^^^^^
   |                  |
   |                  immutable borrow occurs here
   |                  mutable borrow later used here
Run Code Online (Sandbox Code Playgroud)

在仔细考虑我最初尝试的内容之后,我认为将可变引用的生命周期BookBook自身的实例分开是有意义的。然后我想出了这个:

fn borrow_and_read<'a, 'b>(a_book: &'a mut Book<'b>) 
where 'b : 'a {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}
Run Code Online (Sandbox Code Playgroud)

确实编译并输出了预期的结果。

我很困惑为什么我的初始错误消息被a_book多次可变地借用。我认为我可以传递单个可变引用,因为每次使用引用都知道该引用是可变的。我的borrow_and_read函数的最终实现似乎证实了这种想法,但我不完全确定为什么指定Book实例的生命周期比可变引用的生命周期长,从而where 'b : 'a解决了我的问题。

我希望对可变引用和Book实例使用相同的生命周期如何产生我得到的错误有一个深入的了解。

SCa*_*lla 3

您原来的问题是生命周期太有限。通过使 上的借用Book与书名上的借用具有相同的长度 ( "The Book"),可变借用被迫与实际书籍本身一样长,这意味着它永远不能被一成不变地借用。

让我们来探讨一下。检查固定版本然后查看原始版本如何限制它会更容易。

fn borrow_and_read<'a, 'b>(a_book: &'a mut Book<'b>) 
where 'b : 'a {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}
Run Code Online (Sandbox Code Playgroud)

该函数有两个生命周期参数:一个用于书籍本身,另一个用于书籍上的可变借阅。我们还进行了约束'b: 'a,这意味着任何具有生命周期的借用的'a有效期不超过具有生命周期的借用'b这实际上是多余的,因为编译器无论如何都可以看到这一点。通过使用类型为 的参数&'a mut Book<'b>'a已经不能持续比 更长时间了'b

现在我们来看一下main。我们将其称为书本本身的生命周期'book。我们将书的可变借用生命周期称为生命周期'mtb。最后,我们将调用不可变借用 (at observe_book) 'imb。让我们看看每一生能持续多久。

// Initialize `the_book`. 'book has to start before this.

// Mutably borrow `the_book`. 'mtb has to start here.
let a_book: &mut Book = &mut the_book;

// Use the mutable borrow. 'mtb has to still be valid.
borrow_and_read(a_book);
// Use the mutable borrow. 'mtb has to still be valid.
borrow_and_read(a_book);

// Deref the mutable borrow and reborrow immutably.
// 'imb has to start here, so 'mtb has to end here.
// 'imb is a reference to `the_book`, so 'book has to still be active.
observe_book(&*a_book);

// The variables are no longer needed, so any outstanding lifetimes can end here
// That means 'imb and 'book end here.
Run Code Online (Sandbox Code Playgroud)

所以这里问题的关键是,通过这个设置,'mtb必须在 之前结束'book。现在让我们看看该函数的原始版本。

fn borrow_and_read<'a>(a_book: &'a mut Book<'a>) {
    match a_book.curr_page {
        Some(page) => a_book.curr_page = Some(page + 1),
        None => a_book.curr_page = Some(0),
    };
}
Run Code Online (Sandbox Code Playgroud)

现在我们只有一个生命周期参数,它强制标题的生命周期和可变借用的生命周期相同。这意味着'mtb'book必须相同。但我们只是表明这'mtb必须提前结束'book!因此,由于存在这种矛盾,编译器给了我们一个错误。我不知道为什么错误是cannot borrow*a_book的技术细节as mutable more than once at a time,但我想编译器对变量的“用法”的思考与我们谈论生命周期的方式类似。由于'book必须持续到对observe_bookand的调用'mtb与 相同'book,因此它将 的用法'book视为可变借用的用法。再说一遍,我对此并不完全确定。可能值得提出一个问题,看看是否可以改进该消息。


我确实躺在上面一点点。虽然 Rust 不执行隐式类型强制,但它执行生命周期强制。寿命较长的借款可能会被强制转为寿命较短的借款。这最终在这里并不重要,但值得了解。

书名是一个字符串文字,其类型为&'static str,其中'static是在程序的整个持续时间内持续的特殊生命周期。数据嵌入到程序本身的二进制文件中。当我们初始化时the_book,它可以具有该类型Book<'static>,但它同样可以被强制Book<'book>为较短的生命周期'book。当我们采用可变借用时,我们被迫拥有'book: 'mtb,但我们仍然没有其他限制。

当我们调用 的单参数版本时borrow_and_read'book'mtb都必须被强制缩短,共同的生命周期。(在这种情况下,因为'book: 'mtb,'mtb会起作用 - 事实上,它是最长的生命周期)。对于双参数版本,不需要强制。'book并且'mtb可以按原样使用。

现在,当我们a_book一成不变地解引用并重新借用它时,任何可变借用都不能处于活动状态。这意味着,以及被迫结束的mtb更短的寿命。但它有生命周期,而且我们正在使用它,所以不能结束。因此出现了错误。'book'mtba_book'book'book

对于双参数版本,'book不会被迫缩短生命周期,因此可以继续。