在Rust中,引用(Reference)允许你引用某个值而不获取其所有权。将创建一个引用的行为称为借用(Borrowing)。
回顾所有权转移的问题:
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); // println!("{}", s1); // 错误!s1已被移动 println!("'{}'的长度是{}", s2, len); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); (s, length) }
这段代码很笨拙。使用引用可以优雅地解决:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // 传递引用 println!("'{}'的长度是{}", s1, len); // s1仍然有效 } fn calculate_length(s: &String) -> usize { s.len() } // s离开作用域,但它没有所有权,所以不释放内存
图示:
&String s 指向 String s1 +--------+ +---------+ | 指针 | ---> | hello | | 长度 5 | | 容量 5 | +--------+ +---------+
Rust对引用有严格的规则,在编译期确保内存安全。
基本规则:
1. 在任一给定时刻,只能满足下列条件之一: * 一个可变引用 * 任意数量的不可变引用 2. 引用必须总是有效的(不能悬垂)
let s = String::from("hello"); let r = &s; // 创建不可变引用 println!("{}", r); // 通过引用访问值
可以同时存在多个不可变引用:
let s = String::from("hello"); let r1 = &s; let r2 = &s; let r3 = &s; println!("{}, {}, {}", r1, r2, r3); // 全部有效
不可变引用不能修改数据:
let s = String::from("hello"); let r = &s; // r.push_str(", world"); // 错误!不可变引用不能修改数据
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"); }
注意:
同一作用域只能有一个可变引用:
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; // 错误!不能同时有两个可变引用 println!("{}, {}", r1, r2);
这个限制的好处: 在编译期防止数据竞争(Data Race)。
数据竞争的条件:
1. 两个或更多指针同时访问同一数据 2. 至少有一个用于写入 3. 没有同步访问机制
let mut s = String::from("hello"); { let r1 = &mut s; println!("{}", r1); } // r1在这里离开作用域 let r2 = &mut s; // 现在可以了 println!("{}", r2);
不能同时存在可变和不可变引用:
let mut s = String::from("hello"); let r1 = &s; // 没问题 let r2 = &s; // 没问题 let r3 = &mut s; // 大问题! println!("{}, {}, {}", r1, r2, r3);
原因: 不可变引用的用户不希望数据突然改变!
但可以在不可变引用不再使用后创建可变引用:
let mut s = String::from("hello"); let r1 = &s; let r2 = &s; println!("{} 和 {}", r1, r2); // r1和r2在这里之后不再使用 let r3 = &mut s; // 现在可以了 println!("{}", r3);
Rust编译器使用非词法生命周期(Non-Lexical Lifetimes, NLL),可以检测到r1和r2在println!后就不再使用。
悬垂指针(Dangling Pointer)是指向已经释放内存的指针。Rust编译器保证永远不会产生悬垂引用。
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s // 返回对s的引用 } // s在这里离开作用域,内存被释放 // 返回的引用指向已释放内存!
编译器错误:
error[E0106]: missing lifetime specifier
fn no_dangle() -> String { let s = String::from("hello"); s // 转移所有权 }
引用本质上是受约束的指针:
引用的大小:
use std::mem; fn main() { println!("&i32大小:{}", mem::size_of::<&i32>()); println!("&String大小:{}", mem::size_of::<&String>()); println!("&[i32]大小:{}", mem::size_of::<&[i32]>()); } // 在64位系统上,引用都是8字节
Rust在某些情况下会自动解引用:
let s = String::from("hello"); let r = &s; // 这两种写法等价 println!("长度:{}", r.len()); // 自动解引用 println!("长度:{}", (*r).len()); // 显式解引用
优先使用引用避免所有权转移:
// 好的做法:使用引用 fn print_length(s: &String) { println!("长度:{}", s.len()); } // 不好的做法:转移所有权 fn print_length_bad(s: String) { println!("长度:{}", s.len()); } // s在这里被释放,调用者不能再使用
API设计原则:
// 只读访问 -> &T fn inspect(s: &String) { println!("{}", s); } // 需要修改 -> &mut T fn append_world(s: &mut String) { s.push_str(" world"); } // 需要获取所有权 -> T fn take_ownership(s: String) { println!("{} 被获取", s); } // s在这里被释放
let x = 5; let y = &x; // &i32 let z = &y; // &&i32 println!("z = {}", **z); // 需要两次解引用
let mut x = 5; let y = &mut x; // &mut i32 let z = &mut *y; // 通过y重新借用 *z = 10; println!("x = {}", x);
let x = Some(5); match &x { Some(val) => println!("值:{}", val), None => println!("无值"), }
let x = 5; let y = &x; // 等价于 let ref z = x; println!("z = {}", *z);
在模式匹配中使用:
let mut v = vec![1, 2, 3]; match v.get_mut(0) { Some(ref mut val) => *val = 10, None => (), } println!("{:?}", v); // [10, 2, 3]
fn swap(a: &mut i32, b: &mut i32) { let temp = *a; *a = *b; *b = temp; } fn main() { let mut x = 5; let mut y = 10; println!("交换前:x = {}, y = {}", x, y); swap(&mut x, &mut y); println!("交换后:x = {}, y = {}", x, y); }
fn reverse(s: &mut String) { let chars: Vec<char> = s.chars().collect(); let reversed: String = chars.into_iter().rev().collect(); *s = reversed; } fn main() { let mut s = String::from("hello"); println!("原字符串:{}", s); reverse(&mut s); println!("反转后:{}", s); }
fn find_max(arr: &[i32]) -> Option<&i32> { if arr.is_empty() { return None; } let mut max = &arr[0]; for item in arr.iter() { if item > max { max = item; } } Some(max) } fn main() { let numbers = [3, 1, 4, 1, 5, 9, 2, 6]; match find_max(&numbers) { Some(max) => println!("最大值是:{}", max), None => println!("数组为空"), } }
下面代码有编译错误,请修复:
fn main() { let mut s = String::from("hello"); let r1 = &s; let r2 = &mut s; println!("{}, {}", r1, r2); }
解决方案:
fn main() { let mut s = String::from("hello"); let r1 = &s; println!("{}", r1); // r1在这里之后不再使用 let r2 = &mut s; println!("{}", r2); }
本章深入讲解了Rust的引用与借用机制:
理解引用与借用是掌握Rust的关键,这是Rust内存安全保证的核心机制。