Rust 学习(7):所有权系统(下)-引用与借用
文章目录
- 一、回顾与导入
- 二、引用的基本概念
- 2.1 不可变引用(Immutable Reference)
- 2.2 引用 vs 解引用
- 三、可变引用(Mutable Reference)
- 四、借用规则(Borrowing Rules)
- 规则 1:可变引用在同一作用域内只能有一个
- 规则 2:不可变引用可以有多个,但不能与可变引用共存
- 规则 3:引用的作用域持续到最后一次使用
- 表格总结
- 五、悬垂引用(Dangling References)
- 六、引用的类型总结
- 七、完整代码示例
- 八、常见陷阱与最佳实践
- 陷阱 1:误用 `&` 和 `&mut`
- 陷阱 2:在循环中创建可变引用
- 最佳实践
- 九、总结与速查
- 十、思考题
- 参考链接
返回首页 | 上篇 | 下篇
掌握引用的使用 —— 借用、可变引用、悬垂引用与借用规则
一、回顾与导入
上篇文章我们学习了所有权的核心规则:
- 每个值有唯一所有者
- 赋值/传参会转移所有权(移动语义)
- 离开作用域自动释放内存
但这带来了一个问题:如果我只是想读取一个值,而不想获取它的所有权,难道每次都要传入再返回吗?太繁琐了!
// 上篇的繁琐写法fnmain(){lets1=String::from("hello");let(s2,len)=calculate_length(s1);println!("'{}'的长度是 {}",s2,len);}fncalculate_length(s:String)->(String,usize){letlength=s.len();(s,length)}Rust 提供了优雅的解决方案:引用(Reference)和借用(Borrowing)。
二、引用的基本概念
引用允许你使用值但不获取所有权,就像看一本书时可以借阅而不需要买下来。
2.1 不可变引用(Immutable Reference)
使用&符号创建引用:
fnmain(){lets1=String::from("hello");letlen=calculate_length(&s1);// &s1 创建对 s1 的引用println!("'{}'的长度是 {}",s1,len);// s1 仍然可用!}fncalculate_length(s:&String)->usize{s.len()// 使用引用访问值}// s 是引用,离开作用域不会 drop 原值内存模型:
s1 (栈) 堆 +---------+--------+----------+ +---------+---------+---------+---------+---------+ | ptr | len | capacity | | h | e | l | l | o | +---------+--------+----------+ +---------+---------+---------+---------+---------+ ↑ | &s1 (栈) +---------+ | ptr | → 指向 s1 +---------+关键点:
- 引用不会获取所有权
- 原变量
s1仍然拥有数据 - 引用离开作用域不会释放数据
示例:
letx=42;// x 在栈上letr=&x;// r 在栈上,存的是 x 的地址(指向栈)letb=Box::new(42);// 堆上存 42,栈上存 b(指向堆)letr=&b;// r 在栈上,存的是 b 的地址(指向栈)// 间接指向堆上的 42// 更直接:letv=vec![1,2,3];// 数据在堆上letr=&v[0];// r 指向堆上的第一个元素内存示意图:
栈地址 内容 0x1000: x = 42 0x1008: r = 0x1000 (指向 x) 栈地址 内容 0x2000: b = 0x5000 (Box,指向堆) 0x2008: r = 0x2000 (指向 b) 堆地址 内容 0x5000: 422.2 引用 vs 解引用
| 概念 | 符号 | 含义 |
|---|---|---|
| 创建引用 | &value | 创建对值的引用 |
| 解引用 | *ref | 获取引用指向的值 |
fnmain(){letx=5;lety=&x;// y 是引用assert_eq!(5,x);assert_eq!(5,*y);// *y 解引用取得值}三、可变引用(Mutable Reference)
如果需要修改引用的值,可以使用可变引用&mut。
fnmain(){letmuts=String::from("hello");change(&muts);// 传递可变引用println!("{}",s);// hello, world}fnchange(some_string:&mutString){some_string.push_str(", world");}关键规则:
- 原变量必须是
mut声明的 - 引用类型必须是
&mut - 可变引用有严格的使用限制(见下文)
四、借用规则(Borrowing Rules)
Rust 编译器对引用的使用施加了严格的规则,确保内存安全。
规则 1:可变引用在同一作用域内只能有一个
fnmain(){letmuts=String::from("hello");letr1=&muts;letr2=&muts;// ❌ 编译错误:不能同时存在两个可变引用println!("{}, {}",r1,r2);}为什么?防止数据竞争(Data Race)。
规则 2:不可变引用可以有多个,但不能与可变引用共存
fnmain(){letmuts=String::from("hello");letr1=&s;// 不可变引用letr2=&s;// 不可变引用(多个没问题)letr3=&muts;// ❌ 编译错误:已有不可变引用时不能有可变引用println!("{}, {}",r1,r2);}因为:不可变引用希望数据不被修改,可变引用允许修改,两者同时存在会造成冲突。
规则 3:引用的作用域持续到最后一次使用
fnmain(){letmuts=String::from("hello");letr1=&s;// 不可变引用开始letr2=&s;// 不可变引用开始println!("{} {}",r1,r2);// 不可变引用最后使用// r1, r2 的作用域到此结束letr3=&muts;// ✅ 可以,因为前面的不可变引用已失效println!("{}",r3);}表格总结
| 场景 | 是否允许 | 说明 |
|---|---|---|
| 多个不可变引用 | ✅ | 同时读取是安全的 |
| 一个可变引用 | ✅ | 唯一写入权 |
| 可变引用 + 不可变引用共存 | ❌ | 读写冲突 |
| 多个可变引用 | ❌ | 写入冲突 |
五、悬垂引用(Dangling References)
悬垂引用指向的内存已经被释放。Rust 编译器会在编译期阻止悬垂引用的产生。
fnmain(){letreference_to_nothing=dangle();}fndangle()->&String{// ❌ 编译错误lets=String::from("hello");&s// 返回 s 的引用}// s 离开作用域,被 drop,引用指向已释放的内存错误信息:
error[E0106]: missing lifetime specifier --> 5 | fn dangle() -> &String { | ^ expected named lifetime parameter解决方案:返回String本身(转移所有权)而不是引用:
fnno_dangle()->String{lets=String::from("hello");s// 所有权转移出去}六、引用的类型总结
| 引用类型 | 语法 | 能否修改原值 | 能否复制 | 限制 |
|---|---|---|---|---|
| 不可变引用 | &T | ❌ | ✅(引用本身可复制) | 可以有多个 |
| 可变引用 | &mut T | ✅ | ❌ | 同一时刻只能有一个 |
| 裸指针 | *const T/*mut T | 视情况 | ✅ | 不安全,需 unsafe |
七、完整代码示例
fnmain(){println!("=== 1. 不可变引用 ===");lets1=String::from("Rust");letlen=get_length(&s1);println!("'{}' 长度 = {}, s1 仍然可用: {}",s1,len,s1);println!("\n=== 2. 可变引用 ===");letmuts2=String::from("Hello");println!("修改前: {}",s2);append_world(&muts2);println!("修改后: {}",s2);println!("\n=== 3. 多个不可变引用 ===");lets3=String::from("Multiple");letr1=&s3;letr2=&s3;letr3=&s3;println!("三个引用指向: {}, {}, {}",r1,r2,r3);println!("\n=== 4. 可变引用不可共存示例 ===");letmuts4=String::from("test");letr1=&s4;letr2=&s4;println!("r1 = {}, r2 = {}",r1,r2);// r1 和 r2 在这之后不再使用letr3=&muts4;// ✅ 可以,因为前面的引用不再使用r3.push_str("_mutated");println!("r3 = {}",r3);println!("\n=== 5. 解引用 ===");letx=42;lety=&x;println!("x = {}, y = {}, *y = {}",x,y,*y);}fnget_length(s:&String)->usize{s.len()// 通过引用访问}fnappend_world(s:&mutString){s.push_str(", world!");// 通过可变引用修改}println!是宏,它只是读取 s2,并没有转移所有权。
println! 宏会自动对参数取引用,等价于:
println!("{}",&s2);// 传递的是 &String,不是 String八、常见陷阱与最佳实践
陷阱 1:误用&和&mut
fnmain(){letmuts=String::from("hello");letr=&s;// 不可变引用// r.push_str("!"); // ❌ 不可变引用不能修改letm=&muts;// 可变引用m.push_str("!");}陷阱 2:在循环中创建可变引用
fnmain(){letmutv=vec![1,2,3];// ❌ 编译错误:不能同时在循环中持有可变引用自己// for item in &mut v {// v.push(*item + 1);// }// ✅ 正确做法:先收集,再修改letnew_elements:Vec<i32>=v.iter().map(|x|x+1).collect();v.extend(new_elements);}最佳实践
| 场景 | 推荐做法 |
|---|---|
| 只需要读取数据 | 使用&T不可变引用 |
| 需要修改数据 | 使用&mut T可变引用 |
| 需要同时读取多个地方 | 使用多个&T |
| 需要修改且无关顺序 | 使用&mut T,注意作用域 |
| 不确定用哪种 | 优先用不可变引用 |
九、总结与速查
| 概念 | 语法 | 特性 |
|---|---|---|
| 引用 | &value | 不获取所有权 |
| 可变引用 | &mut value | 可以修改原值 |
| 解引用 | *ref | 获取引用指向的值 |
| 借用 | 使用引用的行为 | 遵循借用规则 |
借用规则回顾:
- 可变引用在同一作用域内只能有一个
- 可变引用和不可变引用不能共存
- 引用必须在有效期内(不能悬垂)
核心要点:
- ✅ 引用让函数可以安全地使用值而不获取所有权
- ✅ 默认使用不可变引用(
&T),需要修改时使用&mut T - ✅ 编译器严格执行借用规则,防止数据竞争
- ✅ 引用的作用域是从创建到最后一次使用
十、思考题
- 以下代码能编译通过吗?为什么?
fnmain(){letmuts=String::from("hello");letr1=&s;letr2=&s;letr3=&muts;println!("{}, {}, {}",r1,r2,r3);}- 以下代码的输出是什么?
fnmain(){letmutx=10;lety=&mutx;*y+=5;println!("{}",x);}- 为什么 Rust 不允许同时有可变引用和不可变引用?
参考链接
- Rust Book - References and Borrowing
- Rust Reference - References
- Rust by Example - Borrowing
返回首页 | 上篇 | 下篇
