切片:指向一段数据,而不是全部

切片:指向一段数据,而不是全部

用 &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 手里,helloworld 只是借用了其中两段。所以切片和上一篇的引用受同一套规则约束:只读、不能和可变借用并存、必须活得比数据短。

区间语法值得注意: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[..](通用的切片语法,对 StringVec、数组都适用)。日常能靠隐式转换就靠隐式转换,需要写清楚时 as_str() 更直白。

一个必须记住的边界

&s[0..5] 是按字节索引的,切点必须落在字符边界上,否则直接 panic。"hello world" 全是 ASCII 没问题;中文这种多字节字符,一个字符占 3 个字节,随手写数字很容易切进字符中间。字符边界怎么处理(按字符而不是按字节索引)是字符串专题的内容,这里只需要记住:切片按字节切,别在非 ASCII 文本上直接写数字下标。

到这里,所有权、借用、生命周期、切片这条内存安全的主线就走完了:谁拥有、谁能借、借多久、借哪一段,全部由编译器在编译期盯死。下一篇我们进入"组织自己的数据"------用结构体(struct)把散落的变量组装成有名字的类型。


参考来源

相关推荐
阿翰3 小时前
MiniPdf Office 转 PDF 支持 Rust 原生实现:3 MB 单文件部署、低内存
rust·minipdf
小灰灰搞电子3 小时前
Rust+Slint 实现圆形变色按钮源码分享
rust·slint·变色按钮
flyxl3 小时前
DataZen 架构设计(一):一个现代桌面数据库工具是如何构建的
rust
k4m7v2pz3 小时前
用 rust-verb-shell 重写进程管理:从 game.sh 到 .rvs 的迁移实录
开发语言·后端·rust
mldong15 小时前
同一份 15 个流程 JSON,第六种语言也跑通了:工作流引擎 Rust 移植实录
架构·rust
Naylor2 天前
只借不占:Rust 的引用与借用
后端·rust
对象存储与RustFS2 天前
RustFS 后台扫描器与自愈调参:五档速度、位腐检测周期与并发上限
后端·rust·开源
q平面人3 天前
【总工笔记】mindoc平板、桌面端笔记软件,发布到mindoc服务端
rust·tauri·华为平板·mindoc·总工笔记
MC皮蛋侠客3 天前
Tauri 2.x 系列(九):安全模型——Capabilities、Permissions、Scope 与 CSP
rust·tauri