Rust作为一门新兴的编程语言,与其他编程最不一样的就是变量的所有权与生命周期,而这两项又是保证Rust语言安全的基础。

所有权

在大多数编程语言中,对于编译期能够确定大小的类型,一般存放在栈上;编译期无法确定大小的类型,一般都会存储在堆上。在程序执行时比如赋值、传参、返回时,为了避免数据拷贝,会选择传递引用,而不是直接传递数据。所以会遇到堆内存被多次引用的问题,这些引用做什么操作,什么时候释放,都是需要考虑的问题。

C中要求程序员自己管理内存,自行负责内存的申请与释放;Java等语言到处都是按引用传参,使用追踪式GC来回收内存,有性能损耗且存在stop the world问题;Objective-C/Swift使用自动引用计数来管理内存,但是也会有性能开销。

而Rust语言选择了一条不同的路,单一所有权,通过所有权来管理内存,避免了上述问题,为了保证所有权的独占,Rust语言的所有权规则如下:

  • 一个值只能有一个对应的变量来“拥有”它,这个变量被称为“所有者”。
  • 一个值同一时刻只能有一个所有者。
  • 当所有者离开作用域,这个值将被释放。
fn main() {
    let s = String::from("hello");
    let s1 = s; // s的所有权转移给了s1, s不再有效
    // println!("{}", s); // 此处会报错,因为s已经不再有效
    println!("{}", s1);
    // s1释放
}

上面的例子中,当把s赋值给s1时,s的所有权转移到了s1,s不再有效,也就是Move语义,以确保字符串hello在同一时刻只能有一个所有者。当s1离开作用域时,s1的所有权释放,字符串hello也随之释放。

但是这里如果数据是存储在栈上的简单数据,比如整型、浮点型、布尔型等,这都需要转移所有权,又太复杂了吧。所以Rust语言对于这些简单数据类型,采用了Copy trait来实现,这样就是复制,而不是转移所有权。

fn main() {
    let x = 5;
    let y = x; // 此时x和y都是有效的,因为x是简单数据类型,采用了Copy trait
    println!("x = {}, y = {}", x, y);
    // y释放
    // x释放
}

Copy语义与Move语义

Rust语言中,对于实现了Copy trait的类型,在赋值或者传参时,值会自动按位拷贝;而对于没有实现Copy trait的类型,会采用Move转移所有权的方式来传递数据。

实现了Copy trait的类型:

  • 原生类型:整型(i8,u8,i16,u16,i32,u32,i64,u64,i128,u128,isize,usize)、浮点型(f32,f64)、布尔型、字符型(char)、单元类型()、Never Type(!)。
  • 不可变引用(&T)
  • 函数指针
  • 裸指针(*const T, *mut T)
  • 数组[T;N]、元组(T1, T2, ..., Tn)Option<T>类型(需要注意的是:只有当它们的元素类型都实现了Copy trait时,它们才实现了Copy trait)。

对于复合类型,比如枚举体和结构体,Rust语言默认是不实现Copy trait的,但是如果这些类型的所有成员都实现了Copy trait,那么你可以手动添加#[derive(Copy, Clone)]来实现Copy trait。如果内部结构包含Move语义的类型,那么就无法实现Copy trait。

// 此时因为未实现Copy trait,所以在赋值或者传参时,会采用Move语义
#[derive(Debug)]
struct A {
    a: i32,
    b: u32,
}

fn main() {
    let a = A{a: 1, b: 2};
    let b = a;
    println!("{:?}", a); // 此处会报错,因为a已经不再有效
}
// 我们为struct A添加了Copy trait,这是就是采用Copy语义了
#[allow(dead_code)]
#[derive(Debug, Copy, Clone)]
struct A {
    a: i32,
    b: u32,
}

fn main() {
    let a = A{a: 1, b: 2};
    let _b = a;
    println!("{:?}", a);
}

借用(Borrow)

fn main() {
    let s = String::from("hello");
    print(s);
    // println!("{}", s); // 此处会报错,因为s已经在函数传参时转移了所有权,不再有效
}

fn print(s: String) {
    println!("{}", s);
}

那如果是像上面这种,函数传参后,还需要使用到字符串,没有实现Copy trait,但是又不想转移所有权,这时就需要借用了。Rust语言中,借用分为可变借用和不可变借用,可变借用的语法是&mut,不可变借用的语法是&

