为什么作为参数传递的 Rust 中的 &str 数组具有不同的生命周期?

Why do &str arrays in Rust passed as parameters have different lifetimes?

我正在学习 Rust,正在测试通过函数复制一些数组。我确信 copy/clone 数组信息有内置的 Rust 函数,但我认为个人实现是一个好主意,可以帮助我理解通过函数传递引用。

fn copy_str_arr_original (a1: [&str; 60], a2: &mut [&str; 60]) {
    // copy 1 into 2
    for i in 0..60 {
        a2[i] = a1[i];
    } // change is reflected in a2 as it is passed as &mut
}

但是,这为 &str 类型本身引发了错误 these two types are declared with different lifetimes...。经过进一步研究,我尝试声明自己的生命周期并将它们分配给它,这就解决了!

fn copy_str_arr_fix<'a> (a1: [&'a str; 60], a2: &mut [&'a str; 60]) {
    // copy 1 into 2
    for i in 0..60 {
        a2[i] = a1[i];
    } // change is reflected in a2 as it is passed as &mut
}

为什么会这样呢?为什么数组中的 type 值需要分配生命周期而不是参数本身?换句话说,为什么这根本不起作用?

fn copy_str_arr_bad<'a> (a1: &'a [&str; 60], a2: &'a mut [&str; 60]) {
    // does not work...           ^-----------------------^-------- different lifetimes
    for i in 0..60 {
        a2[i] = a1[i]; 
    } 
}

我仍在努力了解生命周期如何在更复杂的对象(例如数组和结构)的上下文中工作,因此非常感谢任何解释!

简单的回答是编译器不是很聪明。

您不必在每次定义处理引用的函数时都指定一堆生命周期这一事实只是 because the compiler takes a few educated guesses if it can。所以它有点聪明,但不是很聪明。

假设您正在编写一个函数,该函数采用对结构的引用和returns对该结构中字段的引用:

struct Book {
  pages: u16,
  title: String,
}

fn borrow_title(book: &Book) -> &str {
  &book.title
}

十分之九确实是对您传递的参数的引用。但有时不是:

fn borrow_title(book: &Book) -> &'static str {
  if book.pages > 10 {
    "Too long..."
  } else {
    "Not long enough"
  }
}

如您所见,您需要指定返回的 &str 具有不同的生命周期(在本例中为特殊的 'static.

所以既然你说 fn copy_str_arr_original (a1: [&str; 60], a2: &mut [&str; 60]),编译器实际上并没有推理你的实现并且不知道 a1 中引用的生命周期至少应该 只要 a2.

中任何引用的生命周期

至于第二部分,您需要考虑引用只是指向某些数据的指针。该数据可以包含其他参考。在这种情况下,重要的是这些其他参考文献。

这里有 2 个字符串引用数组。假设您将引用从第一个复制到第二个。是否通过引用将这些数组传递给函数并不重要。重要的是,如果拥有第一个数组所有权的任何东西都被丢弃了,字符串也会被丢弃。如果第二个数组仍然持有任何引用,这将导致不安全的内存处理。

为简化起见,假设只有一个字符串,我们将向数组中借值,然后将借来的值复制到另一个数组,删除第一个数组,然后删除字符串。你希望发生什么?

编译器将进行匹配以确保不会保留对字符串的引用。

错误消息有点令人困惑,因为它指的是根据 规则生成的生命周期。在您的情况下,生命周期省略意味着:

fn copy_str_arr_original(a1: [&str; 60], a2: &mut [&str; 60])

是语法糖:

fn copy_str_arr_original<'a1, 'a2_mut, 'a2>(a1: [&'a1 str; 60], a2: &'a2_mut mut [&'a2 str; 60])

也就是说,我们拥有三个完全不相关的人生。 “不相关”意味着调用者可以选择与他们相关联的对象的存活时间。例如,a2 中的字符串可能是静态的并且一直存在到程序结束,而 a1 中的字符串可能会在 copy_str_arr_original() returns 之后立即被删除。或者反过来。如果这种自由度看起来可能会导致问题,那么您就走在正确的轨道上,因为借用检查员同意您的看法。

请注意,'a2_mut 生命周期的长度完全不相关,这有点违反直觉,它可以随调用者的喜好而定。我们的函数已收到引用,因此可以在函数范围内使用它。 'a2_mut lifetime 告诉我们它将 函数范围之外存在多长时间,我们只是不关心它。

'a1'a2是另一回事。由于我们正在将引用从 a1 复制到 a2,因此我们有效地 转换 a1 中的引用(类型 &'a1 str)存储在 a2 中的引用类型(即 &'a2 str):

a2[i] = a1[i];  // implicitly casts &'a1 str to &'a2 str

要使其有效,&'a1 str 必须是 &'a2 str 子类型 。虽然 Rust 没有 类 和 C++ 意义上的子类化,但它确实有涉及生命周期的子类型。从这个意义上说,如果 A 的值保证至少与 B 的值一样长,则 A 是 B 的子类型。换句话说,'a1 必须至少与 'a2 一样长,表示为'a1: 'a2。所以这个编译:

fn copy_str_arr<'a1: 'a2, 'a2, 'a2_mut>(a1: [&'a1 str; 60], a2: &'a2_mut mut [&'a2 str; 60]) {
    for i in 0..60 {
        a2[i] = a1[i];
    }
}

转换成功的另一种方法是只要求生命周期 与您在 copy_str_arr_fix() 中所做的相同。 (您还省略了 'a2_mut 生命周期,编译器正确地将其解释为对不相关的匿名生命周期的请求。)

假设您可以定义copy_str_arr两个不同的、不相关的生命周期,如下所示:

fn copy_str_arr<'a, 'b>(a1: [&'a str; 60], a2: &mut [&'b str; 60]) {
    // ...
}

然后考虑这个例子:

let mut outer: [&str; 60] = [""; 60];

{
    let temp_string = String::from("temporary string");
    
    let inner: [&str; 60] = [&temp_string; 60];

    // this compiles because our bad `copy_str_arr` function allows
    // `inner` and `outer` to have unrelated lifetimes
    copy_str_array(&inner, &mut outer); 

}   // <-- `temp_string` destroyed here

// now `outer` contains references to `temp_string` here, which is invalid
// because it has already been destroyed!

println!("{:?}", outer); // undefined behavior! may print garbage, crash your
                         // program, make your computer catch fire or anything else

如您所见,如果允许 a1a2 具有完全不相关的生命周期,那么我们最终可能会遇到其中一个数组包含对无效数据的引用的情况,这非常糟糕

但是,生命周期不必相同。您可以改为要求您正在复制的生命周期超过您正在复制到的生命周期(从而确保您不会非法延长引用的生命周期):

fn copy_str_arr<'a, 'b>(a1: &[&'a str; 60], a2: &mut [&'b str; 60])
where
    'a: 'b, // 'a (source) outlives 'b (destination)
{
    for i in 0..60 {
        a2[i] = a1[i];
    }
}