引用和借用
Rust 的引用(references)和借用(borrowing)是所有权系统的扩展部分,它们允许你访问数据而不转移所有权。这确保了内存安全,同时避免了不必要的拷贝。借用规则在编译时强制执行,防止数据竞争和无效引用(如悬垂指针)。引用用 &
表示,是指向值的指针,但 Rust 保证它们始终有效。
1. 引用和借用简介
- 引用(&T):一个指向类型 T 的值的指针,不拥有值。
- 借用:创建引用的过程。借用是临时的,作用域结束时结束。
- 为什么使用?:避免移动所有权或昂贵拷贝,同时访问数据。
- 类型:
- 不可变引用:
&T
– 读访问,不能修改。 - 可变引用:
&mut T
– 读写访问,可以修改。
- 不可变引用:
- 解引用:用
*
访问引用的值,如*ref
。
示例:基本引用
fn main() { let x = 5; let y = &x; // y 是 x 的不可变引用 println!("x: {}, y: {}", x, *y); // 输出: x: 5, y: 5 // *y = 10; // 错误!不可变引用不能修改 }
- 解释:y 借用 x,但不拥有。x 仍有效。引用是栈上的指针,指向 x 的位置。
2. 不可变借用
不可变借用允许多个同时存在,因为它们不修改数据。
示例:函数中的不可变借用
fn print_length(s: &String) { // 借用 &String println!("长度: {}", s.len()); } fn main() { let s = String::from("hello"); print_length(&s); // 传递引用 print_length(&s); // 可以多次借用 println!("原值: {}", s); // s 仍拥有所有权 }
- 解释:函数借用 s,不转移所有权。多个 & 借用 OK,因为是只读的。Rust 允许无限个不可变借用。
多引用示例
fn main() { let mut s = String::from("hello"); // mut 不是必需,但这里演示 let r1 = &s; let r2 = &s; println!("r1: {}, r2: {}", r1, r2); // 有效 }
3. 可变借用
可变借用允许修改,但同一时间只能有一个(独占访问)。
示例:可变借用
fn append_world(s: &mut String) { s.push_str(", world!"); } fn main() { let mut s = String::from("hello"); // 必须 mut append_world(&mut s); println!("{}", s); // 输出: hello, world! }
- 解释:
&mut
传递可变引用。借用期间,s 不能被其他方式访问:#![allow(unused)] fn main() { let mut s = String::from("hello"); let r = &mut s; // println!("{}", s); // 错误!不能在可变借用时访问 s // let r2 = &mut s; // 错误!不能有第二个可变借用 r.push_str("!"); }
4. 借用规则
Rust 的借用检查器(borrow checker)强制这些规则:
- 任何值,在给定作用域内,可以有:
- 一个可变引用,或
- 任意多个不可变引用。 但不能同时两者。
- 引用必须始终有效(无悬垂引用)。
- 借用不能超过所有者的生命周期。
示例:借用冲突
fn main() { let mut s = String::from("hello"); let r1 = &s; // 不可变 let r2 = &s; // 另一个不可变 OK // let r3 = &mut s; // 错误!有不可变借用时不能可变借用 println!("{}, {}", r1, r2); }
- 解释:规则防止数据竞争。如果允许同时 & 和 &mut,修改可能使不可变引用失效。
5. 悬垂引用(Dangling References)
Rust 防止返回局部变量的引用,因为所有者超出作用域会导致引用悬垂。
示例:悬垂引用错误
#![allow(unused)] fn main() { // fn dangle() -> &String { // 错误!返回局部引用 // let s = String::from("hello"); // &s // } // s drop,引用无效 }
- 解释:编译错误:missing lifetime specifier。Rust 要求指定生命周期(详见生命周期教程)。正确方式:返回所有权或用静态生命周期。
正确示例:静态引用
#![allow(unused)] fn main() { fn static_ref() -> &'static str { "hello" // 字符串字面量是 'static } }
6. 引用与切片
切片是引用的子集,如数组或字符串的部分。
示例:字符串切片
fn first_word(s: &str) -> &str { // &str 是 String 或 str 的借用 let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let s = String::from("hello world"); let word = first_word(&s); // 借用 println!("{}", word); // 输出: hello // s.clear(); // 错误!word 借用期间不能修改 s }
- 解释:切片借用规则相同。修改 s 会使 word 失效,但借用 checker 防止它。
7. 引用与所有权的交互
- 借用后不能移动:借用存在时,所有者不能被移动。
- 解引用强制:某些操作需要
*
,但方法调用隐式解引用(deref coercion)。 - Deref trait:自定义类型可以实现 Deref 以像引用一样行为(如 smart pointers)。
示例:Deref 强制
fn main() { let s = String::from("hello"); let r = &s; println!("{}", r.len()); // 隐式 *r.len() }
8. 最佳实践和常见陷阱
- 优先借用:避免 clone,除非必要。
- 最小借用作用域:用 {} 限制借用,早释放锁。
- mut 只在必要时:减少可变借用以允许更多不可变访问。
- 常见错误:
- 借用冲突:在循环中借用集合同时修改(用索引或迭代器代替)。
- 悬垂引用:返回函数局部引用(用所有权返回或生命周期)。
- 未 mut 变量:借用 &mut 时,所有者必须 mut。
- 与生命周期结合:复杂借用需生命周期注解(如 'a)。
- 性能:引用是零成本,编译时检查无运行时开销。
练习建议
- 编写函数,接收 &Vec
,返回最大值的引用。 - 创建 struct,用 &mut 修改其字段。
- 尝试切片数组,处理边界借用冲突。
如果需要更多示例、与生命周期的集成,或特定场景的调试,请提供细节!