深入理解 Rust 的 Deref、Autoderef、Autoref 与 Deref Coercion:从 String 和 str 说起

在学习 Rust 的所有权、借用和方法调用时,我们很容易遇到一些看似矛盾的现象。

例如:

fn main() {
    let s = String::from("hello");

    let a = s.len();         // 正确
    let b = String::len(&s); // 正确
    let c = str::len(&s);    // 正确
    let d = str::len(s);     // 编译错误
}

为什么 s 是 String 类型,却可以调用 str 类型上的方法?为什么 s.len() 不需要显式添加 &,而 str::len(s) 却不能通过编译?既然 Rust 支持自动解引用,为什么不能自动完成所有这些转换?

这些问题的答案,涉及四个相互联系但又有明确分工的语言机制:

  • Deref Trait:定义一个类型如何被解引用。
  • Autoderef:方法查找过程中,编译器自动尝试解引用。
  • Autoref:点号方法调用过程中,编译器按需自动借用接收者。
  • Deref Coercion:在允许的类型强制转换位置,将一种引用自动转换为另一种引用。

要真正理解这些机制,我们首先需要弄清楚 String、str、&String 和 &str 的本质区别。


一、从 String、str、&String 和 &str 开始

1.1 String:拥有字符串数据所有权的类型

考虑下面的代码:

let s = String::from("hello");

变量 s 的类型是 String。

String 是 Rust 标准库提供的一种拥有所有权、可增长的 UTF-8 字符串类型。

从概念上看,它管理着三项信息:

String
├── ptr       指向字符串数据
├── len       当前字符串长度(字节数)
└── capacity  已分配的容量(字节数)

其内存结构可以理解为:

栈上的 String 值                 堆内存

┌─────────────────┐
│ ptr ────────────┼───────────┐
├─────────────────┤           │
│ len = 5         │           ▼
├─────────────────┤       ┌───┬───┬───┬───┬───┐
│ capacity >= 5   │       │ h │ e │ l │ l │ o │
└─────────────────┘       └───┴───┴───┴───┴───┘

这里的图示是一种逻辑模型,而不是对 String 内部字段排列顺序的保证。

在常见的 64 位平台上,String 通常占用 24 字节,用于保存这三项元数据。字符串字节则存储在它管理的堆分配中。

String 的主要特点:

  1. 拥有字符串数据的所有权。
  2. 管理堆内存的分配和释放。
  3. 可以增长或缩短字符串。
  4. 提供 push_str()、push()、capacity() 等方法。
  5. 不实现 Copy,按值传递时通常会发生所有权移动。

例如:

let mut s = String::from("hello");
s.push_str(" world");

println!("{s}"); // hello world

由于 s 是可变绑定,因此可以通过它修改字符串。

1.2 str:动态大小的字符串切片类型

str 和 String 并不是同一种类型。

str 表示一段有效的 UTF-8 字符串内容,是一个动态大小类型(Dynamically Sized Type,DST)。

为什么说它是动态大小类型?

因为一个字符串可能包含任意数量的 UTF-8 字节。

例如:

"hello"       5 Bytes
"Rust"        4 Bytes
"你好"        6 Bytes

编译器无法仅根据 str 这个类型,就确定某个字符串包含多少字节。

因此,通常不能像下面这样直接创建一个按值存储的局部变量:

let s: str = "hello"; // 错误

在 Rust 中,我们通常通过 &str、Box<str> 等间接方式使用 str。

需要强调:

str 并不意味着字符串必须存储在堆上。它只描述字符串内容的类型,不决定内存分配方式。

字符串内容可以来自静态内存、堆分配,甚至某些栈上字节缓冲区。

1.3 &str:引用一段字符串内容

考虑:

let s: &str = "hello";

这里的 s 并不是 String,而是 &str。

&str 是对字符串内容的共享引用。

由于 str 是动态大小类型,仅凭一个数据地址无法知道字符串的长度,因此 &str 是一种胖指针(Fat Pointer)。

它在概念上包含:

&str
├── ptr  指向 UTF-8 字节
└── len  被引用字符串的字节长度

例如:

&str                           字符串内容

