rust学习笔记之基础:所有权、生命周期

所有权

所有权(系统)是 Rust 最为与众不同的特性,它让 Rust 无需垃圾回收器(garbage collector)即可保证内存安全,是 Rust 的核心功能之一。

所有运行的程序都必须管理其使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时不断地寻找不再使用的内存;在另一些语言中,开发者必须亲自分配和释放内存。Rust 则选择了第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。

  • 栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈以放入值的顺序存储值并以相反顺序取出值。这也被称作后进先出(last in, first out)。增加数据叫做进栈,而移出数据叫做出栈。栈中的所有数据都必须占用已知且固定的大小。
  • 在编译时大小未知或大小可能变化的数据,要改为存储在堆上。堆是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。内存分配器(memory allocator)在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针(pointer)。这个过程称作在堆上分配内存,有时简称为 “分配”。将数据推入栈中并不被认为是分配。因为指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针。
  • 入栈比在堆上分配内存要快,因为入栈时分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
  • 访问堆上的数据比访问栈上的数据慢,因为必须通过指针来访问。现代处理器在内存中跳转越少就越快,缓存处理器在处理的数据彼此较近的时候(比如在栈上)比较远的时候(比如可能在堆上)能更好的工作。在堆上分配大量的空间也可能消耗时间。
  • 跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。

作用域规则

Rust 的变量不只是在栈中保存数据:它们也占有资源,比如 Box<T> 占有堆(heap)中的内存。Rust 强制实行 RAII(Resource Acquisition Is Initiallization,资源获取即初始化),所以任何对象在离开作用域时,它的析构函数(destructor)就被调用,然后它占有的资源就被释放。这种行为避免了资源泄漏(resource leak),所以你再也不用手动释放内存或者担心内存泄漏(memory leak)!

Rust 中的析构函数概念是通过 Drop trait 提供的。当资源离开作用域,就调用析构函数。你无需为每种类型都实现 Drop trait,只要为那些需要自己的析构函数逻辑的类型实现就可以了。如持有外部资源的类型:‌文件句柄(File)、‌网络连接(TcpStream)、‌ 数据库连接等需要显式关闭的资源。Rust 不会自动关闭这些资源,需通过实现 Drop trait 确保释放。

未实现 Drop trait 的类型离开作用域时,仅释放栈内存,无额外清理逻辑。

所有权规则

Rust 中的每一个值都有一个被称为其所有者(owner)的变量。值在任一时刻有且只有一个所有者。当所有者(变量)离开作用域,这个值将被丢弃。

因为变量要负责释放它们拥有的资源,所以资源只能拥有一个所有者。这也防止了资源的重复释放。注意并非所有变量都拥有资源(例如引用)。
在进行赋值(let x = y)或通过值来传递函数参数(foo(x))的时候,资源的所有权(ownership)会发生转移。按照 Rust 的说法,这被称为资源的移动(move)。在移动资源之后,原来的所有者不能再被使用,这可避免悬挂指针(dangling pointer)的产生。

Rust 有一个叫做 Copy trait 的特殊标注,可以用在类似整型这样的存储在栈上的类型上。如果一个类型实现了 Copy trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Drop trait 的类型使用 Copy trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy trait,将会出现一个编译时错误。

// 此函数取得堆分配的内存的所有权
fn destroy_box(c: Box<i32>) {
    println!("Destroying a box that contains {}", c);

    // `c` 被销毁且内存得到释放
}

fn main() {
    // 栈分配的整型
    let x = 5u32;
    // 将 `x` 复制到 `y`,不存在资源移动
    let y = x;
    // 两个值各自都可以使用
    println!("x is {}, and y is {}", x, y);

    // `a` 是一个指向堆分配的整数的指针
    let a = Box::new(5i32);
    println!("a contains: {}", a);
    // 移动 `a` 到 `b`
    let b = a;
    // 把 `a` 的指针地址(而非数据)复制到 `b`。
    // 现在两者都指向同一个堆分配的数据,但是现在是 `b` 拥有它。
    // `a` 不再能访问数据,因为它不再拥有那部分堆上的内存。

    // 此函数从 `b` 中取得堆分配的内存的所有权
    destroy_box(b);

    // 此时堆内存已经被释放,这个操作会导致解引用已释放的内存,而这是编译器禁止的。
    // println!("b contains: {}", b);
}
变量作用域

作用域是一个项(item)在程序中有效的范围。

