切片类型:字符串切片、数组切片、切片作为参数

切片,说白了就是不拥有所有权的「视图」。
它让你能引用集合中的一段连续元素,而不必把整个集合都拿过来。
我刚开始学 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 的设计哲学:安全、高效、零开销。


专注资料整理