┌────────────────┐
│ ptr ───────────┼─────────┐
├────────────────┤         │
│ len = 5        │         ▼
└────────────────┘     ┌───┬───┬───┬───┬───┐
                       │ h │ e │ l │ l │ o │
                       └───┴───┴───┴───┴───┘

&str 不拥有被引用字符串的所有权,也不负责释放字符串的存储空间。

例如:

let s = String::from("hello");

let slice: &str = s.as_str();

其中:

  • s 拥有字符串数据。
  • slice 借用了 s 中的字符串内容。
  • slice 与 s 指向同一份 UTF-8 字节。
  • 创建 slice 不需要复制字符串数据。

1.4 &String:引用整个 String 值

那么,&String 与 &str 有什么区别?

观察:

let s = String::from("hello");

let r1: &String = &s;
let r2: &str = s.as_str();

两者都属于共享引用,但是它们的目标不同。

                      &String

                   ┌────────────┐
                   │ 指向 String│
                   └──────┬─────┘
                          │
                          ▼
                  ┌─────────────────┐
                  │ String          │
                  │ ptr ────────────┼────────┐
                  │ len = 5         │        │
                  │ capacity >= 5   │        │
                  └─────────────────┘        │
                                             ▼
                                        ┌─────────┐
                                        │ "hello" │
                                        └─────────┘
                                             ▲
                                             │
                      &str                   │
                   ┌────────────┐            │
                   │ ptr ───────┼────────────┘
                   │ len = 5    │
                   └────────────┘

注意:

  • &String 引用的是整个 String 对象。
  • &str 引用的是 UTF-8 字符串内容。
  • &String 本身通常是一个瘦指针(Thin Pointer)。
  • &str 通常包含数据指针与长度两个机器字。

在常见的 64 位平台上:

类型 典型大小 说明
String 24 Bytes 管理指针、长度和容量
&String 8 Bytes 引用整个 String
&str 16 Bytes 数据指针和长度
str 运行时决定 动态大小类型

可以通过下面的程序观察:

use std::mem::size_of;

fn main() {
    println!("{}", size_of::<String>());
    println!("{}", size_of::<&String>());
    println!("{}", size_of::<&str>());
}

注意,这里描述的是常见目标平台上的表示,不应把所有具体字节数当成 Rust 对所有目标平台的保证。

到这里,我们已经建立了基础:

String 是拥有所有权的字符串容器,str 是字符串内容的类型,&String 引用容器,&str 引用字符串内容。

接下来要解决的问题就是:

既然 String 和 str 不同,Rust 为什么可以方便地在它们之间进行方法调用和引用转换?

答案是 Deref Trait。


二、Deref Trait:建立解引用关系

2.1 从普通引用的解引用开始

Rust 使用 & 创建引用,使用 * 解引用。

fn main() {
    let x = 100;
    let r = &x;

    let y = *r;

    println!("{y}"); // 100
}

其中:

let r = &x;

创建一个 &i32。

而:

let y = *r;

沿着引用访问 i32 值。由于 i32 实现了 Copy,因此可以复制出整数 100。

这里 * 的核心含义是:访问被引用或被间接持有的目标值。

但对于 Box<T>、String 等类型,编译器又如何知道解引用应该得到什么?

Rust 提供了 Deref Trait,让类型能够定义自己的解引用行为。

2.2 Deref Trait 的定义

Deref 的核心接口可以概括为:

use std::ops::Deref;

trait Deref {
    type Target: ?Sized;

    fn deref(&self) -> &Self::Target;
}

这是为了教学而省略了一些标准库实现细节的形式。

其中包含两个重要部分。

第一部分:关联类型 Target

type Target: ?Sized;

用于指定解引用的目标类型。

例如:

type Target = str;

意味着解引用的目标类型是 str。

这里的 ?Sized 表示目标类型可以是动态大小类型,例如 str。

第二部分:deref() 方法

fn deref(&self) -> &Self::Target;

用于返回一个指向目标类型的共享引用。

这是 Deref Trait 最重要的约定。

注意它返回的是:

&Self::Target

而不是:

Self::Target

因此,Deref 本身不会要求移动或者复制目标值。

2.3 String 如何实现 Deref?