fn main() {
    {                      // s 在这里无效, 它尚未声明
        let s = "hello";   // 从此处起,s 开始有效

        // 使用 s
    }                      // 此作用域已结束,s 不再有效
}

当 s 进入作用域 时,它就是有效的。这一直持续到它离开作用域为止。

变量与数据交互的方式:复制

大小固定且在编译时已知、存储在栈(Stack)‌的类型默认实现 Copy trait,赋值或传参时自动复制值,不涉及所有权转移。
包括所有整数类型、布尔类型、所有浮点数类型、字符类型 char、元组和数组(当且仅当其包含的类型也都实现 Copy 的时候。比如(i32, i32)实现了 Copy,但(i32, String)就没有)。

let x = 5; // 将 5 绑定到 x,第一个5入栈
let y = x; // 接着生成一个值 x 的拷贝并绑定到 y,第二个5入栈
// x 仍然有效,是因为x的数据类型实现了 Copy trait
变量与数据交互的方式:移动

字符串字面量和 String 数据:

  • 字符串字面量(&str)‌:‌ 存储位置在程序的只读内存区,编译时分配大小,内容不可变 ‌。类型为 &str,零运行时开销,快速且高效。
  • String 类型管理被分配到堆上的、在编译时未知大小运行时向内存分配器请求内存的可变可增长的文本片段。String 由三部分组成:一个指向存放字符串内容内存的指针、一个长度和一个容量。这一组数据存储在栈上。指针指向堆上存放内容的内存部分。
let s1 = String::from("hello"); // 基于字符串字面量来创建 String
let s2 = s1;  // 复制 String数据,指针都指向同一片内存
// s1 不再有效

当 s2 和 s1 离开作用域,他们都会尝试释放相同的内存。这是一个叫做二次释放(double free)的错误,两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。为了确保内存安全,这种场景下 ,在 let s2 = s1 之后,Rust 认为 s1 不再有效,因此 Rust 不需要在 s1 离开作用域后清理任何东西。

变量与数据交互的方式:克隆

如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的通用函数。

let s1 = String::from("hello");
let s2 = s1.clone(); // 这里堆上的数据被复制了

部分移动

在单个变量的解构内,可以同时使用 by-move 和 by-reference 模式绑定。这样做将导致变量的部分移动(partial move),这意味着变量的某些部分将被移动,而其他部分将保留。在这种情况下,后面不能整体使用父级变量,但是仍然可以使用只引用(而不移动)的部分。

fn main() {
    #[derive(Debug)]
    struct Person {
        name: String,
        age: u8,
    }

    let person = Person {
        name: String::from("Alice"),
        age: 20,
    };

    // `name` 从 person 中移走,但 `age` 只是引用
    let Person { name, ref age } = person;
    println!("The person's age is {}", age);
    println!("The person's name is {}", name);

    // 报错!部分移动值的借用:`person` 部分借用产生
    //println!("The person struct is {:?}", person);

    // `person` 不能使用,但 `person.age` 因为没有被移动而可以继续使用
    println!("The person's age from person struct is {}", person.age);
}

函数参数的所有权转移

将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。

fn main() {
  let s = String::from("hello");  // s 进入作用域
  takes_ownership(s);             // s 的值移动到函数里,之后不再有效
  let x = 5;                      // x 进入作用域
  makes_copy(x);                  // x 应该移动函数里,但 i32 是 Copy 的,所以在后面可继续使用 x
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
  println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
  println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

返回值也可以转移所有权

fn main() {
  let s1 = gives_ownership();         // gives_ownership 将返回值移给 s1
  let s2 = String::from("hello");     // s2 进入作用域
  let s3 = takes_and_gives_back(s2);  // s2 被移动到takes_and_gives_back 中,它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {           // gives_ownership 将返回值移动给调用它的函数
  let some_string = String::from("yours"); // some_string 进入作用域
  some_string                              // 返回 some_string 并移出给调用的函数
}

fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
  a_string  // 返回 a_string 并移出给调用的函数
}

借用

多数情况下,我们更希望能访问数据,同时不取得其所有权。为实现这点,Rust 使用了借用(borrowing)机制。对象可以通过引用(&T)来传递,从而取代通过值(T)来传递。

