rust入坑之旅-作用域

Scroll Down

RAII

所有权

Rust 的变量不只是在栈中保存数据:它们也占有资源,比如 Box 占有 堆(heap)中的内存。Rust 强制实行 RAII(Resource Acquisition Is Initiallization,资源获取即初始化),所以任何对象在离开作用域时,它的析构函数就被调用,然后它占有的资源就被释放。Rust 中的析构函数概念是通过 Drop trait 提供的。Drop trait

fn create_box() {
    // 在堆上分配一个整型数据
    let _box1 = Box::new(3i32);

    // `_box1` 在这里被销毁,内存得到释放
}

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

    // 嵌套作用域:
    {
        // 在堆上分配一个整型数据
        let _box3 = Box::new(4i32);

        // `_box3` 在这里被销毁,内存得到释放        
    }

    // 创建一大堆 box(只是因为好玩)。
    // 完全不需要手动释放内存!
    for _ in 0u32..1_000 {
        create_box();
    }

    // `_box2` 在这里被销毁,内存得到释放    
}

此外还可以使用 valgrind 来检查是否还有内存泄漏 rustc xxx.rs && valgrind ./xxx

作用域和以一个变量的生命周期分界线通常在Rust会被混淆,主要是在于没有显示声明生命周期的场景内,如下面的代码中显示,一个变量的生命周期在它创建的时候开始,在它销毁的时候结束。
i变量的作用域在main方法内,而生命周期也是,所以这里就比较混淆二者的概念,但其实这两个还是不太相同的。当显示声明一个变量的生命周期之后,对应的函数 方法 结构体 trait 都会被其约束,比如约束内部的变量所要求的赋值的变量的生命周期要超过对应的结构体函数等。

// 下面使用连线来标注各个变量的创建和销毁,从而显示出生命周期。
fn main() {
    let i = 3; // Lifetime for `i` starts. ────────────────┐
    //                                                     │
    { //                                                   │
        let borrow1 = &i; // `borrow1` lifetime starts. ──┐│
        //                                                ││
        println!("borrow1: {}", borrow1); //              ││
    } // `borrow1 ends. ──────────────────────────────────┘│
    //                                                     │
    //                                                     │
    { //                                                   │
        let borrow2 = &i; // `borrow2` lifetime starts. ──┐│
        //                                                ││
        println!("borrow2: {}", borrow2); //              ││
    } // `borrow2` ends. ─────────────────────────────────┘│
    //                                                     │
}   // Lifetime ends. ─────────────────────────────────────┘

作用域控制了当前变量的工作范围,当普通变量离开了其作用域 要负责释放它们拥有的资源,所以资源只能拥有一个所有者。这也防止了资源的重复释放

borrowing

针对源变量内容的修改称为借用 借用操作则是获取变量的引用,借用处理包括可变引用(mut)和不可变引用,默认是不可变引用,默认情况下这样源变量还可以呗借用,不会失去所有权,可变引用时候会修改源变量的数据,值得注意的是 可以不可变的借用 可变变量和不可变变量,反过来则不行, 不允许可变借用一个不可变变量。
数据可以多次不可变借用,但是在不可变借用的同时,原始数据不能使用可变借用。或者说,同一时间内只允许一次可变借用。仅当最后一次使用可变引用之后,原始数据才可以再次借用,也就是起个别名来多次不可变借用在一个源数据

ref

在通过 let 绑定来进行模式匹配或解构时,ref 关键字可用来创建结构体/元组的字段的引用。

#[derive(Clone, Copy)]
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
    };

    // `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);
}

move

针对获取所有权的处理叫做移动,此时源变量无法在使用,在进行赋值(let x = y)或通过值来传递函数参数(foo(x))的时候,资源的所有权(ownership)会发生转移。按照 Rust 的说法,这被称为资源的移动(move)。移动资源之后,原来的所有者不能再被使用,当存在借用对象时,该对象不能被销毁,也就是无法发生move。

生命周期

当变量未显示标注的生命周期,和作用域比较混淆 变量的生命周期要比变量的作用域工作范围更广一些。

显示标注

Rust 需要显式标注来确定引用的生命周期应该是什么样的。可以用撇号'显式地标出生命周期,语法如下:

foo<'a>
// `foo` 带有一个生命周期参数 `'a`
foo<'a, 'b>
// `foo` 带有一个生命周期参数 `'a,b`

被撇号'标注的方法的作用域不能超过被修饰任意一个变量的生命周期,和闭包类似,使用生命周期需要泛型。3给类型显式地标注生命周期,其语法会像是 &'a T 这样,当函数或者方法被标注了
生命周期之后要注意这2点

  • 任何引用都必须拥有标注好的生命周期。
  • 任何被返回的引用都必须有和某个输入量相同的生命周期或是静态类型(static)。
    另外要注意,如果没有输入的函数返回引用,有时会导致返回的引用指向无效数据,这种情况下禁止它返回这样的引用。
// 一个拥有生命周期 `'a` 的输入引用,其中 `'a` 的存活时间
// 至少与函数的一样长。
fn print_one<'a>(x: &'a i32) {
    println!("`print_one`: x is {}", x);
}

// 可变引用同样也可能拥有生命周期。
fn add_one<'a>(x: &'a mut i32) {
    *x += 1;
}

// 拥有不同生命周期的多个元素。对下面这种情形,两者即使拥有
// 相同的生命周期 `'a` 也没问题,但对一些更复杂的情形,可能
// 就需要不同的生命周期了。
fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("`print_multi`: x is {}, y is {}", x, y);
}

// 返回传递进来的引用也是可行的。
// 但必须返回正确的生命周期。 这里不允许返回y 
fn pass_x<'a, 'b>(x: &'a i32, y: &'b i32) -> &'a i32 { x }

//fn invalid_output<'a>() -> &'a String { &String::from("foo") }
// 上面代码是无效的:`'a` 存活的时间必须比函数的长。
// 这里的 `&String::from("foo")` 将会创建一个 `String` 类型,然后对它取引用。
// 数据在离开作用域时删掉,返回一个指向无效数据的引用。

如果对结构体标注了生命周期,那么引用必须比 标注的结构体 寿命更长。 T: 'a:在 T 中的所有引用都必须比生命周期 'a 活得更长。

#[derive(Debug)]
struct Borrowed<'a>(&'a i32);

fn main() {
    let a: i32 = 2;
    let aaa = Borrowed(&a);
}

如果同时增加了对trait的约束 T: Trait + 'a:T 类型必须实现 Trait trait,并且在 T 中的所有引用都必须比 'a 活得更长。

// 这里接受一个指向 `T` 的引用,其中 `T` 实现了 `Debug` trait,并且在 `T` 中的
// 所有*引用*都必须比 `'a'` 存活时间更长。另外,`'a` 也要比函数活得更长。
fn print_ref<'a, T>(t: &'a T) where
    T: Debug + 'a {
    println!("`print_ref`: t is {:?}", t);
}

'static 生命周期是可能的生命周期中最长的,它会在整个程序运行的时期中存在。


// 产生一个拥有 `'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` 离开作用域时,该引用不能再使用,不过
        // 数据仍然存在于二进制文件里面。
    }

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

        // 将对 `NUM` 的引用强制转换成 `lifetime_num` 的生命周期:
        let coerced_static = coerce_static(&lifetime_num);

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

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

static_string: I'm in read-only memory
coerced_static: 18
NUM: 18 stays accessible