文章目录
-
- 一、回顾与导入
- 二、为什么需要切片?
- [三、字符串切片(String Slice)](#三、字符串切片(String Slice))
-
- [3.1 创建字符串切片](#3.1 创建字符串切片)
- [3.2 Range 语法变体](#3.2 Range 语法变体)
- [3.3 字符串字面量本身就是切片](#3.3 字符串字面量本身就是切片)
- [3.4 用切片重写 first_word 函数](#3.4 用切片重写 first_word 函数)
- [四、字符串字面量与 String 的切片操作对比](#四、字符串字面量与 String 的切片操作对比)
- 五、数组切片
- 六、切片作为函数参数的最佳实践
-
- [6.1 使用 `&str` 而不是 `&String`](#6.1 使用
&str而不是&String) - [6.2 使用 `&T` 而不是 `&Vec<T>`](#6.2 使用
&[T]而不是&Vec<T>)
- [6.1 使用 `&str` 而不是 `&String`](#6.1 使用
- [七、切片边界与 UTF-8](#七、切片边界与 UTF-8)
- 八、完整代码示例
- 九、总结与速查
- 十、思考题
- 参考链接
掌握 Rust 中的切片 ------ 字符串切片、数组切片与切片作为参数
一、回顾与导入
前面两篇我们学习了 Rust 的所有权系统:
- 所有权规则、移动语义、Clone 与 Copy
- 引用与借用、可变引用、借用规则
今天我们来学习另一种不持有所有权 的数据类型 ------ 切片(Slice)。
💡 切片允许你引用集合中一段连续的元素序列,而不需要引用整个集合。
二、为什么需要切片?
来看一个需求:写一个函数,接收一个字符串,返回它第一个单词的长度。
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()
}
fn main() {
let mut s = String::from("hello world");
let word_len = first_word(&s);
println!("第一个单词长度: {}", word_len);
s.clear(); // 清空字符串
// word_len 仍然是 5,但 s 已经空了!这是 bug!
println!("清空后第一个单词长度: {}", word_len);
}
问题:word_len 与 s 的状态不同步。切片可以解决这个问题。
三、字符串切片(String Slice)
字符串切片是 &str 类型,它是对 String 或字符串字面量中一部分的引用。
3.1 创建字符串切片
rust
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // 取索引 0-4,不包含5 -> "hello"
let world = &s[6..11]; // 取索引 6-10 -> "world"
println!("{} {}", hello, world);
}
[start..end] 语法:
start:起始索引(包含)end:结束索引(不包含)- 索引必须落在 UTF-8 字符边界上
3.2 Range 语法变体
rust
fn main() {
let s = String::from("hello");
// 各种切片写法
let slice1 = &s[0..2]; // [0,2) -> "he"
let slice2 = &s[..2]; // 从开头到索引2 -> "he"
let slice3 = &s[2..]; // 从索引2到末尾 -> "llo"
let slice4 = &s[..]; // 整个字符串 -> "hello"
println!("{} {} {} {}", slice1, slice2, slice3, slice4);
}
3.3 字符串字面量本身就是切片
rust
fn main() {
let s: &str = "Hello, world!"; // s 的类型是 &str
// 字符串字面量直接存储在二进制中,类型就是 &str
}
3.4 用切片重写 first_word 函数
rust
fn first_word(s: &str) -> &str { // 参数改为 &str,更通用
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..] // 没有空格,返回整个字符串
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word 是切片,持有对 s 的引用
println!("第一个单词: {}", word); // hello
// s.clear(); // ❌ 如果取消注释会编译错误!
// 因为 word 还在借用 s,不能修改 s
println!("word 仍然有效: {}", word);
}
关键:切片引用了原字符串的一部分,借用检查器会确保切片有效期间原字符串不能被修改。
四、字符串字面量与 String 的切片操作对比
| 操作 | String | &str(字符串字面量/切片) |
|---|---|---|
| 创建 | String::from("hello") |
"hello" |
| 可变性 | 可变的(需 mut) |
不可变 |
| 切片 | &s[0..3] → &str |
&s[0..3] → &str |
| 内存位置 | 堆 | 二进制文件(静态存储区) |
| 所有权 | 拥有数据 | 借用数据 |
rust
fn main() {
let s1 = String::from("hello");
let s2 = "world"; // &str
let slice1 = &s1[1..3]; // "el"
let slice2 = &s2[1..3]; // "or"
println!("{} {}", slice1, slice2); // el or
}
五、数组切片
切片不仅适用于字符串,也适用于数组。
rust
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..4]; // 切片类型 &[i32],包含 [2, 3, 4]
println!("切片: {:?}", slice);
println!("第一个元素: {}", slice[0]);
println!("长度: {}", slice.len());
}
数组切片的类型 :&[T](T 是元素类型)
实用函数:使用数组切片
rust
// 计算切片中元素的和
fn sum_slice(slice: &[i32]) -> i32 {
slice.iter().sum()
}
// 找到切片中最大的元素
fn max_in_slice(slice: &[i32]) -> Option<&i32> {
slice.iter().max()
}
fn main() {
let numbers = [10, 20, 30, 40, 50];
let s1 = &numbers[..3]; // [10, 20, 30]
let s2 = &numbers[2..]; // [30, 40, 50]
println!("s1 的和: {}", sum_slice(s1)); // 60
println!("s2 的和: {}", sum_slice(s2)); // 120
if let Some(max) = max_in_slice(s1) {
println!("s1 的最大值: {}", max); // 30
}
}
六、切片作为函数参数的最佳实践
6.1 使用 &str 而不是 &String
rust
// ❌ 不够通用,只能接收 &String
fn bad(s: &String) -> &str {
&s[0..1]
}
// ✅ 更好,可以接收 &String 或 &str
fn good(s: &str) -> &str {
&s[0..1]
}
fn main() {
let s1 = String::from("hello");
let s2 = "world";
// bad(&s1); // 可以
// bad(s2); // ❌ 类型不匹配
good(&s1); // ✅
good(s2); // ✅
}
6.2 使用 &[T] 而不是 &Vec<T>
同理,对于数组/向量,使用 &[T] 更通用:
rust
// 更通用的函数签名
fn print_first_three(slice: &[i32]) {
for &item in slice.iter().take(3) {
println!("{}", item);
}
}
fn main() {
let arr = [1, 2, 3, 4, 5];
let vec = vec![1, 2, 3, 4, 5];
print_first_three(&arr); // ✅ 可以传数组引用
print_first_three(&vec); // ✅ 可以传 Vec 引用
}
七、切片边界与 UTF-8
重要:字符串切片必须落在 UTF-8 字符边界上,否则会 panic。
rust
fn main() {
let s = "你好,世界!"; // 中文字符每个占 3 字节
println!("字符串长度: {} bytes", s.len()); // 18
let slice1 = &s[0..3]; // ✅ "你"
println!("slice1: {}", slice1);
// let slice2 = &s[0..2]; // ❌ panic! 字节边界不对
// 因为'你'占3字节,0..2 不是一个完整的字符边界
}
安全做法 :使用 chars() 或 char_indices() 处理 UTF-8:
rust
fn first_word_utf8(s: &str) -> &str {
for (i, c) in s.char_indices() {
if c == ' ' {
return &s[0..i];
}
}
s
}
fn main() {
let s = "Rust 编程语言";
let word = first_word_utf8(s);
println!("第一个单词: {}", word); // Rust
}
八、完整代码示例
rust
fn main() {
println!("=== 1. 字符串切片基础 ===");
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
println!("hello = {}, world = {}", hello, world);
println!("\n=== 2. Range 语法简写 ===");
let slice = &s[..5];
println!("&s[..5] = {}", slice);
let slice = &s[6..];
println!("&s[6..] = {}", slice);
let slice = &s[..];
println!("&s[..] = {}", slice);
println!("\n=== 3. first_word 函数演示 ===");
let s = String::from("hello world");
let word = first_word(&s);
println!("第一个单词: {}", word);
// s.clear(); // 取消注释会编译错误
println!("\n=== 4. 数组切片 ===");
let arr = [10, 20, 30, 40, 50];
let arr_slice = &arr[1..4];
println!("数组切片: {:?}", arr_slice);
println!("切片和: {}", sum_slice(arr_slice));
println!("\n=== 5. 字符串字面量 ===");
let literal: &str = "Hello, Rust!";
let word = first_word(literal);
println!("字符串字面量的第一个单词: {}", word);
println!("\n=== 6. UTF-8 边界示例 ===");
let chinese = "你好世界";
println!("字符串: {}", chinese);
let first_char = &chinese[0..3];
println!("第一个字符: {}", first_char);
}
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[..]
}
fn sum_slice(slice: &[i32]) -> i32 {
let mut sum = 0;
for num in slice {
sum += num;
}
sum
}
九、总结与速查
| 概念 | 语法 | 类型 | 说明 |
|---|---|---|---|
| 字符串切片 | &s[start..end] |
&str |
对字符串部分的引用 |
| 数组切片 | &a[start..end] |
&[T] |
对数组部分的引用 |
| 全范围切片 | &s[..] |
&str/&[T] |
整个集合的引用 |
| 字符串字面量 | "hello" |
&str |
本身就是切片 |
核心要点:
- ✅ 切片是一种不持有所有权的引用
- ✅ 字符串切片类型是
&str,数组切片类型是&[T] - ✅ 函数参数优先使用
&str而非&String,使用&[T]而非&Vec<T> - ✅ 切片让借用检查器能确保数据一致性
- ✅ UTF-8 字符串切片必须在字符边界上
十、思考题
- 以下代码会输出什么?为什么?
rust
fn main() {
let s = String::from("Rust");
let slice = &s[1..3];
println!("{}", slice);
}
- 为什么下面的代码不能编译?
rust
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear();
println!("{}", word);
}
&String和&str有什么区别?什么时候应该用哪一个?