likes
comments
collection
share

Rust中的生命周期

作者站长头像
站长
· 阅读数 50

1. 什么是生命周期

生命周期的定义

在Rust中,每一个引用都有一个生命周期,即这个引用所指向的值在内存中存在的时间段(也可以认为是引用在代码的多少行到多少行有效)。生命周期用来确保引用在其整个生命周期内都是有效的。也是为了确保引用有效而存在。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

在上面的代码中,函数longest有两个输入参数,它们都是字符串切片的引用。并且有一个返回值,也是字符串切片的引用,我们都知道rust语言很注重内存安全,所以为了保证引用有效,从而引入生命周期,使用生命周期来检查引用是否有效,上面的函数中,为了检查返回值的引用是否有效,就要先确定返回值的生命周期。那我们该如何确定呢?

rust语言可以自动推断函数的参数和返回值的生命周期,下面章节会讲到如何推断,但它并不是万能的,只有3种情况可以自己推断出来,上面的代码就不属于可以推断出来的情况,那在这种情况下,我们就必须给他手动标注生命周期,因为如果不标注的话,rust的借用检查器就不能知道返回值的生命周期,也就无法检查其引用是否有效。

再看上面的代码,返回值来自于参数,是不是只要让返回值具有跟参数一样的生命周期,这样在至少在这个函数的使用期间,引用是有效的呢?是的,但问题是有两个参数,他们的生命周期有可能不同,跟谁保持一致呢?这个很好解决,只要跟参数中生命周期最小的那个参数保持一致就好了。这样至少在参数的引用有效的同时,返回值也是有效的。这串代码就可以使用,所以上面代码’a的含义就是,返回值的生命周期为两个’a参数生命周期的交集。从而可以确定返回值的生命周期,用来检查其引用是否符合规范。

生命周期和内存管理

Rust通过生命周期来管理内存。当一个变量离开它的作用域时,它占用的内存就会被释放。如果一个引用指向了一个已经被释放的内存空间,那么这个引用就变成了悬垂引用,使用它会导致编译出错。

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

在上面的代码中,变量x在离开它的作用域后被释放 ,但是变量r仍然保留了对它的引用。这样就产生了一个悬垂引用。Rust编译器会检查这种情况,并给出错误提示。

2. 为什么需要生命周期

防止悬垂引用, 确保内存安全

正如上面提到的,Rust通过生命周期来防止悬垂引用。编译器会检查代码中所有引用的生命周期,确保它们在整个生命周期内都是有效的。

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

在上面的代码中,函数longest返回了一个字符串切片的引用。编译器会检查这个返回值的生命周期是否合法。如果返回值是悬垂引用,编译器会给出错误提示。

下面是一个简单的例子,演示了Rust如何通过生命周期来确保内存安全:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

在这段代码中,我们定义了一个名为longest的函数,它接受两个字符串切片作为参数,并返回一个字符串切片。这个函数使用生命周期参数'a来指定输入参数和返回值的生命周期关系。在这种情况下,生命周期参数'a指定了输入参数和返回值必须具有相同的生命周期。

main函数中,我们创建了两个字符串变量string1string2,并将它们的切片传递给longest函数。由于longest函数的生命周期参数指定了输入参数和返回值必须具有相同的生命周期,所以编译器会检查传递给函数的两个切片是否具有相同的生命周期。在这种情况下,由于string2的生命周期比string1短,所以编译器会给出错误提示,指出返回值可能包含悬垂引用。从而确保内存安全。

3. 生命周期的语法

生命周期标注

在函数定义中,可以使用尖括号来标注生命周期参数。生命周期参数的名称必须以撇号开头,例如'a

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

在上面的代码中,函数longest有两个输入参数,它们都是字符串切片的引用。这两个引用都有一个生命周期参数'a,表示它们必须拥有相同的生命周期。函数返回值也有一个生命周期参数'a,表示返回值的生命周期与输入参数的生命周期相同。

生命周期省略规则

在很多情况下,Rust编译器可以自动推断出引用的生命周期。这种情况下,可以省略生命周期标注。

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

在这段代码中,编译器无法推断出参数和返回值的生命周期。这是因为函数的返回值取决于两个参数的比较结果,而编译器无法确定哪个参数会被返回。

当编译器无法确定函数返回值的生命周期时,它会给出错误提示,要求开发者显式指定生命周期参数。例如,我们可以修改longest函数的定义,使用生命周期参数来指定输入参数和返回值的生命周期关系:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