标准库中的 String 实现了:

impl Deref for String {
    type Target = str;

    fn deref(&self) -> &str {
        self.as_str()
    }
}

这意味着:

String ──Deref──> str

准确地说:

当对 String 执行共享解引用操作时,Rust 可以通过 Deref::deref() 获得一个 &str。

例如:

use std::ops::Deref;

fn main() {
    let s = String::from("hello");

    let r: &str = Deref::deref(&s);

    println!("{r}");
}

这里:

Deref::deref(&s)

会调用 String 对 Deref 的实现,得到 &str。

还有一种写法:

let r: &str = &*s;

可以理解为:

  1. *s 使用 String 的解引用行为,访问其 str 目标。
  2. &*s 借用这个目标,形成 &str。

注意,*s 本身并不意味着构造了一个独立的 str 值,也没有将整个字符串复制出来。

由于 str 是动态大小类型,下面的按值移动并不合法:

let t = *s; // 错误:不能按值移动一个 str

因此,更准确的理解是:

Deref 定义的是从一个间接访问类型到其目标类型的解引用行为,而不是“把 String 转换成一个新的 str 对象”。


三、Autoderef:Rust 为什么可以自动解引用?

现在考虑:

let s = String::from("  hello  ");

let result = s.trim();

这里有一个值得注意的事实:

trim() 是 str 上定义的固有方法,而不是 String 自己定义的方法。

概念上,它的签名是:

impl str {
    pub fn trim(&self) -> &str {
        // ...
    }
}

那么为什么 String 类型的变量 s 可以直接调用?

因为 Rust 在解析点号方法调用时,会进行自动解引用。

3.1 方法查找的基本过程

编译器面对:

s.trim()

时,会从接收者 s 的类型开始寻找适用的方法。

一个简化的查找过程是:

接收者 s
类型 String
    │
    ├── 查看 String 上适用的 trim 方法
    │       └── 未找到
    │
    ├── 尝试 Deref
    │       │
    │       ▼
    │      str
    │
    ├── 查看 str 上适用的 trim 方法
    │       └── 找到了 str::trim(&self)
    │
    ▼
使用合适的 &str 接收者调用 trim

这一过程体现了 Autoderef。

但是需要说明,Rust 的正式方法查找算法比这个流程更加精细。

按照 Rust Reference,编译器会:

  1. 从接收者的原始类型开始,重复解引用,构造候选接收者类型。
  2. 根据规则考虑末尾可能发生的数组到切片的非定长转换。
  3. 对每个候选类型 T,依次加入 T、&T 和 &mut T 形式的候选。
  4. 按候选顺序检查固有方法和作用域内适用的 Trait 方法。

对于 s: String,一个示意性的候选序列是:

String
&String
&mut String

str
&str
&mut str

trim(&self) 可以使用 &str 接收者,因此最终能够匹配。

这一点说明了一个重要事实:

Autoderef 并不简单等于“优先找完所有 String 的方法,再找所有 str 的方法”。它是方法接收者候选类型构造和查找过程的一部分。

3.2 多层引用也能自动解引用

考虑:

fn main() {
    let s = String::from("hello");

    let r1: &String = &s;
    let r2: &&String = &r1;

    println!("{}", r2.trim());
}

r2 的类型是:

&&String

为什么可以调用 trim()?

因为编译器可以沿着多层解引用关系寻找方法:

&&String
    │
    ▼
&String
    │
    ▼
String
    │
    ▼
str
    │
    ▼
str::trim(&self)

其中,从引用向被引用的类型前进,以及从 String 向 str 前进,都可以参与方法查找。

但二者的来源略有不同:

  • &T 的解引用是 Rust 引用类型自身支持的能力。
  • String 能够解引用到 str,是因为它实现了 Deref<Target = str>。

因此:

r2.trim()

无需我们手动写出多层 *。

这就是 Autoderef 的重要价值:简化复杂的引用和智能指针上的方法调用。


四、Autoref:Rust 什么时候自动添加 &?

理解了 Autoderef,我们再分析 Autoref。

4.1 方法为什么可以省略 &?

考虑:

let s = String::from("hello");

let n = s.len();

