示例代码:
fn main() {
// 获得一个 `i32` 类型的引用。`&` 表示取引用。
let reference = &4;
match reference {
// 如果用 `&val` 这个模式去匹配 `reference`,就相当于做这样的比较:
// `&i32`(译注:即 `reference` 的类型)
// `&val`(译注:即用于匹配的模式)
// ^ 我们看到,如果去掉匹配的 `&`,`i32` 应当赋给 `val`。
// 译注:因此可用 `val` 表示被 `reference` 引用的值 4。
&val => println!("Got a value via destructuring: {:?}", val),
}
// 如果不想用 `&`,需要在匹配前解引用。
match *reference {
val => println!("Got a value via dereferencing: {:?}", val),
}
// 如果一开始就不用引用,会怎样? `reference` 是一个 `&` 类型,因为赋值语句
// 的右边已经是一个引用。但下面这个不是引用,因为右边不是。
let _not_a_reference = 3;
// Rust 对这种情况提供了 `ref`。它更改了赋值行为,从而可以对具体值创建引用。
// 下面这行将得到一个引用。
let ref _is_a_reference = 3;
// 相应地,定义两个非引用的变量,通过 `ref` 和 `ref mut` 仍可取得其引用。
let value = 5;
let mut mut_value = 6;
// 使用 `ref` 关键字来创建引用。
// 译注:下面的 r 是 `&i32` 类型,它像 `i32` 一样可以直接打印,因此用法上
// 似乎看不出什么区别。但读者可以把 `println!` 中的 `r` 改成 `*r`,仍然能
// 正常运行。前面例子中的 `println!` 里就不能是 `*val`,因为不能对整数解
// 引用。
match value {
ref r => println!("Got a reference to a value: {:?}", r),
}
// 类似地使用 `ref mut`。
match mut_value {
ref mut m => {
// 已经获得了 `mut_value` 的引用,先要解引用,才能改变它的值。
*m += 10;
println!("We added 10. `mut_value`: {:?}", m);
},
}
}
运行结果:
Got a value via destructuring: 4
Got a value via dereferencing: 4
Got a reference to a value: 5
We added 10. `mut_value`: 16
有个疑问没有搞明白:
let value = 5;
match value {
ref r => println!("Got a reference to a value: {:?}", *r),
}
可以运行, 但是
let reference = &4;
match reference {
&val => println!("Got a value via destructuring: {:?}", *val),
}
就运行不了, 会报错
Rust 模式匹配中的 & 和 ref 意义相反:
&在模式中:解构一个引用,提取出背后的值(所有权转移或复制)。ref在模式中:创建一个引用,绑定到匹配的值上。- let reference = &4 的reference取到的实际是&i32, 所以匹配&val就是取对应的值, 也就是4, 而4是具体的值, 使用*来解引用是不对的.
为什么这样写也能正常打印?:
let value = 5;
match value {
ref r => println!("Got a reference to a value: {:?}", r),
}
1. 两个版本的差异
let value = 5;
match value {
ref r => println!("Got a reference to a value: {:?}", *r), // 显式解引用
}
let value = 5;
match value {
ref r => println!("Got a reference to a value: {:?}", r), // 直接打印引用
}
2. 为什么直接打印引用 r 也可以?
关键在于 println! 的 {:?} 格式化器要求参数实现 Debug trait。
而标准库为 所有引用类型(&T 和 &mut T)都实现了 Debug,前提是 T 本身实现了 Debug。
具体来说标准库中有类似这样的实现(示意):
impl<T: Debug + ?Sized> Debug for &T {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&**self, f) // 递归地打印被引用的值
}
}
因此当你打印一个 &T 时,输出的实际是 它指向的 T 的 Debug 表示,即自动解引用了一层(甚至多层)。
- 如果
value的类型是i32(实现了Debug),那么r的类型是&i32,打印r会直接显示4(假设value=4)。 - 这与显式写
*r的效果在 视觉输出上完全相同,因为*r是i32,打印i32也是显示4。
3. 本质区别:类型不同,但输出相同
| 写法 | r 的类型 |
println! 实际看到的参数类型 |
输出效果 |
|---|---|---|---|
println!("...", *r) |
&i32 |
i32(经解引用) |
4 |
println!("...", r) |
&i32 |
&i32 |
4(因为 Debug for &i32 自动解引用) |
虽然底层类型不同(一个是 i32,一个是 &i32),但因为 Debug 对引用的实现很“智能”,最终打印出来的字符串是一样的(除非 T 的 Debug 实现有特殊行为,例如打印地址而不是值——但普通类型不会)。
4. 什么时候必须显式解引用?
- 当需要 值的所有权 或 值的实际类型 进行运算时(例如
*r + 1)。 - 当你使用的 trait 没有为引用实现,但为值实现了时(例如
std::fmt::Display通常没有对引用自动实现,所以println!("{}", r)可能报错,而println!("{}", *r)可以)。 - 当你需要精确控制解引用层数(例如遇到
&&T时)。
对于 Debug ({:?}),由于标准库贴心地为所有引用实现了它,所以通常可以偷懒不写 *。
5. 总结
ref r绑定了一个引用&value。- 直接打印
r能工作,因为Debug for &T会自动打印出引用的值(类似于一次隐式解引用)。 *r则是显式解引用得到T后再打印。- 功能上两者输出相同,但前者少写一个
*,更简洁。
所以两个写法(有 * 和无 *)都是正确的,区别只在于是否显式解引用。
为什么这样写也报错?
let reference = &String::from("Hello");
match reference {
&val => println!("Got a value via destructuring: {:?}", val),
}
原因在于 String 没有实现 Copy trait,而示例中的 i32 是 Copy 的。
1. 先看这个代码的编译错误:
error[E0507]: cannot move out of `*reference` which is behind a shared reference
--> src/main.rs:4:11
|
4 | &val => println!("...", val),
| ^^^
| |
| move occurs because `val` has type `String`, which does not implement the `Copy` trait
| help: consider borrowing here: `&val`
2. 模式 &val 做了什么?
当你在 match 中对一个 &T 类型的值使用模式 &val:
- 它会解构这个引用,把引用背后的
T值移动(或复制)出来绑定到val。 - 对于
&String,T就是String(一个拥有堆上数据的类型)。 String没有实现Copy,所以val会获取String的所有权。
但是这里有个严重问题:reference 只是一个共享引用 &String,它并不拥有这个 String 的所有权。所有权属于原本创建 String 的变量(如果存在)或者临时值。从共享引用中强行把所有权移走是绝对禁止的,因为这会让原所有者再也无法安全地释放内存。
所以编译器报错:cannot move out of borrowed content。
3. 为什么之前的 &i32 可以工作?
let reference = &4; // &i32
match reference {
&val => println!("{}", val), // 可以运行
}
i32实现了Copy,因此&val模式不会“移动”所有权,而是复制出一个新的i32值给val。- 复制一个
i32只是复制 4 个字节,没有任何安全问题,也不影响原引用。
这正是区别所在:Copy 类型可以安全地从引用中取出值(复制),非 Copy 类型则不允许。
4. 如何修正非 Copy 类型的情况?
方案一:直接绑定引用(不析构)
match reference {
val => println!("Got a reference: {:?}", val), // val 是 &String
}
val 的类型是 &String,没有移动所有权,只是把引用复制了一份(引用本身是 Copy 的)。
方案二:使用 ref 模式(显式创建引用)
match reference {
ref val => println!("Got a reference: {:?}", val), // val 是 &&String(双重引用)
}
ref 从匹配的值上创建一个引用,由于 reference 已经是 &String,ref val 会得到 &&String。打印时由于 Debug 的自动解引用,仍然会显示 "Hello"。
方案三:解引用后匹配值(需要所有权转移场景)
如果你确实需要取得 String 的所有权,那必须确保你有权移动它——例如从原来的所有者变量直接匹配,而不是从引用:
let s = String::from("Hello");
match s { // 直接匹配 owned String
val => println!("{}", val), // val 获得所有权
}
5. 总结
为什么
&val模式对&String报错,而对&i32不报错?
&val模式会尝试从引用中取出背后的值。- 对于
i32(Copy),取出是复制,安全且允许。 - 对于
String(非Copy),取出是移动所有权,但共享引用&String不拥有所有权,因此被编译器禁止。
修正方法通常是不用 &val 析构,而是直接匹配变量获得引用,或者小心处理所有权。