编译器(通过借用检查)静态地保证了引用总是指向有效的对象。也就是说,当存在引用指向一个对象时,该对象不能被销毁。

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // &s1 语法让我们创建一个 指向值 s1 的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃。
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {// s 是对 String 的引用
    s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,所以什么也不会发生

变量 s 有效的作用域与函数参数的作用域一样,不过当引用停止使用时并不丢弃它指向的数据,因为没有所有权。当函数使用引用而不是实际值作为参数,无需返回值来交还所有权,因为就不曾拥有所有权。

正如变量默认是不可变的,引用也一样,(默认)不允许修改引用的值。

可变引用

可变数据可以使用 &mut T 进行可变借用。这叫做可变引用(mutable reference),它使借用者可以读/写数据。相反,&T 通过不可变引用(immutable reference)来借用数据,借用者可以读数据而不能更改数据:

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

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

数据借用规则

数据可以多次不可变借用,但是在不可变借用的同时,原始数据不能使用可变借用。或者说,同一时间内只允许一次可变借用。仅当最后一次使用可变引用之后,原始数据才可以再次借用。

可变引用有一个很大的限制:在同一时间,只能有一个对某一特定数据的可变引用。尝试创建两个可变引用将报错:

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{}, {}", r1, r2);
}

可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能“同时”拥有:

fn main() {
    let mut s = String::from("hello");
    {
        let r1 = &mut s;
    } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
    let r2 = &mut s;
}

类似的规则也存在于同时使用可变与不可变引用中。这些代码会导致一个错误:

fn main() {
    let mut s = String::from("hello");
    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    let r3 = &mut s; // 大问题
    println!("{}, {}, and {}", r1, r2, r3);
}

注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。所以如下代码是可以编译的:

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

    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    println!("{} and {}", r1, r2);
    // 此位置之后 r1 和 r2 不再使用

    let r3 = &mut s; // 没问题
    println!("{}", r3);
}

悬垂引用(Dangling References)

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。Rust 不会允许我们这么做:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // 返回一个字符串的引用
    let s = String::from("hello"); // s 是一个新字符串
    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。危险!

ref 模式

#[derive(Clone, Copy)] // 自动实现特定trait的语法
struct Point { x: i32, y: i32 }

fn main() {
    let c = 'Q';

    // 赋值语句中左边的 `ref` 关键字等价于右边的 `&` 符号。
    let ref ref_c1 = c;
    let ref_c2 = &c;
    println!("ref_c1 equals ref_c2: {}", *ref_c1 == *ref_c2);

    let point = Point { x: 0, y: 0 };

    // 在解构一个结构体时 `ref` 同样有效。
    let _copy_of_x = {
        // `ref_to_x` 是一个指向 `point` 的 `x` 字段的引用。
        let Point { x: ref ref_to_x, y: _ } = point;

        // 返回一个 `point` 的 `x` 字段的拷贝。
        *ref_to_x
    };
    println!("copy_of_x: {}", _copy_of_x);

    // `point` 的可变拷贝
    let mut mutable_point = point;

    {
        // `ref` 可以与 `mut` 结合以创建可变引用。
        let Point { x: _, y: ref mut mut_ref_to_y } = mutable_point;

        // 通过可变引用来改变 `mutable_point` 的字段 `y`。
        *mut_ref_to_y = 1;
    }

    println!("point is ({}, {})", point.x, point.y);
    println!("mutable_point is ({}, {})", mutable_point.x, mutable_point.y);

    // 包含一个指针的可变元组
    let mut mutable_tuple = (Box::new(5u32), 3u32);

    {
        // 解构 `mutable_tuple` 来改变 `last` 的值。
        let (_, ref mut last) = mutable_tuple;
        *last = 2u32;
    }

    println!("tuple is {:?}", mutable_tuple);
}

输出

ref_c1 equals ref_c2: true
point is (0, 0)
mutable_point is (0, 1)
tuple is (5, 2)

切片 Slice 类型

切片(slice)类型和数组类似,但其大小在编译时是不确定的。相反,切片是一个双字对象(two-word object),第一个字是一个指向数据的指针,第二个字是切片的长度。这个“字”的宽度和 usize 相同,由处理器架构决定,比如在 x86-64 平台上就是 64 位。

slice 可以用来借用数组的一部分。slice 的类型标记为 &[T]。slice 允许引用集合中一段连续的元素序列,而不用引用整个集合。

字符串 slice 和字符串字面量

  • 字符串 slice(string slice)是 String 中一部分值的引用,类型声明写作 &str
  • 字符串字面量就是 slice,类型是 &str
fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5]; // 不包含结束位置索引
    let slice = &s[..2]; // 开始索引从0开始
    let slice = &s[3..]; // 包含最后一个字节
    let slice = &s[..]; // 整个字符串的 slice
}

例子

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);
    // s.clear(); // error!
    println!("the first word is: {}", word);

    let my_string = String::from("hello world");
    let word = first_word(&my_string[0..6]); // `String` 的部分切片
    let word = first_word(&my_string[..]); // `String` 的全部切片
    let word = first_word(&my_string); // `String` 的引用,等同于 `String` 的全部切片

    let my_string_literal = "hello world";
    let word = first_word(&my_string_literal[0..6]); // 字符串字面量的部分切片
    let word = first_word(&my_string_literal[..]); // 字符串字面量的全部切片
    let word = first_word(my_string_literal);// 字符串字面值就是字符串 slice
}

fn first_word(s: &str) -> &str { // 返回一个 slice
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

其他类型的 slice

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 数组slice,类型 &[i32]

生命周期与引用有效性

编译器(中的借用检查器)用生命周期(lifetime)来保证所有的借用都是有效的。确切地说,一个变量的生命周期在它创建的时候开始,在它销毁的时候结束。

Rust 中的每一个引用都有其生命周期(lifetime),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

生命周期避免了悬垂引用

生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。

如下面例子,尝试使用离开作用域的值的引用:

{
    let r;

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

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

这段代码不能编译因为 r 引用的值在尝试使用之前就离开了作用域。变量 x 并没有 “存在的足够久”。其原因是 x 在到达内部作用域结束时就离开了作用域。不过 r 在外部作用域仍是有效的;作用域越大我们就说它 “存在的越久”。

借用检查器

Rust 编译器有一个借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

这里将 r 的生命周期标记为 'a 并将 x 的生命周期标记为 'b。如你所见,内部的 'b 块要比外部的生命周期 'a 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 'a,不过它引用了一个拥有生命周期 'b 的对象。程序被拒绝编译,因为生命周期 'b 比生命周期 'a 要小:被引用的对象比它的引用者存在的时间更短。

没有产生悬垂引用且可以正确编译的例子:

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

这里 x 拥有生命周期 'b,比 'a 要大。这就意味着 r 可以引用 x:Rust 知道 r 中的引用在 x 有效的时候也总是有效的。

函数中的生命周期

让我们来编写一个函数,返回两个字符串 slice 中较长的那一个。这个函数获取两个字符串 slice 并返回一个字符串 slice。

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

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

如果尝试实现 longest 函数,它并不能编译:提示文本揭示了返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 x 或 y。事实上我们也不知道,因为函数体中 if 块返回一个 x 的引用而 else 块返回一个 y 的引用!

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

生命周期标注语法

生命周期标注并不改变任何引用的生命周期的长短。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。生命周期标注描述了多个引用生命周期相互的关系,而不影响其生命周期。

生命周期标注有着一个不太常见的语法:生命周期参数名称必须以撇号 ' 开头,其名称通常全是小写,类似于泛型其名称非常短。'a 是大多数人默认使用的名称。生命周期参数标注位于引用的 & 之后,并有一个空格来将引用类型与生命周期标注分隔开。

&i32        // 引用
&'a i32     // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

单个生命周期标注本身没有多少意义,因为生命周期标注告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。

就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中。这里我们想要告诉 Rust 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期。

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

当具体的引用被传递给 longest 时,被 'a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值就能保证在 x 和 y 中较短的那个生命周期结束之前保持有效。这就是我们告诉 Rust 需要其保证的约束条件。通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 longest 函数并不需要知道 x 和 y 具体会存在多久,而只需要知道有某个可以被 'a 替代的作用域将会满足这个签名。

当在函数中使用生命周期标注时,这些标注出现在函数签名中,而不存在于函数体中的任何代码中。这些生命周期在每次函数被调用时都可能不同。这也就是为什么我们需要手动标记生命周期。

注意下面示例中的代码不能通过编译:错误表明为了保证 println! 中的 result 是有效的,string2 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(longest)函数的参数和返回值都使用了相同的生命周期参数 'a

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 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 y 指定一个生命周期。

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

在这个例子中,我们为参数 x 和返回值指定了生命周期参数 'a,不过没有为参数 y 指定,因为 y 的生命周期与参数 x 和返回值的生命周期没有任何关系。

综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

结构体定义中的生命周期标注

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

这个结构体有一个字段 part,它存放了一个字符串 slice,这是一个引用。类似于泛型参数类型,必须在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。这个标注意味着 ImportantExcerpt 的实例不能比其 part 字段中的引用存在的更久。novel 的数据在 ImportantExcerpt 实例创建之前就存在。另外,直到 ImportantExcerpt 离开作用域之后 novel 都不会离开作用域,所以 ImportantExcerpt 实例中的引用是有效的。

trait 方法中生命期的标注

trait 方法中生命期的标注基本上与函数类似。注意,impl 也可能有生命周期的标注。

// 带有生命周期标注的结构体。
#[derive(Debug)]
 struct Borrowed<'a> {
     x: &'a i32,
 }

// 给 impl 标注生命周期。
impl<'a> Default for Borrowed<'a> {
    fn default() -> Self {
        Self {
            x: &10,
        }
    }
}

fn main() {
    let b: Borrowed = Default::default();
    println!("b is {:?}", b); // b is Borrowed { x: 10 }

    let b = Borrowed::default();
    println!("b is {:?}", b); // b is Borrowed { x: 10 }
}

结合泛型类型参数、trait bounds 和生命周期

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
    where T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

生命周期强制转换

一个较长的生命周期可以强制转成一个较短的生命周期,使它在一个通常情况下不能工作的作用域内也能正常工作。强制转换可由编译器隐式地推导并执行,也可以通过声明不同的生命周期的形式实现。

// 在这里,Rust 推导了一个尽可能短的生命周期。然后这两个引用都被强制转成这个生命周期。
fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 {
    first * second
}

// `'a: 'b` 读作生命周期 `'a` 至少和 `'b` 一样长。
// 在这里我们我们接受了一个 `&'a i32` 类型并返回一个 `&'b i32` 类型,这是强制转换得到的结果。
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
    first
}

