Skip to content

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)-
rust
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 类型的变量:

rust
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]:

rust
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]: 可以通过切片修改源数据中对应的元素
rust
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 是两种不同的类型(在 字符串 章节将详细介绍):

rust
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 可以接受更多类型:

rust
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 大小写的方法:

rust
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:

rust
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],因此可以直接将数组引用当作切片来使用:

rust
let arr = [11, 22, 33, 44];
let slice: &[i32] = &arr;               // &[i32; 4] 自动转换为 &[i32]

println!("{}", slice.first().unwrap()); // 11

由于 . 运算符会自动创建引用,数组也可以直接调用切片的所有方法:

rust
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检查是否以某切片结尾

方法示例:

rust
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,字符串切片有其专属方法集。

切片与借用规则

切片是一种借用行为,受借用规则约束:

  • 同一时刻,同一数据只能有一个可变切片,或任意数量的不可变切片
  • 可变切片与不可变切片不能同时存在
rust
let mut arr = [1, 2, 3];
let r1 = &arr[..];        // 不可变切片
let r2 = &mut arr[..];    // ❌ 不可变切片存在时,不能创建可变切片
println!("{:?}", r1);
let r3 = &mut arr[..];    // ✅ r1 已不再使用,可以创建可变切片
r3[0] = 10;

基于 MIT 协议发布