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

什么是引用

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

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s1 = String::from("Hello");
print_name(s1);

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

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

下面是用引用作为参数

1
2
3
4
5
6
7
8
9
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. 在重叠的作用域中,可以有多个不可变引用,或者,只能有一个可变引用,但是可变与不可变引用,在重叠的作用域内不能共存

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

1
2
3
4
5
fn main() {
let mut s = String::from("Hello");
let r1 = &s;
let r2 = &s;
}

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

1
2
3
4
5
6
7
8
fn main() {
let mut s = String::from("Hello");
{
let r1 = &mut s;
}

let r2 = &mut s;
}

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

1
2
3
4
5
6
7
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 作用域重叠
}

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

1
2
3
4
5
6
7
8
9
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 中,编译时就会将错误暴露出来

1
2
3
4
5
6
7
8
9
10
11
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 的完整引用,如果只想获取字符串内容中的部分引用,看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 类型就是一个字符串切片类型,注意,字符串切片也就是对于字符串的引用,所以引用规则也要遵从上面提到的规则。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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..];
}
}

""
}

对于数组的切片

1
2
3
4
5
6
7
8
fn main() {
let scores = [100, 97, 45, 60, 88];
let score_slice = &scores[1..3];

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

}