头图

1. 引用

很多场景中,我们可能只是想读取某个变量指向的值,并不想获得其所有权,这个时候就可以使用引用。其实在很多其他的编程语言中,也有引用的概念。

  • 简单来讲,引用是创建一个变量,指向另个指针的地址,而不是直接指向 该指针指向的堆内存地址
  • 通过 & 取地址符获取对一个指针变量的引用

例如在 Rust 中,我们这样创建引用:

let s1 = String::from("hello");

// 获取 s1 的引用
let s = &s1;
  • 下图就很好的表示了 引用 的关系

    • 变量s1是栈内存中的一个指针地址,通过 ptr 记录了存储于堆内存中的 String("hello") 的地址
    • 变量s也存在栈内存中,通过 ptr 记录了 s1 的指针地址,来实现对 String("hello") 的引用

截屏2022-02-04 上午11.29.19.png

2. 借用

使用引用作为函数参数的行为被称作借用,如何使用借用来规避某个变量的所有权发生移动,我们可以看以下例子:

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

    // 使用 s 的引用作为入参,s 的所有权就不会发生移动
    let len = get_length(&s);

    println!("the length of {} is {}", s, len); // the length of hello! is 6
}

fn get_length(string: &String) -> usize {
    // 引用和变量一样,默认也是不可变的
    // string.push_str("world"); // `string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

    string.len()
}

3.可变引用

从上面的例子我们可以知道,引用和变量一样,默认也是不可变的,我们不能修改引用所指向的值。但如果想要这么做,那么可以使用 mut 关键字,将引用转为可变的:

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

    /*
     * 使用 mut 关键字,将 s 的可变引用作为入参,
     * 这样 s 的所有权既不会发生移动,函数中也能通过 可变引用 来修改 s 的值
     */
    let len = get_length(&mut s);
    
    // 在 get_length 函数中我们实现了对 s 的修改
    println!("the length of {} is {}", s, len); // the length of hello!world is 11
}

fn get_length(string: &mut String) -> usize {
    // 通过可变引用对值进行修改
    string.push_str("world");
    string.len()
}

3.1 可变引用的重要限制

  • 对应一个指针变量,在一个特定的作用域内,只能有一个可变引用。原因也很好理解,如果在一块作用域内,当一个变量存在两个可变引用,那就意味着同一时间可能有两个变量控制着同一块内存空间,就会发生数据竞争,很容易在运行时产生bug,因此 Rust 通过编译时检查,来规避这样的问题出现

3.1.1 同一作用域,只能存在一个可变引用

通过例子可以看到,当我们对 变量origin 进行了两次可变引用,编译时就直接报错

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

    let ref1 = &mut origin;
    let ref2 = &mut origin; // error: cannot borrow `origin` as mutable more than once at a time

    println!("{}, {}", ref1, ref2);
}

3.1.2 不可变引用可以有多个

如果是同时使用多个不可变引用,则不会有这个限制,因为不可变引用其实就是只读的,不存在可能的内存安全风险。通过这个例子我们可以看到,Rust是允许这样使用的:

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

    let ref1 = &origin;
    let ref2 = &origin;

    println!("{}, {}", ref1, ref2);
}

3.1.3 某些场景下,可以存在多个可变引用

从上面的两个例子我们已经知道,如果一个作用域内同时存在两个以上的可变引用,那么就可能发生数据竞争,那么是否存在某些场景,会出现多个可变引用呢?我们看下面这个例子:

  • 其实在程序执行过程中,只要走出作用域,那么作用域中的变量就会被释放,这个例子中当声明 ref2 时,ref1 已经被销毁了,所以还是可以保证可变引用的第一条原则

    fn main() {
      let mut origin = String::from("hello");
    
      {
          let ref1 = &mut origin;
          
          println!("{}", ref1);
      }
    
      // 当 ref2 声明时,ref1 已经被销毁了
      let ref2 = &mut origin;
    
      println!("{}", ref2);
    }

3.1.4 不能同时拥有一个可变引用和一个不可变引用

对于一个指针变量,它的可变引用和不可变引用其实是互斥的,不能同时存在。原因很简单,可变引用可以修改指向内存空间的值,当值被修改,不可变引用的意义也就不存在了。因此 Rust 在编译时会进行检查,发现这种情况则会直接报错:

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

    let ref1 = &origin;
    let ref2 = &mut origin;// cannot borrow `origin` as mutable because it is also borrowed as immutable

    println!("{} {}", ref1, ref2);
}

4. 悬垂引用

这一种出现在 C/C++ 等语言中的 bug 场景,描述的是这样一种场景,一个变量所指向的内存空间已经被释放或分配给其他程序使用,但这个变量任然有效。在 Rust 中,编译器会检查这种场景,来避免出现悬垂引用。假设编译通过,下面就是一个会产生悬垂引用的场景:

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

    // s 的所有权被移动到 helper 的作用域中
    let a = helper(s);

    /*
     * 假设编译通过:
     * 当 helper 调用完毕,s 指向的内存空间就被释放了,但在 helper 中返回了 s 的引用,
     * 其实这个引用已经失效,这时 变量a 就变为了一个 悬垂引用
     */
}

fn helper(s: String) -> &String {
    println!("{}", s);
    &s // error: missing lifetime specifier
}

但其实在编译时,Rust就不会允许 helper 返回 s 的引用


木木剑光
2.2k 声望2k 粉丝

Ant Design 社区贡献者