深入理解 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 的主要特点:
- 拥有字符串数据的所有权。
- 管理堆内存的分配和释放。
- 可以增长或缩短字符串。
- 提供
push_str()、push()、capacity()等方法。 - 不实现
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;
可以理解为:
*s使用String的解引用行为,访问其str目标。&*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,编译器会:
- 从接收者的原始类型开始,重复解引用,构造候选接收者类型。
- 根据规则考虑末尾可能发生的数组到切片的非定长转换。
- 对每个候选类型
T,依次加入T、&T和&mut T形式的候选。 - 按候选顺序检查固有方法和作用域内适用的 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();
这里:
s的类型是String。- 编译器进行点号方法查找。
- 找到
String自己定义的len(&self)。 - 自动借用
s,形成所需的&String接收者。 - 调用
String::len()。
主要涉及:
Autoref。
特别注意:String 和 str 都有各自的固有 len() 方法。对于这里的 s.len(),会找到 String 自己的 len(),并不需要通过 Deref 才能获得 len()。
6.2 String::len(&s)
let b = String::len(&s);
这里:
- 显式选择
String::len。 String::len需要&String。&s的类型正是&String。- 参数类型直接匹配。
既不需要自动添加引用,也不需要 Deref Coercion。
6.3 str::len(&s)
let c = str::len(&s);
这里:
- 显式选择
str::len。 str::len需要&str。&s的类型是&String。- 编译器根据
String: Deref<Target = str>,把&String强制转换成&str。 - 调用
str::len()。
主要涉及:
Deref Coercion。
6.4 s.trim()
let d = s.trim();
这里:
- 接收者类型是
String。 - 查找适用的
trim()方法。 - 通过
Deref将str纳入候选接收者类型。 - 找到
str::trim(&self)。 - 自动提供
&str接收者。
主要涉及:
Autoderef + Autoref。
6.5 str::trim(&s)
let e = str::trim(&s);
这里:
- 显式选择
str::trim。 - 需要
&str。 - 提供的是
&String。 - 发生 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 的自动引用与解引用机制,最重要的不是记住某几个等价写法,而是始终追问三个问题:
- 当前表达式的实际类型是什么?
- 目标方法或函数需要什么类型?
- 当前语法位置允许编译器执行哪些自动调整?
只要按照这三个问题进行分析,大部分涉及 String、str、引用、智能指针和 Deref 的代码,都可以被清晰而严格地解释。

浙公网安备 33010602011771号