02 引用和解引用以及常用的方法和特征
引用(Reference,
&):指向内存中某个值的地址,它不拥有该数据的所有权。解引用(Deref,
*):通过*操作符访问引用所指向的底层真实数据。自动解引用(Deref Coercion):Rust 编译器非常智能,在调用方法时,它会自动帮你做多次解引用。例如,
&String可以直接调用&str的方法。常见特征(Traits):
Copy:数据按位拷贝(如基础整型、布尔型),栈上分配。Clone:显式复制(深拷贝),可能涉及堆内存分配。Deref:允许自定义类型的解引用行为。
引用(Reference, &)与解引用(Deref, *)
引用(Reference,
&):指向内存中某个值的地址,它不拥有该数据的所有权。解引用(Deref,
*):通过*操作符访问引用所指向的底层真实数据。自动解引用(Deref Coercion):Rust 编译器非常智能,在调用方法时,它会自动帮你做多次解引用。例如,
&String可以直接调用&str的方法。
常用数据类型转换与解引用技巧(重点补充)
在 Rust 开发中,所有权和字符串/切片之间的转换非常频繁。掌握以下几种核心转换,能帮你避开绝大多数类型不匹配的编译错误:
1. String 转 &str(所有权转借用,极度常用)
String 是堆上分配的可变字符串,而 &str 是不可变的字符串切片(通常指向堆上的某段数据或静态内存)。
方式一:通过自动解引用(Deref Coercion) 当把
String的引用传给需要&str的函数时,Rust 会自动转换:
fn print_str(s: &str) {
println!("{}", s);
}
let my_string = String::from("hello");
print_str(&my_string); // &String 自动变成了 &str
方式二:显式切片
let s = String::from("hello");
let slice: &str = &s[..]; // 取整个字符串的切片
2. &str 转 String(借用转所有权,堆上分配)
当你需要把一个固定的字符串字面量(&str)变成一个可以修改、可以拥有所有权的字符串时:
**使用
.to_string()或.to_owned()或String::from()**:
let literal: &str = "hello";
let s1 = literal.to_string();
let s2 = String::from(literal);
3. 数组/向量(Vec/Slice)与切片转换
Vec<T>转&[T](向量转切片): 类似于String到&str,Vec也可以自动解引用或通过切片语法转为切片:
let v: Vec<i32> = vec![1, 2, 3];
let slice: &[i32] = &v; // 自动解引用为切片
常见特征(Traits)
Copy:数据按位拷贝(如基础整型、布尔型),栈上分配。Clone:显式复制(深拷贝),可能涉及堆内存分配。Deref:允许自定义类型的解引用行为(让你自己写的智能指针结构体也能像普通引用一样使用*和点号.method()访问)。
为什么能够解引用?深入底层的 Deref 特征
刚才我们提到了 Deref 特征,这正是 Rust 能够实现“解引用(*)”和“自动解引用(Deref Coercion)”的幕后功臣。
在 Rust 中,解引用并不是凭空发生的,而是通过实现标准库中的 std::ops::Deref 和 std::ops::DerefMut 特征(Trait)来支持的。
1. Deref 特征的真面目
在标准库中,Deref 特征的定义大致长这样:
pub trait Deref {
// 关联类型:解引用后得到的目标类型
type Target: ?Sized;
// 核心方法:给定一个自身的不可变借用,返回目标类型的不可变借用
fn deref(&self) -> &Self::Target;
}
只要你为一个自定义结构体(比如你写的智能指针、包装类型)实现了 Deref 特征,Rust 编译器就会认可它:“这个结构体可以被解引用”。
2. 一个自定义结构体实现 Deref 的完整示例
假设我们自己写了一个简易的智能指针 MyBox<T>,它把数据存放在堆上。如果不实现 Deref,我们就必须写 *(b.data) 才能拿到数据;实现了之后,就可以直接用 *b:
use std::ops::Deref;
// 自定义一个包裹泛型 T 的结构体
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
// 为 MyBox 实现 Deref 特征
impl<T> Deref for MyBox<T> {
type Target = T; // 指定解引用后目标类型是 T
fn deref(&self) -> &Self::Target {
&self.0 // 返回元组内部数据的引用
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
// 奇迹发生了:*(y) 实际上在编译时被 Rust 自动翻译成了 *(y.deref())
assert_eq!(5, *y);
}
3. 幕后发生了什么?(编译器如何处理 *y)
当你写下 *y 时,如果 y 的类型实现了 Deref 特征,Rust 编译器实际上悄悄帮你执行了这一行代码:
*(y.deref())
先调用
y.deref(),拿到内部数据的引用(&T)。再用
*把这个引用解开,拿到真实的值。
4. 可变解引用:DerefMut
除了不可变的 Deref,如果你的包装类型不仅想“读”,还想允许通过 * 去修改内部的数据,就需要额外实现 DerefMut 特征:
use std::ops::{Deref, DerefMut};
// DerefMut 依赖于 Deref
impl<T> DerefMut for MyBox<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
有了 DerefMut,你就可以写出 *y = 10; 这样修改底层数据的代码。
5. 为什么 String 能自动变成 &str?(自动解引用的链条)
回到我们之前提到的 String 到 &str 的转换。其实标准库在底层就是这样为 String 实现 Deref 的:
String实现了Deref<Target str>。所以,当你把一个
&String传给一个需要&str的函数时,Rust 编译器发现类型不匹配,它就会自动去查:“这个类型有没有实现Deref?”发现
String实现了Deref<Target str>,于是编译器自动调用了一次deref(),把&String变成了&str。如果有多层包装(比如
&MyBox<String>->&String->&str),Rust 的自动解引用强制转换(Deref Coercion)甚至会连续调用多次deref(),直到类型完全匹配为止!