Rust 速成课/引用与借用

这节课要讨论的内容是引用与借用, 这是 Rust 中的另一个关键概念. 从它的表现形式来看, 它很类似其它语言中函数调用时的传值和传引用. 事不宜迟, 让我们马上进入课程吧!

引用

设想一下如下场景, 我们要定义一个函数, 该函数接收一个字符串参数, 然后它将字符串逆序返回. 这时候, 你想将原字符串和倒序后的字符串打印出来...似乎很容易, 可事实真的如此吗?

fn reverse_string(s: String) -> String {
    s.chars().rev().collect()
}

fn main() {
    let s1 = String::from("any string");
    let s2 = reverse_string(s1);
    println!("{} {}", s1, s2);
}

但是当我们试图编译这份代码时, 却遇到了如下的编译错误

println!("{} {}", s1, s2);
                  ^^ value borrowed here after move

这是一个所有权的问题, 当我们分析代码, 会发现字符串的所有权, 已经被转移到了函数内部, 因此, 我们无法在外部打印出 s1. 我们有一些解决方法, 第一种方法是函数在结束之前返回所有权, 也就是将原字符串和逆序后的字符串一起返回. 我们可以改写一下代码:

fn reverse_string(s: String) -> (String, String) {
    (s, s.chars().rev().collect())
}

这可以正常工作, 但不是很方便. 在平时的编程工作中, 我们经常需要将一个变量传递到函数, 然后在函数返回后继续使用. 我们要做的正确的方法是使用引用. 很多语言, 尤其是高级语言都模糊了值类型和引用类型的区别, 但幸运的是 Rust 没有这么干, 我们来看下如何在 Rust 中使用引用. 基本上你要做的, 是在类型和变量前面加上取地址符, 就像下面这样.

fn reverse_string(s: &String) -> String {
    s.chars().rev().collect()
}

fn main() {
    let s1 = String::from("any string");
    let s2 = reverse_string(&s1);
    println!("{} {}", s1, s2);
}

我们继续编译, 它应该能很欢快的运行. 我们创建了一个引用 s1 的值的函数, 但该函数不拥有 s1, 所以当函数结束时, 它所指向的值将不会被删除. 总的来说的话, 如果你曾经写过 C 和 C++ 的话, 会很熟悉这种语法. Rust 中引用的基本用法就是这样了.

可变引用

有时候, 我们可能会想在函数中修改参数. 有必要再次提醒你, Rust 中的所有变量默认都是不可变变量. 因此对应的, 所有不可变变量的引用也都是不可变引用. 那有没有可变引用呢? 答案是有的. 为了容易理解, 我又虚构了一个使用场景. 现在我有一个函数, 它接收一个字符串参数, 然后在原字符串的基础上添加一个字符串后缀.

字符串有个方法叫做 push_str, 它的作用就是在字符串尾巴部分加上新的数据. 我们尝试编写这个函数. 我们加上了 mut 声明, mut 的位置在取地址符之后, 变量名之前, 注意不要搞错.

fn append_string(s: &mut String) {
    s.push_str("!");
}

fn main() {
    let mut s = String::from("any string");
    append_string(&mut s);
    println!("{}", s);
}

编译并运行它, 你将能看到字符串如愿以偿的带上了感叹号.

any string!

另外, 对于可变引用来说, 任何变量一次只能有一个可变引用. 我不会深入去讲这个问题, 因为它和多线程相关. 如果一个变量允许两个及以上的引用, 可能会存在数据竞争问题. 也就是, 多个线程同时去修改一块内存. 这是一个相当经典的 Bug, Rust 试图在编译器层面禁止这种 Bug 出现.

我们的 Rust 理论课程已经结束了, 你能坚持到这里, 很不容易, Rust 真的是一门不太讨喜的语言, 它太难了. 但一切还没有结束, 我们的最后一节课是实践课, 我会在实践课中向你介绍一些 Rust 更加高深的知识.