切片类型:字符串切片、数组切片、切片作为参数
切片,说白了就是不拥有所有权的「视图」。
它让你能引用集合中的一段连续元素,而不必把整个集合都拿过来。
我刚开始学 Rust 时,总觉得切片这个概念有点绕——明明有数组、有字符串,为什么还要多一个切片?
后来在项目里处理大文件解析时,我才真正体会到它的价值。
字符串切片
字符串切片,是对 String 或 &str 中某一部分的引用。
语法很简单:&s[start..end]。
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
注意,切片索引是字节偏移量,不是字符索引。
如果你切到中文字符的中间,程序会直接 panic。
嗯,这里要特别小心——我在做中文分词工具时,就踩过这个坑。
其实字符串字面量 "hello" 本身就是一个 &str 切片。
它指向二进制文件中固定位置的字符串数据。
所以你可以这样写:
let s: &str = "hello world"; // 这已经是切片了
数组切片
数组切片和字符串切片原理一样,只是元素类型不同。
它让你能引用数组的一部分,而不复制数据。
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4]; // [2, 3, 4]
数组切片的类型是 &[i32],注意不是 &[i32; 5]。
前者是动态大小类型(DST),后者是固定大小数组。
我个人习惯把切片看作「胖指针」——它包含两个信息:指向数据的指针,以及长度。
为什么切片这么重要?
你想想看,如果你要写一个函数,处理任意长度的数组片段——
用固定数组做参数,那只能处理特定大小。
用切片做参数,任何数组、向量、甚至其他切片都能传进来。
切片作为参数
这是切片最实用的场景。
函数参数写成 &[T],就能接收各种集合的切片。
fn sum_slice(slice: &[i32]) -> i32 {
let mut total = 0;
for &item in slice {
total += item;
}
total
}
fn main() {
let arr = [1, 2, 3, 4, 5];
let vec = vec![10, 20, 30];
println!("{}", sum_slice(&arr)); // 15
println!("{}", sum_slice(&vec)); // 60
println!("{}", sum_slice(&arr[1..4])); // 9
}
看到了吗?同一个函数,数组、向量、切片片段都能用。
这就是切片的威力——抽象掉底层容器。
我曾经在重构一个数据处理模块时,把所有接收 &Vec<T> 的参数改成 &[T]。
结果调用方从数组、向量、甚至文件映射的切片都能传进来,代码复用率直接翻倍。
&[T] 而不是 &Vec<T>。前者更通用,后者会限制调用方必须传向量。
字符串切片 vs 数组切片
| 特性 | 字符串切片 | 数组切片 |
|---|---|---|
| 类型 | &str |
&[T] |
| 元素 | UTF-8 字节 | 任意类型 T |
| 索引单位 | 字节(注意字符边界) | 元素索引 |
| 常见来源 | String、字符串字面量 |
数组、Vec、其他切片 |
| 是否可修改 | 不可变(&str) |
可变(&mut [T]) |
这里有个细节:字符串切片默认是不可变的。
如果你想修改字符串中的某些字符,得用 &mut str,但这种情况很少见。
我建议你优先用 &str,除非你明确需要原地修改。
切片与生命周期
切片是引用,自然涉及生命周期。
最常见的场景:函数返回切片时,必须保证切片引用的数据在返回后仍然有效。
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[..]
}
这个函数返回的切片生命周期,和输入参数 s 绑定在一起。
Rust 编译器会自动推断:返回的引用不能比输入引用活得更久。
嗯,这里不用你手动标注生命周期,编译器会帮你搞定。
切片与所有权
切片不拥有数据,所以你不能通过切片释放数据。
但你可以通过切片修改数据——前提是切片是可变的,并且没有其他引用。
let mut arr = [1, 2, 3, 4, 5];
{
let slice = &mut arr[1..4];
slice[0] = 100; // 修改 arr[1]
}
println!("{:?}", arr); // [1, 100, 3, 4, 5]
注意花括号的作用:限制可变切片的作用域。
一旦切片离开作用域,原来的数组又可以正常访问了。
这是 Rust 借用检查器的典型用法——我刚开始写时经常忘记加作用域,导致编译错误。
切片与模式匹配
切片可以和模式匹配结合,实现一些很优雅的写法。
fn process_slice(slice: &[i32]) {
match slice {
[] => println!("空切片"),
[a] => println!("只有一个元素: {}", a),
[a, b] => println!("两个元素: {}, {}", a, b),
[first, middle @ .., last] => {
println!("首: {}, 尾: {}, 中间: {:?}", first, last, middle);
}
}
}
这个 .. 语法叫「剩余模式」,匹配中间所有元素。
我个人觉得这是 Rust 最酷的特性之一——用模式匹配处理切片边界情况,代码既安全又简洁。
.. 匹配切片剩余部分,可以避免手动计算索引。这在处理「首尾元素 + 中间部分」的场景特别有用。
切片与性能
切片操作是 O(1) 的——它只是创建了一个新的胖指针,不复制数据。
所以你可以放心地切来切去,不用担心性能开销。
但要注意:切片索引越界会 panic。
如果你不确定索引是否合法,用 get 方法返回 Option:
let arr = [1, 2, 3];
let slice = arr.get(0..5); // 返回 None,不会 panic
let slice = &arr[0..5]; // 直接 panic
我在生产代码中,凡是涉及用户输入或外部数据的切片操作,一律用 get。
安全第一,panic 留给调试阶段就好。
总结一下
切片是 Rust 中「零成本抽象」的典范。
它让你安全地引用集合的一部分,而不引入所有权转移或数据复制。
记住三个要点:
- 切片是引用,不拥有数据
- 切片是胖指针,包含地址和长度
- 函数参数优先用切片,提高通用性
你想想看,如果没有切片,你要怎么处理「只读数组的一部分」?
要么复制一份(浪费内存),要么传索引范围(容易越界)。
切片把这两个问题都解决了——这就是 Rust 的设计哲学:安全、高效、零开销。