String 自身定义了:

impl String {
    pub const fn len(&self) -> usize {
        // ...
    }
}

它需要的接收者是:

&String

但我们写的是:

s.len()

而不是:

(&s).len()

这是因为 Rust 的点号方法调用支持自动借用接收者。

因此,可以将:

s.len()

概念性地理解成:

String::len(&s)

注意,这只是用于说明方法调用时接收者调整的等价思路,不代表编译器必须先将源代码重写成这个形式。

4.2 自动添加 &mut

对于需要可变接收者的方法:

let mut s = String::from("hello");

s.push_str(" world");

push_str() 的签名是:

impl String {
    pub fn push_str(&mut self, string: &str) {
        // ...
    }
}

它要求:

&mut String

因此:

s.push_str(" world")

可以概念性地理解成:

String::push_str(&mut s, " world")

这里的自动借用是 &mut,并且必须满足 Rust 的可变借用规则。

所以,Autoref 不只是添加 &,还可能按方法接收者要求建立可变借用。

4.3 为什么普通函数调用不自动添加 &?

现在假设有一个函数:

fn show(s: &str) {
    println!("{s}");
}

调用:

let s = String::from("hello");

show(s);

会报错。

原因是:

show 需要:&str
实际传入:String

普通函数实参不会像点号方法的接收者那样,被自动借用成 &String。

因此必须写:

show(&s);

同样:

str::len(s);  // 错误

也不会自动变成:

str::len(&s);

这是因为:

str::len(s)

属于显式提供方法接收者的路径调用形式,并不是点号方法调用表达式。

所以,虽然 . 和 :: 都能用于调用方法,但只有前者触发这一套点号方法接收者查找与调整机制。

重要结论:Autoref 主要解释的是点号方法调用中的自动借用行为,不能把它理解成 Rust 会对所有函数参数自动添加引用。


五、Deref Coercion:为什么 &String 可以变成 &str?

现在我们已经知道:

  • String 实现了 Deref<Target = str>。
  • Autoderef 可以参与方法查找。
  • Autoref 可以在点号方法调用中自动借用。

但还有一个问题:

fn show(s: &str) {
    println!("{s}");
}

fn main() {
    let s = String::from("hello");

    show(&s);
}

为什么这段代码可以编译?

这里的参数类型实际上也不相同:

show 需要:&str

&s 的类型:&String

Rust 必须将:

&String

转换为:

&str

这个转换使用的机制就是 Deref Coercion(解引用强制转换)。

5.1 Deref Coercion 的基本规则

如果类型 T 实现了:

Deref<Target = U>

那么在适当的强制转换位置,Rust 可以将:

&T

转换成:

&U

例如:

String 实现 Deref<Target = str>

因此:

&String → &str

可以自动发生。

注意,这里的转换不是:

String → str

而是:

&String → &str

这是一个非常关键的区别。

5.2 编译器如何完成转换?

对于:

show(&s);

可以理解成下面的过程:

s: String
    │
    │ 显式执行 &s
    ▼
&String
    │
    │ Deref Coercion
    ▼
&str
    │
    ▼
show(s: &str)

进一步展开,可以写成:

fn show(s: &str) {
    println!("{s}");
}

fn main() {
    let s = String::from("hello");

    let r: &String = &s;

    let slice: &str = &**&r;
    show(slice);
}

不过上面的显式解引用写法不够直观。对于 r: &String,更直接的形式是:

let slice: &str = &**r;

其中第一次 * 从 &String 到 String,第二次 * 使用 String 的 Deref 到 str,最后再借用成 &str。

在实际开发中,我们通常直接写:

let slice: &str = &s;

编译器会完成必要的转换。

这里没有创建新的 String,也没有复制字符串字节。

5.3 Deref Coercion 什么时候发生?

Deref Coercion 属于 Rust 的类型强制转换机制,而类型强制转换只会在允许的特定位置发生。

例如:

场景一:函数参数

fn show(s: &str) {
    println!("{s}");
}

let owned = String::from("hello");

show(&owned);

场景二:显式类型的变量初始化

let owned = String::from("hello");

let borrowed: &str = &owned;

场景三:函数返回值

