跳到主要内容

引用与借用(References & Borrowing)

引用(References)允许你使用值但不获取其所有权。借用(Borrowing)是我们获取引用作为函数参数的行为。

引用

引用像一个指针,因为它是一个地址,我们可以由此访问储存于该地址的属于其他变量的数据。与指针不同,引用确保指向某个特定类型的有效值。

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

let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

&s1 语法让我们创建一个指向s1 的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃。

借用

我们将创建一个引用的行为称为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。你并不拥有它。

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

change(&s); // 传递引用
}

fn change(some_string: &String) {
// some_string.push_str(", world"); // 这会编译错误!
}

正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。

可变引用

我们可以通过一个小调整来修复上面代码的错误,允许我们修改一个借用的值,这就是可变引用(mutable reference):

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

change(&mut s);

println!("{}", s); // 输出: hello, world
}

fn change(some_string: &mut String) {
some_string.push_str(", world");
}

可变引用的限制

可变引用有一个很大的限制:如果你有一个对该变量的可变引用,你就不能再创建对该变量的引用。这些尝试创建两个 s 的可变引用的代码会失败:

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

let r1 = &mut s;
// let r2 = &mut s; // 错误!不能同时有两个可变引用

println!("{}", r1);
}

这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:

  1. 两个或更多指针同时访问同一数据
  2. 至少有一个指针被用来写入数据
  3. 没有同步数据访问的机制

作用域与可变引用

我们可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能同时拥有:

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

{
let r1 = &mut s;
r1.push_str(", world");
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

let r2 = &mut s;
r2.push_str("!");

println!("{}", s); // 输出: hello, world!
}

混合可变和不可变引用

Rust 也不允许我们在拥有不可变引用的同时拥有可变引用:

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

let r1 = &s; // 没问题
let r2 = &s; // 没问题
// let r3 = &mut s; // 大问题!

println!("{} and {}", r1, r2);
// 此时 r1 和 r2 不再使用
}

不过,一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次使用不可变引用(println!)发生在声明可变引用之前,所以如下代码是可以编译的:

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

let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此时 r1 和 r2 不再使用

let r3 = &mut s; // 没问题
println!("{}", r3);
}

悬垂引用

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。

相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂引用:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串

&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!

这里的解决方法是直接返回 String

fn main() {
let string = no_dangle();
println!("{}", string);
}

fn no_dangle() -> String {
let s = String::from("hello");

s // 返回 s,所有权被移动出去,没有值被释放
}

引用的规则

让我们概括一下之前对引用的讨论:

  1. 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用
  2. 引用必须总是有效的

实际应用示例

计算字符串长度

fn main() {
let s1 = String::from("hello");
let s2 = String::from("world");

let result = longest(&s1, &s2);
println!("The longest string is {}", result);
}

fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}

修改向量

fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];

add_one(&mut numbers);

println!("{:?}", numbers); // [2, 3, 4, 5, 6]
}

fn add_one(nums: &mut Vec<i32>) {
for num in nums {
*num += 1;
}
}

查找最大值

fn main() {
let numbers = vec![34, 50, 25, 100, 65];

let result = largest(&numbers);
println!("The largest number is {}", result);
}

fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];

for item in list {
if item > largest {
largest = item;
}
}

largest
}

字符串切片

字符串切片(string slice)是 String 中一部分值的引用:

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

let hello = &s[0..5];
let world = &s[6..11];

println!("{} {}", hello, world);
}

字符串切片作为参数

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

// first_word 中传入 `String` 的 slice
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// first_word 也接受 `String` 的引用,
// 这等价于 `String` 的 slice
let word = first_word(&my_string);

let my_string_literal = "hello world";

// first_word 中传入字符串字面值的 slice
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);

// 因为字符串字面值**就是**字符串 slice,
// 这样写也可以,即不使用 slice 语法!
let word = first_word(my_string_literal);
}

fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}

&s[..]
}

其他类型的切片

fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

最佳实践

  1. 优先使用引用而不是所有权转移:除非确实需要获取所有权
  2. 使用字符串切片 &str 作为参数:比 &String 更灵活
  3. 遵循借用规则:避免数据竞争
  4. 使用切片处理集合的部分数据:更高效且安全
// 好的设计:使用引用和切片
fn process_data(data: &[i32]) -> i32 {
data.iter().sum()
}

fn format_name(first: &str, last: &str) -> String {
format!("{} {}", first, last)
}

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let total = process_data(&numbers);

let full_name = format_name("张", "三");

println!("总和: {}, 姓名: {}", total, full_name);
}