fn main() {
    let first = 2; // 较长的生命周期

    {
        let second = 3; // 较短的生命周期

        println!("The product is {}", multiply(&first, &second)); // The product is 6
        println!("{} is the first", choose_first(&first, &second)); // 2 is the first
    };
}

静态生命周期

'static 生命周期是可能的生命周期中最长的,它会在整个程序运行的时期中存在。'static 生命周期也可被强制转换成一个更短的生命周期。有两种方式使变量拥有 'static 生命周期,它们都把数据保存在可执行文件的只读内存区:

  • 使用 static 声明来产生常量(constant)。
  • 产生一个拥有 &'static str 类型的 string 字面量。

所有的字符串字面量都拥有 'static 生命周期,我们也可以选择像下面这样标注出来:

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

例子

// 产生一个拥有 `'static` 生命周期的常量。
static NUM: i32 = 18;

// 返回一个指向 `NUM` 的引用,该引用不取 `NUM` 的 `'static` 生命周期,而是被强制转换成和输入参数的一样。
fn coerce_static<'a>(_: &'a i32) -> &'a i32 {
    &NUM
}

fn main() {
    {
        // 产生一个 `string` 字面量并打印它:
        let static_string = "I'm in read-only memory";
        println!("static_string: {}", static_string); // static_string: I'm in read-only memory

        // 当 `static_string` 离开作用域时,该引用不能再使用,不过数据仍然存在于二进制文件里面。
    }

    {
        // 产生一个整型给 `coerce_static` 使用:
        let lifetime_num = 9;

        // 将对 `NUM` 的引用强制转换成 `lifetime_num` 的生命周期:
        let coerced_static = coerce_static(&lifetime_num);
        println!("coerced_static: {}", coerced_static); // coerced_static: 18
    }

    println!("NUM: {} stays accessible!", NUM); // NUM: 18 stays accessible!
}

生命周期省略规则

生命周期省略规则并不是需要开发者遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期标注来解决。

函数或方法的参数的生命周期被称为输入生命周期,而返回值的生命周期被称为输出生命周期。编译器采用三条规则来判断引用何时不需要明确的标注。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn 定义,以及 impl 块。

  • 第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:fn foo<'a>(x: &'a i32),有两个引用参数的函数有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。
  • 第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32
  • 第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明是个对象的方法, 那么所有输出生命周期参数被赋予 self 的生命周期。
posted @ 2025-07-21 10:31  carol2014  阅读(30)  评论(0)    收藏  举报