生命期

生命期的意思是 "变量的生命期有多长"。你只需要考虑引用的生命期。这是因为引用的生命期不能比它们来自的对象更长。例如,这个函数就不能用。

fn returns_reference() -> &str {
    let my_string = String::from("I am a string");
    &my_string // ⚠️
}

fn main() {}

问题是my_string只存在于returns_reference中。我们试图返回 &my_string,但是 &my_string 不能没有 my_string。所以编译器说不行。

这个代码也不行。

fn returns_str() -> &str {
    let my_string = String::from("I am a string");
    "I am a str" // ⚠️
}

fn main() {
    let my_str = returns_str();
    println!("{}", my_str);
}

但几乎是成功的。编译器说:

error[E0106]: missing lifetime specifier
 --> src\main.rs:6:21
  |
6 | fn returns_str() -> &str {
  |                     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
6 | fn returns_str() -> &'static str {
  |                     ^^^^^^^^

missing lifetime specifier的意思是,我们需要加一个'的生命期。然后说它contains a borrowed value, but there is no value for it to be borrowed from。也就是说,I am a str不是借来的。它写&'static str就说consider using the 'static lifetime。所以它认为我们应该尝试说这是一个字符串的文字。

现在它工作了。

fn returns_str() -> &'static str {
    let my_string = String::from("I am a string");
    "I am a str"
}

fn main() {
    let my_str = returns_str();
    println!("{}", my_str);
}

这是因为我们返回了一个 &str,生命期为 static。同时,my_string只能以String的形式返回:我们不能返回对它的引用,因为它将在下一行死亡。

所以现在fn returns_str() -> &'static str告诉Rust, "别担心,我们只会返回一个字符串字面量". 字符串字面量在整个程序中都是有效的,所以Rust很高兴。你会注意到,这与泛型类似。当我们告诉编译器类似 <T: Display> 的东西时,我们承诺我们将只使用实现了 Display 的输入。生命期也类似:我们并没有改变任何变量的生命期。我们只是告诉编译器输入的生命期是多少。

但是'static并不是唯一的生命期。实际上,每个变量都有一个生命期,但通常我们不必写出来。编译器很聪明,一般都能自己算出来。只有在编译器不知道的时候,我们才需要写出生命期。

下面是另一个生命期的例子。想象一下,我们想创建一个City结构,并给它一个&str的名字。我们可能想这样做,因为这样做的性能比用String快。所以我们这样写,但还不能用。

#[derive(Debug)]
struct City {
    name: &str, // ⚠️
    date_founded: u32,
}

fn main() {
    let my_city = City {
        name: "Ichinomiya",
        date_founded: 1921,
    };
}

编译器说:

error[E0106]: missing lifetime specifier
 --> src\main.rs:3:11
  |
3 |     name: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
2 | struct City<'a> {
3 |     name: &'a str,
  |

Rust 需要 &str 的生命期,因为 &str 是一个引用。如果name指向的值被丢弃了会怎样?那就不安全了。

'static呢,能用吗?我们以前用过。我们试试吧。

#[derive(Debug)]
struct City {
    name: &'static str, // change &str to &'static str
    date_founded: u32,
}

fn main() {
    let my_city = City {
        name: "Ichinomiya",
        date_founded: 1921,
    };

    println!("{} was founded in {}", my_city.name, my_city.date_founded);
}

好的,这就可以了。也许这就是你想要的结构。但是,请注意,我们只能接受 "字符串字面量",所以不能接受对其他东西的引用。所以这将无法工作。

#[derive(Debug)]
struct City {
    name: &'static str, // must live for the whole program
    date_founded: u32,
}

fn main() {
    let city_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()]; // city_names does not live for the whole program

    let my_city = City {
        name: &city_names[0], // ⚠️ This is a &str, but not a &'static str. It is a reference to a value inside city_names
        date_founded: 1921,
    };

    println!("{} was founded in {}", my_city.name, my_city.date_founded);
}

编译器说:

error[E0597]: `city_names` does not live long enough
  --> src\main.rs:12:16
   |
12 |         name: &city_names[0],
   |                ^^^^^^^^^^
   |                |
   |                borrowed value does not live long enough
   |                requires that `city_names` is borrowed for `'static`
...
18 | }
   | - `city_names` dropped here while still borrowed

这一点很重要,因为我们给它的引用其实已经够长寿了。但是我们承诺只给它一个&'static str,这就是问题所在。

所以现在我们就试试之前编译器的建议。它说尝试写struct City<'a>name: &'a str。这就意味着,只有当name活到City一样寿命的情况下,它才会接受name的引用。

#[derive(Debug)]
struct City<'a> { // City has lifetime 'a
    name: &'a str, // and name also has lifetime 'a.
    date_founded: u32,
}

