在阅读Rust官方教程时,会看到两个词,引用和借用,也就是 References and Borrowing。这里很容易让人混乱,如果了解C/C++这类有指针的语言,则引用很容易理解,但是 Rust 中的借用这个词是什么意思呢?我觉得,在初学 Rust 时,可以忽略这个词,或者就简单理解为,它所涉及到的东西,就是引用,就是一个指针,就可以了,避免陷入进去。所以,接下来我就就聊一聊引用。

什么是引用

简单来说,引用就是一个指针,这个指针指向了某个内存地址。在说所有权时,我们知道,当把一个 String 当作参数传到函数时,它的所有权也就会被移动到函数的参数上,如果在调用完函数时,我们依旧想使用这个 String,则需要将所有权再返回,这样就很麻烦,所以用引用,会方便很多,因为引用,并不会获得这个 String 的所有权。看下面的代码

fn main() {
    let s1 = String::from("Hello");
    print_name(s1);
    
    // 下面这一句再访问 s1 就会编译出错,因为 s1 的所有权已经没了
    //println!("s1 again: {}", s1);
}

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

下面是用引用作为参数

fn main() {
    let s1 = String::from("Hello");
    print_name(s1);
    println!("s1 again: {}", s1);
}

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

在上面的代码中,我们将参数从 String 改为 &String,这样函数的参数需要的就不是 String 的所有权,而是 String 的引用,所以在函数 print_name 结束时,main 函数中依然可以使用 name。下图是使用引用时的数据状态。

p003101_ref-pointer

可变引用

上面的代码中的引用是不可变的,现在我们来说一下可变引用,首先,有几条规则要记住。

  1. 要使用可变引用,首先原始数据要是可变的 (Copy类型除外)
  2. 在重叠的作用域中,可以有多个不可变引用,或者,只能有一个可变引用,但是可变与不可变引用,在重叠的作用域内不能共存

下面的代码没问题,重叠的作用域内有多个不可变引用

fn main() {
    let mut s = String::from("Hello");
    let r1 = &s;
    let r2 = &s;
}

下面的代码也没问题,作用域不重叠

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

    let r2 = &mut s;
}

下面的代码有问题,在重叠的作用域内同时存在可变和不可变引用

fn main() {
    let mut s = String::from("Hello");
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;
    println!("{}, {}, {}", r1, r2, r3); // 因为这里访问了 r1 r2,所以 r1 r2 和 r3 作用域重叠
}

如果改成下面这样,就没有问题了,作用域不重叠

fn main() {
    let mut s = String::from("Hello");
    let r1 = &s;
    let r2 = &s;
    println!("{} {}", r1, r2);  // 当前代码中,r1 r2 的作用域到此就结束了,因为后面没有再访问 r1 r2

    let r3 = &mut s;
    println!("{}", r3);
}

什么是悬垂指针

一个指针所指向的内存已经被释放,但是还在使用这个指针,那这个指针就成为了悬垂指针,在其他编程语言中,编译时没问题,但是 Rust 中,编译时就会将错误暴露出来

fn main() {
    let name = get_name();
}

fn get_name() -> &String {
    let new_name = String::from("Fred");

     // 这里会报错噢,因为在函数结束时,new_name 就会被释放,
     // &new_name 会成为一个悬垂指针
    &new_name  
}

什么是切片

切片,可以理解为对一个数据的部分引用,例如 String,之前 &String 是获取一个 String 的完整引用,如果只想获取字符串内容中的部分引用,看下面的代码

fn main() {
    let s1 = String::from("HelloRust");

    // 获取 s1 [0, 5) 这个索引区间内的字符串引用,也就是 Hello
    // 注意不包括索引为 5 这个字符
    let slice1 = &s1[0..5];

    // 获取 s1 [0, 5] 这个索引区间的字符串引用,也就是 HelloR
    // 这个是包含结尾索引 5
    let slice2 = &s1[0..=5];

    // 如果要从0开始,则可以省略开头索引
    let slice3 = &s1[..5];      // Hello
    let slice4 = &s1[..=5];     // HelloR

    // 如果要截取到结尾,则可以省略结尾索引
    let slice5 = &s1[5..];  // Rust
}

在 Rust 中 &str 类型就是一个字符串切片类型,注意,字符串切片也就是对于字符串的引用,所以引用规则也要遵从上面提到的规则。

下面代码获取一个文件名的扩展名(仅做示例,不是生产代码)

fn main() {
    let file_name = String::from("TheRustBook.pdf");
    let extension = get_extension(&file_name);

    // .pdf
    println!("Extension: {}", extension);
}

fn get_extension(file_name: &String) -> &str {
    for(i, c) in file_name.chars().enumerate() {
        if c == '.' {
            return &file_name[i..];
        }
    }

    ""
}

对于数组的切片

fn main() {
    let scores = [100, 97, 45, 60, 88];
    let score_slice = &scores[1..3];

    // [97, 45]
    println!("{:?}", score_slice);

}