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

How do lifetimes in Rust impact mutability?

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

在这个例子中,我模拟了分享一本书并在其中翻页的概念。为此,我使用了一个可变引用,我将其传递给 borrow_and_read 函数,该函数更新 Book 结构的 curr_page 字段。我的 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);
}

(Playground)

对于 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),
    };
}

然后我尝试添加一个生命周期注释,为 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),
    };
}

这产生了以下错误:

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

在仔细考虑了我最初的尝试之后,我决定将 Book 的可变引用的生命周期与 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),
    };
}

编译并输出预期结果。

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

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

你原来的问题是生命周期太有限了。通过使 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),
    };
}

此函数有两个生命周期参数:一个用于图书本身,一个用于图书的可变借阅。我们还约束 'b: 'a,这意味着任何生命周期为 'a 的借用的有效期不超过生命周期为 'b 的借用。 This is actually redundant,因为无论如何编译器都能看到。通过使用 &'a mut Book<'b> 类型的参数,'a 的持续时间不会超过 'b.

现在让我们看看main。我们将书本本身称为生命周期 'book。我们将书的可变借用称为生命周期 'mtb。最后,我们将调用不可变借用(在 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.

所以这里问题的症结在于,使用此设置,'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),
    };
}

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


我确实在上面躺了一点。虽然 Rust 不进行隐式类型强制转换,但它确实进行生命周期强制转换。可以将具有较长生命周期的借用强制转换为具有较短生命周期的借用。这最终在这里并不重要,但值得了解。

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

当我们调用 borrow_and_read 的 one-parameter 版本时,'book'mtb 都必须被强制缩短到更短的共同生命周期。 (在这种情况下,因为 'book: 'mtb'mtb 会起作用——事实上,这是最长的寿命)。使用 two-parameter 版本,不需要强制转换。 'book'mtb 可以按原样使用。

现在当我们取消引用 a_book 并以不可变的方式重新借用它时,任何可变借用都不能激活。这意味着 mtb'book'mtb 都被迫结束的较短寿命。但是 a_book 有生命周期 'book 而我们正在使用它,所以 'book 不能结束。因此错误。

使用 two-parameter 版本,'book 不会被迫缩短生命周期,因此它可以继续。