» Rust快速入门 » 1. 基础篇 » 1.12 作用域

作用域

作用域在所有权、借用和生命周期中扮演着重要的角色。也就是说,它指示编译器何时允许借用、何时可以释放资源以及何时变量被创建或销毁。

RAII

在 Rust 中,变量不仅仅在栈中保存数据:它们还拥有资源,例如 Box<T> 拥有堆中的内存。Rust 强制执行 RAII(资源获取即初始化),因此每当对象超出范围时,都会调用其析构函数并释放其资源。 这种行为可防止资源泄漏,用户自也不必手动释放内存或担心内存泄漏问题。

fn create_box() {
    // 在堆上分配一个整数
    let _1 = Box::new(58i32);
    // `_1` 在这里被销毁,内存被释放
}

fn main() {
    // 在堆上分配一个整数
    let _2 = Box::new(5i32);

    // 创建大量的 Box
    // 无需手动释放内存!
    for _ in 0..1_000 {
        create_box();
    }

    // `_2` 在这里被销毁,内存被释放
}

析构函数

在 Rust 中,析构函数的概念是通过 Drop 特征提供的。当资源超出范围时,将调用析构函数。 并不是每种类型都需要实现此特征,只有在需要其特有的析构逻辑时才需要实现它。

struct HasDrop;

impl Drop for HasDrop {
    fn drop(&mut self) {
        println!("Dropping HasDrop!");
    }
}

struct HasTwoDrops {
    one: HasDrop,
    two: HasDrop,
}

impl Drop for HasTwoDrops {
    fn drop(&mut self) {
        println!("Dropping HasTwoDrops!");
    }
}

fn main() {
    let _x = HasTwoDrops { one: HasDrop, two: HasDrop };
    println!("Running!");
}

所有权

因为变量负责释放自己的资源资源只能有一个所有者。这可防止资源被多次释放。

在进行赋值(let a = b)或按值传递函数参数(func(v))时,资源的所有权被转移。在 Rust 中,这被称为 move

fn use_box(c: Box<i32>) {
    println!("Destroying a box that contains {}", c);
}

fn main() {
    // `a` 是指向堆中分配的整数的指针
    let a = Box::new(58i32);
    // 将 `a` 转移到 `b`
    let b = a;

    // error[E0382]: borrow of moved value: `a`
    println!("a -> {}", a);

    use_box(b); // Destroying a box that contains 58

    // error[E0382]: borrow of moved value: `b`
    println!("b -> {}", b);
}

借用

大多数情况下,用户希望在不获取所有权的情况下访问数据。为了实现这一点,Rust 使用了借用机制。与通过值传递对象不同(T),对象可以通过引用传递(&T)。

// 此函数获取一个 box 的所有权并销毁它
fn useup_box_i32(boxed_i32: Box<i32>) {
    println!("Destroying box that contains {}", boxed_i32);
}

// 此函数借用一个 i32
fn borrow_i32(borrowed_i32: &i32) {
    println!("This int is: {}", borrowed_i32);
}

fn main() {
    // 在堆中创建一个 boxed i32,并在堆栈中创建一个 i32
    let boxed_i32 = Box::new(5_i32);
    let stacked_i32 = 8_i32;

    // 借用 box 的内容,所有权没有转移。
    // 因此其内容可以再次被借用。
    borrow_i32(&boxed_i32); // This int is: 5
    borrow_i32(&stacked_i32); // This int is: 8

    // `boxed_i32` 现在可以转移所有权给 `useup_box_i32` 并被其销毁
    useup_box_i32(boxed_i32); // Destroying box that contains 5
}

可变引用

可变数据可以通过 &mut T 进行可变借用。这称为可变引用,它为借用者提供了读/写权限。

#[derive(Clone, Copy)]
struct Album {
    // `&'static str` 是对在只读内存中分配的字符串的引用
    singer: &'static str,
    title: &'static str,
    year: u32,
}

// 此函数接受对 Album 的引用
fn borrow_album(album: &Album) {
    println!("I immutably borrowed {} - {} edition", album.title, album.year);
}

// 此函数接受对可变 Album 的引用,并将 `year` 更改为 2014
fn new_edition(album: &mut Album) {
    album.year = 2014;
    println!("I mutably borrowed {} - {} edition", album.title, album.year);
}