fn get_text(s: &String) -> &str {
    s
}

函数返回类型明确要求 &str,而表达式 s 是 &String,所以允许发生 Deref Coercion。

但是,如果没有相应的类型需求,Rust 不会无缘无故地进行转换。

例如:

let s = String::from("hello");

let r = &s;

这里 r 推断为:

&String

而不是 &str。

如果明确指定目标类型:

let r: &str = &s;

才会在这个位置把 &String 强制转换为 &str。

因此:

Deref Coercion 是由特定位置的目标类型要求驱动的,而不是编译器在任何地方随意执行的转换。

5.4 Deref Coercion 不会主动添加引用

这是最容易混淆的地方。

let s = String::from("hello");

str::len(s);

为什么不行?

因为 str::len() 需要:

&str

而 s 是:

String

从:

String

到:

&str

首先需要创建引用,然后才能利用 Deref 完成引用类型转换。

Deref Coercion 不负责无条件地执行第一步。

因此:

str::len(s);  // 错误
str::len(&s); // 正确

可以把两者的区别记成:

Autoref:
    T → &T
    在点号方法调用的接收者调整中按需发生

Deref Coercion:
    &T → &U
    当 T: Deref<Target = U> 时,
    在适当的类型强制转换位置发生

这是两种不同的机制。


六、完整分析各种 String 和 str 方法调用

现在我们可以把前面的知识组合起来。

fn main() {
    let s = String::from("  hello  ");

    let a = s.len();
    let b = String::len(&s);
    let c = str::len(&s);
    let d = s.trim();
    let e = str::trim(&s);
}

逐个分析。

6.1 s.len()

let a = s.len();

这里:

  1. s 的类型是 String。
  2. 编译器进行点号方法查找。
  3. 找到 String 自己定义的 len(&self)。
  4. 自动借用 s,形成所需的 &String 接收者。
  5. 调用 String::len()。

主要涉及:

Autoref。

特别注意:String 和 str 都有各自的固有 len() 方法。对于这里的 s.len(),会找到 String 自己的 len(),并不需要通过 Deref 才能获得 len()。

6.2 String::len(&s)

let b = String::len(&s);

这里:

  1. 显式选择 String::len。
  2. String::len 需要 &String。
  3. &s 的类型正是 &String。
  4. 参数类型直接匹配。

既不需要自动添加引用,也不需要 Deref Coercion。

6.3 str::len(&s)

let c = str::len(&s);

这里:

  1. 显式选择 str::len。
  2. str::len 需要 &str。
  3. &s 的类型是 &String。
  4. 编译器根据 String: Deref<Target = str>,把 &String 强制转换成 &str。
  5. 调用 str::len()。

主要涉及:

Deref Coercion。

6.4 s.trim()

let d = s.trim();

这里:

  1. 接收者类型是 String。
  2. 查找适用的 trim() 方法。
  3. 通过 Deref 将 str 纳入候选接收者类型。
  4. 找到 str::trim(&self)。
  5. 自动提供 &str 接收者。

主要涉及:

Autoderef + Autoref。

6.5 str::trim(&s)

let e = str::trim(&s);

这里:

  1. 显式选择 str::trim。
  2. 需要 &str。
  3. 提供的是 &String。
  4. 发生 Deref Coercion。

主要涉及:

Deref Coercion。

6.6 对比表

表达式 是否合法 主要机制
s.len() 是 Autoref
String::len(&s) 是 参数直接匹配
String::len(s) 否 缺少引用
str::len(&s) 是 Deref Coercion
str::len(s) 否 不会自动创建引用
s.trim() 是 Autoderef + Autoref
str::trim(&s) 是 Deref Coercion
str::trim(s.as_str()) 是 参数直接匹配
s.as_str() 是 调用 String 自身的方法

要特别强调:这里说的“主要机制”是为了区分不同写法的关键原因,而不是声称某次方法解析在编译器内部只涉及一种类型调整。


七、从 Deref 进一步理解 DerefMut

前面讨论的 Deref 主要提供共享解引用。

但如果我们希望通过解引用修改目标值呢?

Rust 为此提供 DerefMut。

它的核心形式是:

