Rust: Rust 切片(Slice):高效的数据视图与字符串切片
最后更新:2026-08-26
切片(Slice)是 Rust 中一种特殊引用——它不指向整个数据,而是指向其中的一段连续区域。
切片让你能安全地"裁剪"出数据的一部分来操作,而不需要复制数据。
1. 你将学到
- 字符串切片
&str的概念与创建 - 使用
[start..end]范围语法创建切片 - 字符串切片必须落在 UTF-8 字符边界
- 数组切片
&[T]与向量切片&[Vec] - 切片作为函数参数——最灵活的字符串输入类型
- 切片的内部表示:指针 + 长度
2. 概念图解
flowchart LR
subgraph "堆内存"
HEAP["'H','e','l','l','o','','R','u','s','t','!'"]
end
subgraph "String s"
S_PTR["ptr"] --> HEAP
S_LEN["len: 12"]
S_CAP["cap: 12"]
end
subgraph "&str slice"
SL_PTR["ptr"] -->|"指向前5字节"| HEAP
SL_LEN["len: 5"]
end
3. 一个数据分析师的故事
(1) 痛苦:从日志中提取信息太慢
Maria 是一家电商公司的数据分析师。她需要从海量服务器日志中提 IP 地址:
- 日志文件每行 tens of thousands of characters
- 如果用
substring复制,每条日志需要多分配几千字节内存 - 一天处理 1,000,000 条日志,多分配了 几个 GB 额外内存
- 服务器因此 OOM(内存溢出)崩溃了一次
"我需要一种"只看不复制"的方式来提取数据——就像用手指划过报纸上的一行字,不需要把字抄下来。"
(2) Rust 切片的方案
RUST
fn main() {
let log_line = "192.168.1.1 - - [01/Jul/2026:12:00:00] \"GET /index.html\"";
// 切片——不复制,只是"指向"原字符串的一部分
let ip = &log_line[0..13]; // "192.168.1.1"
let date = &log_line[20..38]; // "01/Jul/2026:12:00:00"
let path = &log_line[50..62]; // "/index.html"
println!("IP: {}", ip);
println!("日期: {}", date);
println!("路径: {}", path);
// 重点:没有复制任何字符串!所有切片都指向 log_line 的不同区域
println!("原始日志仍然可用: {}", log_line);
}
切片就像一个"视窗",指向原数据的不同窗口。没有复制,没有分配——零开销。这在处理大规模数据时至关重要。
4. 切片的原理
(1) 内存模型
RUST
let s = String::from("Hello, Rust!");
let slice = &s[0..5]; // "Hello"
graph TB
subgraph "String s"
S_ptr[ptr ──→ H e l l o , R u s t !]
S_len[len: 12]
S_cap[cap: 12]
end
subgraph "&str slice"
SL_ptr[ptr ──→ H e l l o]
SL_len[len: 5]
end
S_ptr -.-> heap[堆上数据]
SL_ptr -.-> heap
| 特性 | 整个 String |
&s[0..5] 切片 |
|---|---|---|
| 内存 | ptr + len + cap(3 个字) | ptr + len(2 个字) |
| 所有权 | 有所有权 | 借用(引用) |
| 是否复制数据 | — | 不复制 |
| 访问范围 | 整个字符串 | "Hello" 5 个字节 |
(2) 范围语法
| 写法 | 含义 | 示例 |
|---|---|---|
[0..5] |
0 到 5(不含 5) | "Hello" |
[..5] |
从开头到 5 | "Hello" |
[5..] |
从 5 到结尾 | ", Rust!" |
[..] |
整个字符串 | "Hello, Rust!" |
(3) 切片类型速查
| 切片类型 | 写法 | 大小 | 说明 |
|---|---|---|---|
| 字符串切片 | &str |
16 字节 | ptr + len(胖指针) |
| 数组切片 | &[T] |
16 字节 | ptr + len(胖指针) |
| 数组引用 | &[T; N] |
8 字节 | 仅 ptr(编译时已知长度) |
| 可变切片 | &mut [T] |
16 字节 | ptr + len(可修改元素) |
5. 切片示例
▶ 示例 1:字符串切片(难度 ⭐)
RUST
// ============================================
// 字符串切片的基本用法
// ============================================
fn main() {
let s = String::from("Hello, Rust World!");
// 各种切片方式
let hello = &s[..5]; // "Hello"
let rust = &s[7..11]; // "Rust"
let world = &s[12..]; // "World!"
let full = &s[..]; // 全部
println!("hello: '{}'", hello);
println!("rust: '{}'", rust);
println!("world: '{}'", world);
println!("full: '{}'", full);
// 字符串字面量本身就是 &str
let literal: &str = "直接创建切片";
let first_word = &literal[..2];
println!("字面量切片: '{}'", first_word);
}
输出:
TEXT
📖 仅展示
hello: 'Hello'
rust: 'Rust'
world: 'World!'
full: 'Hello, Rust World!'
字面量切片: '直接'
字符串字面量(如
"hello")类型本身就是&str——一个指向二进制文件的切片。它不需要&前缀,因为它已经是引用了。
▶ 示例 2:字符串切片边界陷阱(难度 ⭐⭐)
RUST
// ============================================
// UTF-8 边界:切片必须在字符边界上
// ============================================
fn main() {
let s = "Rust编程"; // 字节数: R(1) u(1) s(1) t(1) 编(3) 程(3) = 10 字节
// 安全切片
let rust = &s[..4]; // "Rust"(前 4 个字节刚好是 ASCII)
println!("Rust 部分: {}", rust);
// 以下是错误示例(取消注释运行会 panic!)
// let bad = &s[0..5]; // ❌ 5 在 4 和 6 之间,落在"编"的第一个字节中间
// 安全的做法:用 chars() 和 char_indices() 遍历
for (i, c) in s.char_indices() {
println!("字节索引 {}: 字符 '{}'", i, c);
}
// 用 char_indices 找到安全的切片边界
if let Some((pos, _)) = s.char_indices().nth(4) {
let safe_slice = &s[..pos];
println!("前 4 个字符切片: {}", safe_slice);
}
}
输出:
TEXT
📖 仅展示
Rust 部分: Rust
字节索引 0: 字符 'R'
字节索引 1: 字符 'u'
字节索引 2: 字符 's'
字节索引 3: 字符 't'
字节索引 4: 字符 '编'
字节索引 7: 字符 '程'
前 4 个字符切片: Rust编程
切片是按字节索引,不是按字符索引。如果切片边界落在多字节字符中间,程序会 panic 崩溃。用
.char_indices()获取安全的字符边界索引。
▶ 示例 3:数组切片与向量切片(难度 ⭐⭐)
RUST
// ============================================
// 数组和向量的切片
// ============================================
fn main() {
// 数组切片
let arr = [1, 2, 3, 4, 5];
let slice_arr = &arr[1..4]; // [2, 3, 4]
println!("数组切片: {:?}", slice_arr);
// 向量切片
let vec = vec![10, 20, 30, 40, 50];
let slice_vec = &vec[..3]; // [10, 20, 30]
println!("向量切片(前3个): {:?}", slice_vec);
// 修改切片内容(需要 &mut)
let mut numbers = vec![1, 2, 3, 4, 5];
let mut_slice = &mut numbers[1..4]; // [2, 3, 4]
mut_slice[0] = 99; // 修改切片会影响原向量
println!("修改后原向量: {:?}", numbers);
// 切片类型大小
println!("&[i32] 占用: {} 字节", std::mem::size_of::<&[i32]>());
println!("&[i32;5] 占用: {} 字节", std::mem::size_of::<&[i32; 5]>());
}
输出:
TEXT
📖 仅展示
数组切片: [2, 3, 4]
向量切片(前3个): [10, 20, 30]
修改后原向量: [1, 99, 3, 4, 5]
&[i32] 占用: 16 字节
&[i32;5] 占用: 8 字节
切片类型(
&[i32])占用 16 字节(8 字节指针 + 8 字节长度),而普通引用(&[i32;5])只占 8 字节(只有指针)。这就是"胖指针"——切片是带有长度信息的指针。
▶ 示例 4:切片作为函数参数(难度 ⭐⭐)
RUST
// ============================================
// &str 作为函数参数——最灵活的方式
// ============================================
// ✅ 最佳实践:接收 &str——无论传入 &str 还是 &String 都可以
fn first_word(s: &str) -> &str {
for (i, &b) in s.as_bytes().iter().enumerate() {
if b == b' ' {
return &s[..i];
}
}
&s[..] // 没有空格,返回整个字符串
}
fn main() {
// 传入 &str 字面量
let result1 = first_word("hello world");
println!("字面量第一个单词: '{}'", result1);
// 传入 &String(自动转为 &str)
let s = String::from("Rust is awesome");
let result2 = first_word(&s); // &String 自动转换为 &str
println!("String 第一个单词: '{}'", result2);
// 传入数组切片
let arr = [1, 2, 3, 4, 5];
let sum: i32 = sum_slice(&arr[..]); // 用 [..] 把数组转成切片
println!("数组切片和: {}", sum);
}
fn sum_slice(slice: &[i32]) -> i32 {
let mut total = 0;
for x in slice {
total += *x;
}
total
}
输出:
TEXT
📖 仅展示
字面量第一个单词: 'hello'
String 第一个单词: 'Rust'
数组切片和: 15
最佳实践:函数参数接收字符串时用
&str而不是&String。&String会自动转为&str(通过 Deref 强制转换),因此&str参数更通用——既接受字面量,也接受 String 引用。
▶ 示例 5:综合练习——日志解析器(难度 ⭐⭐⭐)
RUST
// ============================================
// 切片实战:零拷贝日志解析
// ============================================
fn parse_log_line(line: &str) -> (&str, &str, &str) {
let ip_end = line.find(' ').unwrap_or(line.len());
let ip = &line[..ip_end];
let rest = &line[ip_end..].trim_start();
let method_end = rest.find(' ').unwrap_or(rest.len());
let method = &rest[..method_end];
let path_part = &rest[method_end..].trim_start();
let path_end = path_part.find(' ').unwrap_or(path_part.len());
let path = &path_part[..path_end];
(ip, method, path)
}
fn find_longest<'a>(strings: &[&'a str]) -> &'a str {
strings.iter().max_by_key(|s| s.len()).unwrap_or(&"")
}
fn summarize(data: &[i32]) -> (f64, i32, i32) {
if data.is_empty() {
return (0.0, 0, 0);
}
let sum: i32 = data.iter().sum();
let avg = sum as f64 / data.len() as f64;
let min = *data.iter().min().unwrap();
let max = *data.iter().max().unwrap();
(avg, min, max)
}
fn main() {
let logs = [
"192.168.1.1 GET /index.html HTTP/1.1",
"10.0.0.5 POST /api/login HTTP/1.1",
"172.16.0.1 DELETE /api/user/42 HTTP/1.1",
];
println!("=== 日志解析(零拷贝)===");
for log in &logs {
let (ip, method, path) = parse_log_line(log);
println!("IP: {:<15} 方法: {:<6} 路径: {}", ip, method, path);
}
println!("\n=== 最长路径 ===");
let paths: Vec<&str> = logs.iter().map(|l| {
let (_, _, p) = parse_log_line(l);
p
}).collect();
println!("最长路径: '{}'", find_longest(&paths));
println!("\n=== 数值切片统计 ===");
let scores = [85, 92, 78, 95, 88, 70, 96];
let (avg, min, max) = summarize(&scores);
println!("成绩切片: {:?}", scores);
println!("平均: {:.1}, 最低: {}, 最高: {}", avg, min, max);
}
输出:
TEXT
📖 仅展示
=== 日志解析(零拷贝)===
IP: 192.168.1.1 方法: GET 路径: /index.html
IP: 10.0.0.5 方法: POST 路径: /api/login
IP: 172.16.0.1 方法: DELETE 路径: /api/user/42
=== 最长路径 ===
最长路径: '/api/user/42'
=== 数值切片统计 ===
成绩切片: [85, 92, 78, 95, 88, 70, 96]
平均: 86.3, 最低: 70, 最高: 96
日志解析中,
parse_log_line返回的三个&str都指向原始字符串的不同区域——零拷贝、零分配。find_longest接收&[&str]切片,通用性极强。数值切片的统计函数同样只借用数据。
❓ 常见问题
Q 切片和引用有什么区别?
A 切片是胖指针(指针 + 长度),普通引用只是指针。
Q 字符串切片 &str 和 String 有什么关系?
A &str 可以看作 String 的一部分视图。
Q 为什么 &s[0..5] 在中文上会 panic?
A 因为切片是按字节索引,中文 UTF-8 编码占 3 字节。
Q &str 作为参数和 &String 作为参数,选哪个?
A 永远选 &str。
Q vec![1,2,3] 的切片 &vec[..] 创建新数据了吗?
A 没有,零开销。
📖 小节
- 切片(Slice) 是数据的"视图",不拥有数据,不复制数据
- 字符串切片
&str指向 String 或字面量的一段 - 范围语法:
[start..end]、[..end]、[start..]、[..] - 切片是胖指针(指针 + 长度),占用 16 字节
- 字符串切片必须落在 UTF-8 字符边界上
- 函数参数接收字符串用
&str(比&String更通用) - 数组切片
&[T]和向量切片用法相同
📝 作业
- 难度 ⭐:创建一个
String,用[..]切片获取整个字符串,验证切片的类型是&str。 - 难度 ⭐⭐:写一个函数
fn last_word(s: &str) -> &str,返回最后一个单词(按空格分割)。分别在字符串字面量和String上调用它。 - 难度 ⭐⭐⭐:创建一个包含中文和 emoji 的字符串
"Rust🦀编程",用.char_indices()找到安全切片边界,只提取 "Rust🦀" 部分(提示:🦀 占 4 字节)。