fn main() {
    let city_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()];

    let my_city = City {
        name: &city_names[0],
        date_founded: 1921,
    };

    println!("{} was founded in {}", my_city.name, my_city.date_founded);
}

另外记住,如果你愿意,你可以写任何东西来代替'a。这也和泛型类似,我们写TU,但实际上可以写任何东西。

#[derive(Debug)]
struct City<'city> { // The lifetime is now called 'city
    name: &'city str, // and name has the 'city lifetime
    date_founded: u32,
}

fn main() {}

所以一般都会写'a, 'b, 'c等,因为这样写起来比较快,也是常用的写法。但如果你想的话,你可以随时更改。有一个很好的建议是,如果代码非常复杂,把生命期改成一个 "人类可读"的名字可以帮助你阅读代码。

我们再来看看与trait的比较,对于泛型。比如说

use std::fmt::Display;

fn prints<T: Display>(input: T) {
    println!("T is {}", input);
}

fn main() {}

当你写T: Display的时候,它的意思是 "只有当T有Display时,才取T"。 而不是说: "我把Display给T".

对于生命期也是如此。当你在这里写 'a:

#[derive(Debug)]
struct City<'a> {
    name: &'a str,
    date_founded: u32,
}

fn main() {}

意思是 "如果name的生命期至少与City一样长,才接受name的输入"。 它的意思不是说: "我会让name的输入与City一样长寿"。

现在我们可以了解一下之前看到的<'_>。这被称为 "匿名生命期",是使用引用的一个指标。例如,当你在实现结构时,Rust会向你建议使用。这里有一个几乎可以工作的结构体,但还不能工作:

    // ⚠️
struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

fn main() {}

所以我们对struct做了我们需要做的事情:首先我们说name来自于一个&str。这就意味着我们需要lifetime,所以我们给了它<'a>。然后我们必须对struct做同样的处理,以证明它们至少和这个生命期一样长。但是Rust却告诉我们要这样做:

error[E0726]: implicit elided lifetime not allowed here
 --> src\main.rs:6:6
  |
6 | impl Adventurer {
  |      ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

它想让我们加上那个匿名的生命期,以表明有一个引用被使用。所以如果我们这样写,它就会很高兴。

struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer<'_> {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

fn main() {}

这个生命期是为了让你不必总是写诸如impl<'a> Adventurer<'a>这样的东西,因为结构已经显示了生命期。

在Rust中,生命期是很困难的,但这里有一些技巧可以避免对它们太过紧张。

  • 你可以继续使用自有类型,使用克隆等,如果你想暂时避免它们。
  • 很多时候,当编译器想要lifetime的时候,你只要在这里和那里写上<'a>就可以了。这只是一种 "别担心,我不会给你任何不够长寿的东西"的说法。
  • 你可以每次只探索一下生命期。写一些拥有值的代码,然后把一个代码变成一个引用。编译器会开始抱怨,但也会给出一些建议。如果它变得太复杂,你可以撤销它,下次再试。

让我们用我们的代码来做这个,看看编译器怎么说。首先我们回去把生命期拿出来,同时实现DisplayDisplay就打印Adventurer的名字。

// ⚠️
struct Adventurer {
    name: &str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {}

第一个抱怨就是这个:

error[E0106]: missing lifetime specifier
 --> src\main.rs:2:11
  |
2 |     name: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct Adventurer<'a> {
2 |     name: &'a str,
  |

它建议怎么做:在Adventurer后面加上<'a>,以及&'a str。所以我们就这么做。

// ⚠️
struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {}

现在它对这些部分很满意,但对impl块感到奇怪。它希望我们提到它在使用引用。

error[E0726]: implicit elided lifetime not allowed here
 --> src\main.rs:6:6
  |
6 | impl Adventurer {
  |      ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

error[E0726]: implicit elided lifetime not allowed here
  --> src\main.rs:12:28
   |
12 | impl std::fmt::Display for Adventurer {
   |                            ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`

好了,我们将这些写进去......现在它工作了!现在我们可以创建一个Adventurer,然后用它做一些事情:

struct Adventurer<'a> {
    name: &'a str,
    hit_points: u32,
}

impl Adventurer<'_> {
    fn take_damage(&mut self) {
        self.hit_points -= 20;
        println!("{} has {} hit points left!", self.name, self.hit_points);
    }
}

impl std::fmt::Display for Adventurer<'_> {

        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{} has {} hit points.", self.name, self.hit_points)
        }
}

fn main() {
    let mut billy = Adventurer {
        name: "Billy",
        hit_points: 100_000,
    };
    println!("{}", billy);
    billy.take_damage();
}

这个将打印:

Billy has 100000 hit points.
Billy has 99980 hit points left!

所以你可以看到,lifetimes往往只是编译器想要确定。而且它通常很聪明,几乎可以猜到你想要的生命期,只需要你告诉它,它就可以确定了。