在这种情况下,生命周期参数'a指定了输入参数和返回值必须具有相同的生命周期。这样,编译器就能够检查传递给函数的参数是否满足生命周期约束,并确保返回值不会包含悬垂引用。

但是在很多情况下,Rust编译器可以自动推断出引用的生命周期。这种情况下,可以省略生命周期标注。Rust编译器使用一套称为生命周期省略规则的规则来自动推断出正确的生命周期。

生命周期省略规则分为三条:

  1. 每一个引用参数都获得它自己的生命周期参数。例如,函数

fn foo(x: &i32)会被转换为fn foo<'a>(x: &'a i32)

  1. 如果函数只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数。例如,函数

fn foo<'a>(x: &'a i32) -> &i32会被转换为fn foo<'a>(x: &'a i32) -> &'a i32

  1. 如果函数有多个输入生命周期参数,但其中一个是&self&mut self,那么它被赋予所有输出生命周期参数。例如,方法

fn foo(&self, x: &i32) -> &i32会被转换为fn foo<'a, 'b>(&'a self, x: &'b i32) -> &'a i32

这些规则使得Rust编译器能够在很多情况下自动推断出正确的生命周期。但是,在一些复杂的情况下,编译器仍然无法自动推断出正确的生命周期,需要程序员显式标注。

4. 生命周期的使用场景

函数参数和返回值

当函数的输入参数或返回值包含引用时,需要使用生命周期来确保引用的有效性。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

在上面的代码中,函数longest有两个输入参数,它们都是字符串切片的引用。这两个引用都有一个生命周期参数'a,表示它们必须拥有相同的生命周期。函数返回值也有一个生命周期参数'a,表示返回值的生命周期与输入参数的生命周期相同。

结构体定义

当结构体中包含引用时,需要使用生命周期来确保引用的有效性。

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt { part: first_sentence };
}

在上面的代码中,结构体ImportantExcerpt包含一个字符串切片的引用。这个引用有一个生命周期参数'a,表示它必须拥有一个确定的生命周期。为了确保该字符串切片不会出现悬垂引用的情况,就需要其保持与该结构体一样的生命周期,意味着该结构体可以使用的时候,字符串切片也可以使用。

5. 生命周期的高级用法

生命周期子类型和多态

Rust支持生命周期子类型和多态。生命周期子类型指的是一个生命周期可以包含另一个生命周期。

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

在上面的代码中,函数longest的第一个输入参数有一个生命周期参数'a,而第二个输入参数没有生命周期参数。这表示第二个输入参数的生命周期可以是任意的,它不会影响函数的返回值。

静态生命周期

Rust中有一个特殊的生命周期'static,表示引用的值在整个程序运行期间都是有效的。

let s: &'static str = "I have a static lifetime.";

在上面的代码中,变量s是一个字符串切片的引用,它拥有静态生命周期。这表示它在整个程序运行期间都是有效的。

6. 生命周期和借用检查器

借用检查器的作用

Rust编译器中包含一个借用检查器,它负责检查代码中所有引用是否满足借用规则。如果不满足,编译器会给出错误提示。

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;

    println!("{}, {}, and {}", r1, r2, r3);
}

在上面的代码中,变量s同时存在可变引用和不可变引用。这违反了Rust的借用规则。编译器会检查这种情况,并给出错误提示。

生命周期检查确保引用在其整个生命周期内都是有效的。然而,这并不意味着只要引用的生命周期相同,借用就是正确的。Rust的借用规则不仅仅关注引用的生命周期,还关注引用的可变性。

在上面的代码中,尽管r1r2r3的生命周期都是相同的,但是它们违反了Rust的借用规则,因为它们试图在同一作用域内同时对同一变量进行不可变和可变借用。根据Rust的借用规则,对于同一变量,可以有多个不可变引用或一个可变引用,但不能同时拥有不可变引用和可变引用。

7. 生命周期的局限性

生命周期的局限性

虽然Rust通过生命周期来管理内存和确保内存安全,但是生命周期也有一些局限性。例如,有时候编译器无法自动推断出正确的生命周期,需要程序员显式标注。这会增加程序员的负担,并降低代码可读性。

from刘金,转载请注明原文链接。感谢!

转载自:https://juejin.cn/post/7226696723359236133
评论
请登录