fn main() {
    let immut_album = Album {
        // 字符串字面量的类型是 `&'static str`
        singer: "Taylor Swift",
        title: "reputation",
        year: 2011,
    };

    // 创建一个可变副本
    let mut mut_album = immut_album;

    // 不可变借用不可变对象
    borrow_album(&immut_album);
    // I immutably borrowed reputation - 2011 edition

    // 不可变借用可变对象
    borrow_album(&mut_album);
    // I immutably borrowed reputation - 2011 edition
    
    // 可变借用一个可变对象
    new_edition(&mut mut_album);
    // I mutably borrowed reputation - 2014 edition

    // error[E0596]: cannot borrow `immut_album` as mutable
    new_edition(&mut immut_album);
}

生命周期

生命周期是一种编译器用来确保所有借用有效的机制构造。具体来说,变量的生命周期始于其创建时,结束于其销毁时。

显式注解

// `succeeded_borrow` 接受两个具有不同生命周期 `'a` 和 `'b` 的 `i32` 引用。
// 这两个生命周期必须都至少与函数 `succeeded_borrow` 的生命周期一样长。
fn succeeded_borrow<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("x is {} and y is {}", x, y);
}

// 一个没有参数但带有生命周期参数 `'a` 的函数。
fn failed_borrow<'a>() {
    let _x = 12;

    // error[E0597]: `_x` does not live long enough
    let y: &'a i32 = &_x;
    // 在函数内部使用生命周期 `'a` 作为显式类型注解将失败,因为 `&_x` 的生命周期比 `y` 的生命周期短。
    // 无法将短生命周期强制转换为长生命周期。
}

fn main() {
    let (five, eight) = (5, 8);
    
    // 将这两个变量的借用 (`&`) 传递到函数中。
    succeeded_borrow(&five, &eight);
    // 任何被借用的输入都必须比借用者的生命周期更长。
    // 换句话说,`five` 和 `eight` 的生命周期必须比 `succeeded_borrow` 更长。
    
    failed_borrow();
    // 此处 `failed_borrow` 中没有强制 `'a` 比函数的生命周期更长,
    // 但 `'a` 的生命周期确实更长。因为没约束的生命周期默认为 `'static`。
}

结构体生命周期

结构体中的生命周期注解与函数类似。

// 此结构体的两个字段引用生命周期必须都超出此结构体的生命周期。
#[derive(Debug)]
struct NamedBorrowed<'a> {
    x: &'a i32,
    y: &'a i32,
}

#[derive(Debug)]
enum Either<'a> {
    Num(i32),
    Ref(&'a i32),
}

fn main() {
    let (x, y) = (5, 8);

    let double = NamedBorrowed { x: &x, y: &y };
    let reference = Either::Ref(&x);
    let number    = Either::Num(y);

    println!("x and y are borrowed in {:?}", double);
    println!("x is borrowed in {:?}", reference);
    println!("y is not borrowed in {:?}", number);
}

静态生命周期

Rust 有一些专有的生命周期名称,其中之一就是 'static

// 有 'static 生命周期的引用
let s: &'static str = "hello world";

// 'static 作为 trait 约束的一部分
fn generic<T>(x: T) where T: 'static {}

引用生命周期 有两种常见的方式可以创建 'static 生命周期的变量,两者都存储在最终产出二进制文件的只读存储中(即静态数据区):

  • 使用 static 声明创建一个常量。
  • 创建一个 string 字面量,其类型为:&'static str
static NUM: i32 = 18;
let s = "I'm a string literal";

Trait 约束

static 用作 trait 约束时表示该类型不包含任何非静态引用。

use std::fmt::Debug;

fn print_it( input: impl Debug + 'static ) {
    println!("'static value: {:?}", input);
}

fn main() {
    // i 有所有权并且不包含任何引用,因此它是 'static 的
    let i = 58;
    print_it(i); // 'static value: 58

    // &i 并非 'static
    // error[E0597]: `i` does not live long enough
    print_it(&i);
}

一些生命周期模式很常见,因此编译器中借用检查器会允许用户省略它们,以便节省输入并提高可读性。这被称为生命周期省略

代码挑战

尝试修改编辑器中提供的代码以添加合适的生命周期。

Loading...
> 此处输出代码运行结果
上页
下页