Slice 切片
Rust 中的切片(Slice)是对某段连续内存数据的引用,不持有所有权。切片不仅是一种操作,也是一种独立的原始数据类型。
初读这一章你可能会有点迷惑,当你学习完 字符串 一章后,你会有更清晰的认知,不要着急,你先记住切片是一种操作,切片后会产生一个新的数据类型,叫切片类型。
切片操作语法
假设 s 是可被切片的数据,切片语法如下:
| 语法 | 含义 | 数学表示 | 是否包含终点 |
|---|---|---|---|
s[n1..n2] | 从 n1 到 n2(不含 n2) | [n1, n2) | 不包含 |
s[n1..=n2] | 从 n1 到 n2(含 n2) | [n1, n2] | 包含 |
s[n1..] | 从 n1 到末尾 | [n1, len) | - |
s[..n2] | 从开头到 n2(不含 n2) | [0, n2) | 不包含 |
s[..] | 全部元素 | [0, len) | - |
let s = String::from("hello world");
let hello = &s[..5]; // [..5] = [0..5] = "hello"
let world = &s[6..]; // [6..] = [6..s.len()] = "world"
let all = &s[..]; // [..] = [0..s.len()] = "hello world"切片的索引边界可以使用 usize 类型的变量:
let arr = [11, 22, 33, 44, 55];
let n: usize = 3;
let slice = &arr[..n]; // ✅ 取前 3 个元素切片作为数据类型
切片不只是一种操作,Rust 还将其上升为独立的原始数据类型,描述方式为 [T],其中 T 是元素类型。例如对 i32 数组切片,得到的类型是 [i32]。
切片类型 [T] 与切片引用 &[T]
[T] 是切片类型(DST,编译时大小未知),&[T] 是切片的引用类型(胖指针,固定 16 字节)。
由于切片的长度在编译时无法确定(边界可能是运行时变量),编译器不允许直接使用 [T],在 Rust 中几乎总是使用切片的引用 &[T]:
let arr = [11, 22, 33, 44, 55];
let n: usize = 3;
let slice_direct: [i32] = arr[0..n]; // ❌ 编译时大小未知,无法直接使用
let slice_ref: &[i32] = &arr[0..n]; // ✅ 通过引用使用切片,大小固定为 16 字节(胖指针)虽然
[T]和&[T]概念上有区别,但因为实际中几乎总是通过引用使用切片,所以日常讨论中两者经常混用,统称"切片"。
不可变切片与可变切片
- 不可变切片
&[T]: 只读,不能通过切片修改源数据 - 可变切片
&mut [T]: 可以通过切片修改源数据中对应的元素
let mut arr = [11, 22, 33, 44];
let slice1 = &arr[..2]; // 不可变切片
println!("{:?}", slice1); // [11, 22]
let slice2 = &mut arr[..2]; // 可变切片
slice2[0] = 1111; // 通过切片修改源数据
println!("{:?}", slice2); // [1111, 22]
println!("{:?}", arr); // [1111, 22, 33, 44]可切片的数据类型
只要数据在内存中连续存储,就可以对其进行切片操作:
| 源类型 | 切片引用类型 | 说明 |
|---|---|---|
[T; N](数组) | &[T] | 固定大小数组 |
Vec<T> | &[T] | 动态数组 |
String | &str | 字符串切片,类型名特殊 |
str &str | &str | 对字符串字面量或切片再切片 |
注意区分数组和切片的类型写法:
- 数组类型:
[T; N],数组引用:&[T; N] - 切片类型:
[T],切片引用:&[T]
字符串切片 str 的特殊性
String 的切片类型是 str(引用形式为 &str),而不是 &[String];
&str 和 &String 是两种不同的类型(在 字符串 章节将详细介绍):
let s = String::from("hello world!");
let s1 = &s[6..]; // s1 的类型是 &str(字符串切片)
let s2 = &s; // s2 的类型是 &String(字符串的引用)内存布局上,&str 是一个胖指针,直接指向字符串数据的某个位置;&String 则是指向栈上 String 结构体(句柄,超胖指针)的普通指针,再间接指向堆上数据。
&str 优于 &String
函数参数应优先使用 &str 而非 &String,因为 &str 可以接受更多类型:
fn print_message(msg: &str) {
println!("{}", msg);
}
let s = String::from("hello");
print_message(&s); // ✅ &String 自动强制转换为 &str
print_message("world"); // ✅ 字符串字面量直接作为 &str
String实现了 Deref 隐式转换,因此&String可以自动转换为&str,但反过来不行。使用&str作为参数类型更通用,兼容String的借用。
字符串切片的修改限制
为保证字符串总是有效的 UTF-8 编码,Rust 不允许通过切片随意修改字符串字符。&mut str 仅提供了三个修改 ASCII 大小写的方法:
let mut s = String::from("HELLO");
let ss = &mut s[..];
// 1. 将 ASCII 字符转换为小写
ss.make_ascii_lowercase();
println!("{}", s); // hello
// 2. 将 ASCII 字符转换为大写
ss.make_ascii_uppercase();
println!("{}", s); // HELLO
// 3. 获取可变字节切片,可以修改字节内容,但必须保证仍然是有效的 UTF-8 编码
unsafe{
ss.as_bytes_mut()
.iter_mut()
.for_each(|b| *b = b'a'); // 将所有字节改为 'a'
}
println!("{}", s); // aaaaa除此之外,没有其他原地修改字符串切片内容的方法。
UTF-8 字节边界
对字符串切片时,索引必须落在合法的 UTF-8 字符边界上,否则会在运行时 panic:
let s = "你好world";
// let bad = &s[0..2]; // ❌ panic: "你"是 3 字节,在字符中间切片
let you = &s[0..3]; // ✅ "你" 恰好 3 字节
// 推荐: 使用迭代器避免手动计算字节偏移
let first: String = s.chars().take(1).collect(); // "你"数组自动转换为切片
数组引用 &[T; N] 可以自动强制转换为切片引用 &[T],因此可以直接将数组引用当作切片来使用:
let arr = [11, 22, 33, 44];
let slice: &[i32] = &arr; // &[i32; 4] 自动转换为 &[i32]
println!("{}", slice.first().unwrap()); // 11由于 . 运算符会自动创建引用,数组也可以直接调用切片的所有方法:
let arr = [11, 22, 33, 44];
println!("{}", arr.first().unwrap()); // 11,等价于 (&arr).first()常用切片方法
切片提供了丰富的内置方法,以下是常用方法对照表:
| 方法 | 签名 | 说明 |
|---|---|---|
len() | fn len(&self) -> usize | 返回切片中元素的个数 |
is_empty() | fn is_empty(&self) -> bool | 检查切片是否为空 |
contains() | fn contains(&self, x: &T) -> bool | 检查切片是否包含某个元素 |
repeat() | fn repeat(&self, n: usize) -> Vec<T> | 将切片元素重复 n 次 |
reverse() | fn reverse(&mut self) | 反转切片(需要可变) |
swap() | fn swap(&mut self, a: usize, b: usize) | 交换两个索引处的元素 |
windows() | fn windows(&self, size: usize) | 返回滚动窗口迭代器 |
join() | fn join(&self, sep: &T) -> Vec<T> | 连接元素并返回向量 |
starts_with() | fn starts_with(&self, needle: &[T]) -> bool | 检查是否以某切片开头 |
ends_with() | fn ends_with(&self, needle: &[T]) -> bool | 检查是否以某切片结尾 |
方法示例:
let arr = [11, 22, 33, 44, 55];
println!("{}", arr.len()); // 5,元素个数
println!("{}", arr.is_empty()); // false,是否为空
println!("{}", arr.contains(&22)); // true,是否包含某元素
println!("{:?}", arr.repeat(2)); // [11,22,33,44,55,11,22,33,44,55]
// reverse(): 反转(需要可变)
let mut arr2 = [11, 22, 33];
arr2.reverse();
println!("{:?}", arr2); // [33, 22, 11]
// swap(): 交换两个索引处的元素
let mut arr3 = [1, 2, 3, 4];
arr3.swap(1, 2);
println!("{:?}", arr3); // [1, 3, 2, 4]
// windows(): 滚动窗口迭代
let arr4 = [10, 20, 30, 40];
for w in arr4.windows(2) {
println!("{:?}", w); // [10,20], [20,30], [30,40]
}
// join(): 连接元素
println!("{}", ["hello", "world"].join(" ")); // hello world
println!("{:?}", [[1, 2], [3, 4]].join(&0)); // [1, 2, 0, 3, 4]
// starts_with() / ends_with()
println!("{}", arr.starts_with(&[11, 22])); // true
println!("{}", arr.ends_with(&[55])); // true更多方法参见 标准库 slice。
注意: 大部分切片方法不适用于
&str,字符串切片有其专属方法集。
切片与借用规则
切片是一种借用行为,受借用规则约束:
- 同一时刻,同一数据只能有一个可变切片,或任意数量的不可变切片
- 可变切片与不可变切片不能同时存在
let mut arr = [1, 2, 3];
let r1 = &arr[..]; // 不可变切片
let r2 = &mut arr[..]; // ❌ 不可变切片存在时,不能创建可变切片
println!("{:?}", r1);
let r3 = &mut arr[..]; // ✅ r1 已不再使用,可以创建可变切片
r3[0] = 10;