use std::ops::Deref;

trait DerefMut: Deref {
    fn deref_mut(&mut self) -> &mut Self::Target;
}

同样,这里是展示核心接口的简化形式。

DerefMut 建立在 Deref 之上,并额外提供可变解引用能力。

标准库的 String 也实现了 DerefMut<Target = str>。

因此:

fn main() {
    let mut s = String::from("hello");

    let slice: &mut str = &mut s;

    slice.make_ascii_uppercase();

    println!("{s}");
}

输出:

HELLO

这里:

&mut String → &mut str

属于可变解引用强制转换。

但是,需要注意:

&mut str 并不等价于 &mut String。

例如:

let mut s = String::from("hello");

let slice: &mut str = &mut s;
slice.make_ascii_uppercase();

可以正常修改原有字符串内容。

但是:

slice.push_str(" world");

无法编译。

因为 push_str() 是 String 上的方法,而不是 str 上的方法。

str 表示一段长度固定的 UTF-8 字符串内容视图,不能像 String 那样管理容量并追加字符。

因此,DerefMut 也不意味着获得了原始拥有者类型的全部能力。

它仅仅使我们能够通过可变引用访问目标类型所允许的操作。


八、自己实现 Deref:理解这一设计的真正价值

前面的例子都使用标准库的 String。

现在我们自己定义一个类型:

use std::ops::Deref;

struct MyString {
    inner: String,
}

impl Deref for MyString {
    type Target = str;

    fn deref(&self) -> &str {
        self.inner.as_str()
    }
}

fn main() {
    let s = MyString {
        inner: String::from("  hello  "),
    };

    println!("{}", s.trim());
    println!("{}", s.len());
}

输出:

hello
9

这里最关键的是:

impl Deref for MyString {
    type Target = str;
}

我们并没有在 MyString 中定义:

fn trim(...)
fn len(...)

但却可以通过:

s.trim()
s.len()

使用 str 类型上对应的方法。

这是因为方法查找可以沿着:

MyString
    │
    │ Deref
    ▼
str
    │
    ├── trim()
    ├── len()
    ├── contains()
    ├── starts_with()
    └── ...

寻找适用的方法。

不仅如此,我们还可以将 MyString 借用给需要 &str 的函数:

fn show(s: &str) {
    println!("{s}");
}

fn main() {
    let s = MyString {
        inner: String::from("hello"),
    };

    show(&s);
}

这里:

&MyString → &str

也可以通过 Deref Coercion 完成。

现在,我们终于能够回答一个更加根本的问题:

为什么 Rust 要设计 Deref Trait?

因为它使得具有指针式间接访问语义的类型,可以自然地暴露目标类型的操作,而不必对每个方法都手动编写转发代码。

例如:

  • String 可以解引用到 str。
  • Vec<T> 可以解引用到 [T]。
  • Box<T> 可以解引用到 T。
  • Rc<T> 和 Arc<T> 也可以提供对其目标值的共享访问。

但是,Deref 不应该被随意当成一般的类型转换接口使用。

因为实现 Deref 后,编译器可能在方法调用和引用转换等位置隐式使用它。方法名冲突、API 兼容性和解引用行为的可预期性都需要考虑。

如果只是希望提供普通的引用转换接口,AsRef 往往也是值得考虑的选择。


九、彻底厘清四种机制的关系

经过前面的分析,我们可以建立一个统一的模型。

9.1 Deref Trait:定义解引用关系

例如:

impl Deref for String {
    type Target = str;

    fn deref(&self) -> &str {
        self.as_str()
    }
}

它提供:

String ──Deref──> str

这是一种类型所实现的能力。

9.2 Autoderef:沿解引用关系查找方法

例如:

s.trim()

编译器可以沿着:

String → str

寻找适用的方法。

这是点号方法解析过程中的机制。

9.3 Autoref:自动提供方法所需的引用

例如:

s.len()

当选择的方法需要 &self 时,编译器可以自动借用接收者。

类似地:

s.push_str(" world")

当方法需要 &mut self 时,编译器可以自动建立可变借用,但必须符合借用规则。

9.4 Deref Coercion:转换已经存在的引用

例如:

let s = String::from("hello");

