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())

  1. 先调用 y.deref(),拿到内部数据的引用(&T)。

  2. 再用 * 把这个引用解开,拿到真实的值。


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(),直到类型完全匹配为止!