切片:指向一段数据,而不是全部
用 &str 和 &T 引用集合中连续的一段,理清 String、&String、&str 三者的区别与转换
上一篇结尾我们说了句有点绕的话:你其实早就在用 &str 了,它就是字符串切片。这篇把它讲清楚,顺便把 String、&String、&str 这三个几乎每个 Rust 新手都要绕一阵子的类型一次理清。
先从"返回下标"的脆弱说起
假设你要写一个函数,返回一句话里的第一个单词。在 Java 或 Python 里,你大概会返回一个子串;但 Rust 新手的第一版往往是返回下标:
rust
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i; // 找到空格,返回它的位置
}
}
s.len()
}
"hello world" 会返回 5,看起来没问题。但 Book 点破了这个版本的死穴:下标和字符串之间的关联,全靠程序员的自觉,编译器无从检查。你拿到下标 5 之后,字符串一变(比如被清空),这个 5 就指向了错误的位置甚至越界------等运行时才炸。
Rust 的思路是换个返回值:与其告诉调用方"单词在第几个位置",不如直接给出"这一段就是那个单词"------不复制数据,而是返回一个指向这段数据的引用。这个引用,就是切片(slice)。
切片 = 起始指针 + 长度
Book 的定义很干脆:切片让你引用一个集合中连续的一段 元素;它是一种引用,所以不拥有所有权。字符串切片的类型写作 &str:
rust
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
s 的堆数据是 "hello world" 这 11 个字节。&s[0..5] 创建了一个切片,内部只存两个东西:一个指向第 0 个字节的指针,和一个长度 5。它没有复制任何数据,也不拥有数据------所有权还在 s 手里,hello 和 world 只是借用了其中两段。所以切片和上一篇的引用受同一套规则约束:只读、不能和可变借用并存、必须活得比数据短。
区间语法值得注意:0..5 里的 5 是"结尾之后再往后的一个位置"(不包含),跟 Python 的 s[0:5] 一样。&s[..] 表示整段(相当于 0..len),&s[6..] 表示从 6 到末尾。
重写 first_word:返回切片
有了切片,first_word 可以重写为:
rust
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
返回类型从 usize 变成了 &str。安全性来自借用关系:返回值是借用,它和参数 s 之间有明确的关联(生命周期那条线,编译器自动推断,不用手写)。如果调用方拿到切片后去清空原字符串:
rust
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // 编译错误:s 正被 word 借用着
编译器直接拒绝。Book 的解释很直接:如果允许这行代码,clear 会释放内存,word 就成了悬垂引用。下标版本这种 bug 要等程序跑起来才炸;切片版本在编译期就被拦住了。这正是前几篇反复出现的同一个主题:把"程序员要自觉"的事,变成编译器强制检查的规则。
字符串字面量也是 &str
rust
let s = "hello";
这个 s 的类型是 &str,不是 String。它指向可执行文件里一段只读的数据,所以字符串字面量天然不可变------&str 本身就是不可变引用。上一篇说"&str 就是字符串切片",现在可以补全这句话:字符串字面量是一个"整个程序运行期间都有效"的切片(就是之前提过的 'static)。
不只是字符串:&T
切片不限于字符串。数组、Vec 都能切,语法一模一样:
rust
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 类型是 &[i32]
&[i32] 和 &str 结构相同:指向第一个元素的指针 + 长度。以后学到 Vec 时,&v[..] 就是把整个 Vec 变成切片的最常见写法。
String、&String、&str:三个类型一次分清
现在把三者摆在一起:
-
String:拥有数据。堆上有一块缓冲区,由它负责分配和释放(第一篇的 RAII)。 -
&str:不拥有 数据,只是一段字符串数据的引用,可能是String的一部分、字面量,或其他任何地方。 -
&String:指向一个String盒子的引用。
区别的实质:&str 指向"字符串数据本身",&String 指向"那个装着数据的盒子"。日常写函数参数,几乎总是用 &str,因为最通用:
rust
fn greet(name: &str) { println!("Hello, {name}!"); }
greet("world"); // 字面量直接传
let s = String::from("world");
greet(&s); // 传 &String 也能编译,为什么?
最后一行能过,是因为 String 实现了 Deref,目标类型是 str:编译器看到 &String 而参数要 &str 时,会自动做一次解引用强制转换 (deref coercion),把你写的 &s 悄悄变成 &s[..]。这个转换是编译期完成的,零运行时开销,而且只发生在参数类型明确需要的场合。
显式写法也有两种:s.as_str()(String 的方法,明确抽出整个字符串的切片)和 &s[..](通用的切片语法,对 String、Vec、数组都适用)。日常能靠隐式转换就靠隐式转换,需要写清楚时 as_str() 更直白。
一个必须记住的边界
&s[0..5] 是按字节索引的,切点必须落在字符边界上,否则直接 panic。"hello world" 全是 ASCII 没问题;中文这种多字节字符,一个字符占 3 个字节,随手写数字很容易切进字符中间。字符边界怎么处理(按字符而不是按字节索引)是字符串专题的内容,这里只需要记住:切片按字节切,别在非 ASCII 文本上直接写数字下标。
到这里,所有权、借用、生命周期、切片这条内存安全的主线就走完了:谁拥有、谁能借、借多久、借哪一段,全部由编译器在编译期盯死。下一篇我们进入"组织自己的数据"------用结构体(struct)把散落的变量组装成有名字的类型。
参考来源
-
The Rust Book 第 4.3 章「The Slice Type」--- 切片定义、&str 与 &T、first_word 重写、字面量即切片的权威出处:doc.rust-lang.org/book/ch04-0...
-
The Rust Book 第 15.2 章 --- deref coercion 的定义与 &String 到 &str 的自动转换:doc.rust-lang.org/book/ch15-0...
-
String 标准库文档 --- Deref<Target = str> 实现与 as_str() 方法说明:doc.rust-lang.org/std/string/...