let r: &str = &s;

这里编译器根据目标类型,把:

&String → &str

这是引用类型的隐式强制转换。

9.5 四者的分工

机制 核心问题 典型示例
Deref 如何定义解引用目标? String → str
Autoderef 如何沿解引用关系寻找方法? s.trim()
Autoref 方法需要引用时是否自动借用? s.len()
Deref Coercion 已有引用如何转换到目标引用? &String → &str

它们相互配合,却不是同一种操作。

尤其要避免下面这个错误认识:

Rust 有自动解引用,因此可以自动完成 String、&String、str 和 &str 之间的任意转换。

正确的理解应该是:

Rust 在特定语法和类型检查场景中,按照明确的规则进行借用、解引用和类型强制转换。这些操作受所有权、生命周期、方法查找与类型约束共同限制。


十、几个最容易产生误解的问题

10.1 Deref 是否等于复制数据?

不是。

例如:

let s = String::from("hello");

let r: &str = &s;

这里创建的是借用视图,没有复制 UTF-8 字符串字节。

10.2 &String 转为 &str 是否转移所有权?

不是。

let s = String::from("hello");

let r: &str = &s;

println!("{r}");
println!("{s}");

完全合法。

r 只是共享借用 s 的内容。

10.3 String 能 Deref 到 str,是否说明两者是父子类?

不是。

Rust 的 Deref 不是传统面向对象语言中的类继承。

String 实现 Deref<Target = str>,表示它支持一种指针式的间接访问关系,并不意味着 String 是 str 的子类。

Rust 也不会因此把 String 的所有权自动转移成一个独立的 str。

10.4 自动解引用是否在所有函数调用中都发生?

不能这样理解。

例如:

fn show(s: &str) {}

let s = String::from("hello");

show(&s); // 正确
show(s);  // 错误

前者允许发生 Deref Coercion,后者不能自动创建所需的共享引用。

10.5 为什么 s.len() 和 str::len(&s) 都返回 5?

因为它们都返回 UTF-8 字节长度。

但是它们不是同一个方法定义:

  • String::len() 是 String 自身的方法。
  • str::len() 是 str 自身的方法。

例如:

let s = String::from("你好");

assert_eq!(s.len(), 6);
assert_eq!(str::len(&s), 6);
assert_eq!(s.chars().count(), 2);

这里 len() 返回 6,因为两个汉字的 UTF-8 编码合计占 6 字节。

所以不能把 len() 简单理解成“字符个数”。


十一、总结:建立完整的 Rust 字符串心智模型

如果把本文的全部内容总结成一条逻辑链,可以得到:

String
│
│ 拥有 UTF-8 字符串数据
│
├── &String
│   │
│   │ 共享借用 String 对象
│   │
│   └── Deref Coercion
│           │
│           ▼
│          &str
│
└── 实现 Deref<Target = str>
            │
            ▼
           str
            │
            ├── len()
            ├── trim()
            ├── chars()
            ├── contains()
            └── starts_with()

在此基础上:

s.trim()

利用 Autoderef 和 Autoref,在点号方法查找中找到适用的方法。

而:

str::trim(&s)

利用已经存在的引用 &s,通过 Deref Coercion 满足 &str 的参数要求。

至于:

str::trim(s)

则不合法,因为显式参数传递不会无条件自动添加所需要的 &。

因此,要理解 Rust 的自动引用与解引用机制,最重要的不是记住某几个等价写法,而是始终追问三个问题:

  1. 当前表达式的实际类型是什么?
  2. 目标方法或函数需要什么类型?
  3. 当前语法位置允许编译器执行哪些自动调整?

只要按照这三个问题进行分析,大部分涉及 String、str、引用、智能指针和 Deref 的代码,都可以被清晰而严格地解释。


参考资料

  1. The Rust Programming Language — Treating Smart Pointers Like Regular References
  2. Rust Reference — Method-call expressions
  3. Rust Reference — Type coercions
  4. Rust Standard Library — Deref
  5. Rust Standard Library — String
  6. Rust Standard Library — str
posted @ 2026-10-29 14:35  ZhengQC  阅读(7)  评论(0)    收藏  举报