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

5

我正在通过显式注释函数签名来测试在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函数的最终实现中得到了确认,但我不完全确定为什么通过使用where 'b : 'a指定Book实例的生命周期超出可变引用范围可以解决我的问题。

我希望能够深入理解为什么将同样的生命周期用于可变引用和Book实例会导致错误。


@Stargateur,我不确定你的 Rust Playground 链接是否是指向一个可用的示例。在我的帖子中,我已经说明了我能够让我的代码工作,但我正在尝试理解为什么我的初始实现没有成功。 - AC-5
我刚刚向您展示了如果您没有注释任何内容编译器会做什么。当您使用相同的生命周期时,您就是在说我想借用这个引用和book life一样长... - Stargateur
@Stargateur 是不是说问题在于借用引用的时间与书籍生命周期一样长,会同时保持多个可变引用?通过指定书籍可以超过引用的寿命,引用在调用 borrow_and_read 之间被丢弃了?我感到困惑,因为我使用的是一个单一的引用,在主函数中创建,我认为它在整个主函数的生命周期内都存在。我知道不包括生命周期注释也能得到一个工作的程序,但我希望能更好地理解 - 不仅仅是让程序正常运行。 - AC-5
我相信这个实例如何似乎超出了其自身参数的生命周期?的答案可以帮到你,但我不能确定,至少它是相关的。 - Stargateur
1个回答

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

这个函数有两个生命周期参数:一个是书本本身的寿命,另一个是对书本的可变借用。我们还限制了'b: 'a,这意味着任何具有生命周期'a的借用都不能比具有生命周期'b的借用更长。实际上这是多余的,因为编译器可以自动判断。通过使用类型为&'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的技术细节,但我想编译器认为变量的“使用”与我们谈论寿命的方式类似。由于'book必须持续到对observe_book的调用,并且'mtb'book相同,因此它将'book的使用视为可变借用的使用。再次强调,我并不完全确定。可能值得提出问题,看看消息是否可以改进。

我刚才其实撒了一个小谎。虽然Rust不支持隐式类型转换,但是它支持生命周期强制转换。具有较长生命周期的借用可以被强制转换为具有较短生命周期的借用。这在这里并不太重要,但值得知道。

书的标题是一个字符串字面量,类型为&'static str,其中'static是整个程序运行期间持续存在的特殊生命周期。数据嵌入到程序本身的二进制文件中。当我们初始化the_book时,它可能具有类型Book<'static>,但也可以强制转换为某些更短生命周期'bookBook<'book>类型。当我们进行可变借用时,我们被迫使用'book: 'mtb,但我们仍然没有其他限制。

当我们调用只有一个参数的borrow_and_read版本时,'book'mtb都必须被强制转换为一个更短、共同的生命周期。(在这种情况下,由于'book: 'mtb'mtb将工作-实际上,它是可以工作的最长寿命)。使用两个参数的版本,不需要强制转换。可以直接使用'book'mtb
现在,当我们对a_book进行解引用并以不可变方式重新借用它时,不能有可变借用处于活动状态。这意味着mtb'book'mtb都被强制转换成的较短寿命必须结束。但是,a_book的生命周期是'book,我们正在使用它,因此'book无法结束。因此出现了错误。
使用两个参数的版本,'book没有被强制转换为更短的生命周期,因此它可以继续存在。

如果有人能够为第一个错误提供技术解释,我会很乐意听取。我仍然对这一点感到困惑,因为单个可变借用持续了所有三个可变用途,但原始错误消息指出多个可变借用是被禁止的。我可以理解每次调用borrow_and_read时,每个用法都会在结尾处被丢弃,但我想知道实际发生的情况。 - AC-5

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