fn main() {
    let mut s = String::from("hello");
    print(&s);
    add(&mut s);
    println!("{}", s); // 输出:hello world
}

// 这里不对字符串进行修改,所以用不可变借用
fn print(s: &String) {
    println!("{}", s);
}

// 这里对字符串进行修改,所以用可变借用
fn add(s: &mut String) {
    s.push_str(" world");
}

需要说明的是,这里使用的是借用这个词,而不是引用,因为Rust语言中的引用和其他语言中的引用不一样,Rust语言中的引用是指向某个值的指针,而不是别名。在其他语言中,引用就相当于拿到了值的别名,跟原来的值是同一个东西,可以进行任何无差别访问。

而在Rust语言中的借用都是临时借用使用权,并不破坏单一所有权原则,好比我从你那里借了一本书,但是我肯定不能随随便便在上面乱写乱画,因为书还是归你所有,用完之后就会归返。所以Rust的借用默认是不可变的,如果想要修改引用的值,需要使用可变借用(取得书主人的授权)。

借用的限制

为了保证内存安全,Rust语言中的借用也有一些限制,比如:

  • 在同一作用域中,同一数据只能有一个可变借用。
  • 在同一个作用域中,同一数据可以有多个不可变借用
  • 在一个作用域中,可变借用与不可变借用不能同时存在
  • 所有借用的生命周期不能超出值的生命周期。

前面的三条主要是为了保证内存安全,核心原则就是:共享不可变,可变不共享

最后一条主要是为了防止悬垂指针。

fn main() {
    let s = get_str();
    println!("{}", s);
}

fn get_str<'a>() -> &'a str {
    let s = String::from("hello");
    &s // 返回s的引用
}

上面的例子中,get_str函数返回了s的引用,但是sget_str函数结束之后就会被释放,也就是借用超出了值的生命周期,所以这里是不合法的,编译器会报错。

error[E0515]: cannot return reference to local variable `s`
 --> src/main.rs:8:5
  |
8 |     &s // 返回s的引用
  |     ^^ returns a reference to data owned by the current function

For more information about this error, try `rustc --explain E0515`.

生命周期

上面提到了生命周期,而在Rust中,生命周期是用来解决借用的限制问题的。生命周期是指在程序运行时,某个值的有效范围。生命周期的作用是保证借用的值在借用结束之前有效。

有些值具有静态生命周期,它们会在整个程序运行期间一直有效。比如:

  • 全局变量
  • 静态变量
  • 字符串字面量
  • 函数指针
  • &‘static T类型的引用

而其他的类型,比如局部变量,函数参数,函数返回值等,都是具有动态生命周期的,它们的生命周期是在函数调用时确定的。

生命周期参数

在Rust中,生命周期参数是用来描述引用的生命周期的,它的语法是'a,其中a是生命周期参数的名称,可以是任意的合法标识符。生命周期参数位于引用符号&和引用的类型之间,比如:

&i32 // 引用
&'a i32 // 标注生命周期参数的不可变引用
&'a mut i32 // 标注生命周期参数的可变引用

标注的生命周期参数只是用于编译器的借用检查。

函数签名中的生命周期参数:

fn foo<'a, 'b: 'a> (x: &'a str, y: &'b str) -> &'a str;

其中'b: 'a的意思是生命周期参数'b的生命周期至少要大于等于'a的生命周期,也就是说'b的生命周期不能小于'a的生命周期。

条件限制:

  • 函数签名中输入(出借方)的生命周期长度必须大于等于输出(借出方)的生命周期长度
  • 禁止没有任何输出参数的情况下,返回引用;主要是避免造成悬垂指针。

结构体中的生命周期参数:

struct Foo<'a> {
    x: &'a str,
}

impl <'a> Foo<'a> {
    fn new(s: &'a str) -> Self {
        Foo { x: s }
    }

    fn foo(&self) -> &'a str {
        self.x
    }
}

条件限制:

  • 结构体实例的生命周期应该短于或等于任意一个成员的生命周期

省略生命周期参数规则

Rust编译器会根据一些规则来自动推导生命周期参数,这些规则称为省略生命周期参数规则

  • 每一个引用类型都有独立的生命周期参数,比如'a, 'b
  • 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
  • 如果存在多个输入生命周期参数,但是其中一个参数是&self或者&mut self,那么self的生命周期被赋予所有输出生命周期参数