借用与引用
借用(Borrowing)是 Rust 中在不获取所有权的情况下访问数据的机制。
引用
引用允许你引用某个值而不取得其所有权:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 借用 s1
println!("'{}' 的长度是 {}", s1, len); // ✅ s1 仍然有效
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s 离开作用域,但不释放数据(因为没有所有权)
&s1 创建一个指向 s1 的引用,但不拥有它。
Loading diagram...
引用不可变
默认情况下,引用是不可变的:
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
// some_string.push_str(", world"); // ❌ 编译错误:不能修改引用的值
}
可变引用
使用 &mut 创建可变引用:
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"); // ✅ 可以修改
}
可变引用的限制
同一时间,同一作用域内,只能有一个可变引用:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // ❌ 编译错误:不能同时有两个可变引用
println!("{}", r1);
}
好处:防止数据竞争(data race)。
不能同时存在可变和不可变引用
fn main() {
let mut s = String::from("hello");
let r1 = &s; // ✅ 不可变引用
let r2 = &s; // ✅ 多个不可变引用是允许的
// let r3 = &mut s; // ❌ 不能在存在不可变引用时创建可变引用
println!("{} and {}", r1, r2);
}
借用检查器可视化
引用的作用域
引用的作用域从声明开始,持续到最后一次使用:
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);
}
这个特性叫做非词法作用域生命周期(Non-Lexical Lifetimes, NLL)。
悬垂引用
Rust 编译器保证引用永远不会成为悬垂引用(dangling reference):
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // ❌ 编译错误
let s = String::from("hello");
&s
} // s 离开作用域被释放,返回的引用指向无效内存
修复方法:返回所有权而不是引用:
fn main() {
let string = no_dangle();
}
fn no_dangle() -> String { // ✅ 正确
let s = String::from("hello");
s // 移动所有权
}
借用规则总结
Loading diagram...
三大规则:
- 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用
- 引用必须总是有效的(不能悬垂)
- 引用的生命周期不能超过被引用数据的生命周期
引用与解引用
使用 * 解引用:
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y); // 解引用 y 获取值
}
自动解引用:
Rust 在某些情况下会自动解引用(如方法调用):
fn main() {
let s = String::from("hello");
let len = s.len(); // 自动解引用
let r = &s;
let len = r.len(); // 自动解引用 r,调用 String 的 len 方法
}
多重引用
可以创建引用的引用:
fn main() {
let x = 5;
let y = &x; // &i32
let z = &y; // &&i32
assert_eq!(5, **z); // 两次解引用
}
切片(Slice)
切片是一种特殊的引用,引用集合的连续序列:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
// 简写
let hello = &s[..5]; // 等同于 &s[0..5]
let world = &s[6..]; // 等同于 &s[6..11]
let whole = &s[..]; // 整个字符串
println!("{}", hello);
}
字符串字面量就是切片:
fn main() {
let s: &str = "Hello, world!"; // &str 是字符串切片类型
}
数组切片:
fn main() {
let a = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3]; // [2, 3]
assert_eq!(slice, &[2, 3]);
}
实践示例
示例 1:安全的字符串分割
fn first_word(s: &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 mut s = String::from("hello world");
let word = first_word(&s); // 不可变借用
// s.clear(); // ❌ 编译错误:不能在存在不可变借用时修改
println!("第一个单词: {}", word);
}
示例 2:可变引用传递
fn append_exclamation(s: &mut String) {
s.push('!');
}
fn main() {
let mut greeting = String::from("Hello");
append_exclamation(&mut greeting);
println!("{}", greeting); // "Hello!"
}
引用的内存表示
fn main() {
let x: i32 = 42;
let r: &i32 = &x;
println!("x 的地址: {:p}", &x);
println!("r 的值(地址): {:p}", r);
println!("r 解引用的值: {}", *r);
}
内存布局:
栈内存:
┌──────────────┐
│ x: 42 │ ← i32,4 字节
├──────────────┤
│ r: 0x... │ ← 引用(指针),8 字节(64 位系统)
│ │ │
│ └──> 指向 x
└──────────────┘
智能指针 vs 引用
| 特性 | 引用 &T | 智能指针 Box<T> |
|---|---|---|
| 所有权 | 无 | 有 |
| 解引用 | *r | *b |
| 实现 | 简单指针 | 堆分配 + Drop |
| 大小 | 指针大小 | 指针大小(指向堆) |
fn main() {
let x = 5;
let r = &x; // 引用
let b = Box::new(x); // 智能指针(堆分配)
println!("r: {}", *r);
println!("b: {}", *b);
}
实践建议
✅ 推荐做法
fn main() {
let s = String::from("hello");
// 1. 优先使用不可变引用
print_string(&s);
// 2. 只在需要时使用可变引用
let mut s = s;
modify_string(&mut s);
// 3. 使用切片而不是索引
let word = first_word(&s); // 返回 &str
}
fn print_string(s: &String) {
println!("{}", s);
}
fn modify_string(s: &mut String) {
s.push_str(", world");
}
fn first_word(s: &String) -> &str {
// 返回切片而不是索引
&s[..]
}
❌ 避免做法
fn main() {
let mut s = String::from("hello");
// ❌ 创建不必要的可变引用
let r1 = &mut s;
let r2 = &mut s; // 编译错误
// ❌ 返回局部变量的引用
let bad_ref = create_dangling();
}
fn create_dangling() -> &String { // ❌ 悬垂引用
let s = String::from("hello");
&s // s 在函数结束时被释放
}
练习题
练习 1:修复借用错误
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);
}
练习 2:实现字符串反转
使用引用实现字符串反转:
fn reverse_string(s: &mut String) {
// 实现反转
}
fn main() {
let mut s = String::from("hello");
reverse_string(&mut s);
println!("{}", s); // "olleh"
}
查看答案
fn reverse_string(s: &mut String) {
*s = s.chars().rev().collect();
}
fn main() {
let mut s = String::from("hello");
reverse_string(&mut s);
println!("{}", s); // "olleh"
}
小结
- ✅ 引用允许访问数据而不获取所有权
- ✅
&T是不可变引用,&mut T是可变引用 - ✅ 同一时间只能有一个可变引用或多个不可变引用
- ✅ 引用必须总是有效,不会悬垂
- ✅ 切片是特殊的引用类型
- ✅ Rust 的借用检查器在编译时保证内存安全
下一步,我们将学习生命周期。