9·所有权系统入门

切片(Slice)

切片(Slice)

学习目标

  1. 理解字符串切片 &str
  2. 掌握数组切片 &[T]
  3. 理解切片与所有权的关系
  4. 掌握切片的常用方法

核心概念

字符串切片

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5];    // "hello"
    let world = &s[6..11];   // "world"

    // 简写
    let hello = &s[..5];     // 从开头到 5(不含)
    let world = &s[6..];     // 从 6 到结尾
    let whole = &s[..];      // 整个字符串
}

内存布局

s (String)       堆内存
┌──────────┐    ┌─────────────────┐
│ ptr ────────→ │ h │ e │ l │ l │ o │   │ w │ o │ r │ l │ d │
│ len: 11  │    └─────────────────┘
│ cap: 11  │
└──────────┘

&str 切片 (栈)   指向堆内存的一部分
┌──────────┐
│ ptr ────────→ [h, e, l, l, o]
│ len: 5   │
└──────────┘

&str vs String

fn main() {
    let s1: String = String::from("hello");      // 堆分配,可变,拥有所有权
    let s2: &str = "hello";                       // 字符串字面量,编译进二进制
    let s3: &str = &s1[..];                       // String 的切片
    let s4: &str = s1.as_str();                   // 同上
}
类型存储可变所有权
String拥有
&str栈指向堆/只读内存借用

切片作为函数参数

// 推荐:接受 &str,可以同时接受 String 和 &str
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i];
        }
    }
    s
}

fn main() {
    let s = String::from("hello world");
    let word = first_word(&s);       // String → &str
    println!("{}", word);

    let word = first_word("hi there"); // 字面量 → &str
    println!("{}", word);
}

数组切片

fn sum(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

fn main() {
    let arr = [1, 2, 3, 4, 5];

    let slice = &arr[1..3];   // [2, 3],类型是 &[i32]
    println!("{:?}", slice);

    let total = sum(&arr);    // 整个数组的切片
    println!("sum = {}", total);

    let partial = sum(&arr[2..]);  // [3, 4, 5]
    println!("partial sum = {}", partial);
}

切片方法

fn main() {
    let s = String::from("hello world");

    // 查找
    let first_space = s.find(' ');            // Some(5)
    let (first, second) = s.split_at(5);      // ("hello", " world")

    // 分割
    let words: Vec<&str> = s.split_whitespace().collect();
    // ["hello", "world"]

    // 字符串切片的方法
    let hello = &s[..5];
    println!("len: {}", hello.len());         // 5
    println!("is_empty: {}", hello.is_empty()); // false
    println!("starts_with: {}", hello.starts_with("hel")); // true
}

UTF-8 切片注意

fn main() {
    let s = String::from("你好世界");

    // ❌ panic: 中文字符是 3 字节,不能在字节中间切
    // let hello = &s[0..2];

    // ✅ 按字符边界切
    let hello = &s[0..6];    // "你好"(每个中文字符 3 字节)
    println!("{}", hello);

    // 安全的做法:按字符操作
    for c in s.chars() {
        println!("{}", c);
    }
}

实践练习

练习 1:提取子串

fn extract_domain(email: &str) -> &str {
    email.split('@').nth(1).unwrap_or("")
}

fn main() {
    println!("{}", extract_domain("user@example.com"));  // "example.com"
    println!("{}", extract_domain("admin@rust.org"));    // "rust.org"
}

练习 2:切片反转

fn first_and_last(s: &str) -> (&str, &str) {
    let chars: Vec<char> = s.chars().collect();
    let first: &str = &s[..chars[0].len_utf8()];
    let last_start = s.len() - chars[chars.len()-1].len_utf8();
    let last: &str = &s[last_start..];
    (first, last)
}

fn main() {
    let (f, l) = first_and_last("hello");
    println!("first: {}, last: {}", f, l);
}

练习 3:统计单词

fn word_count(text: &str) -> usize {
    text.split_whitespace().count()
}

fn main() {
    let essay = "Rust is a systems programming language";
    println!("单词数: {}", word_count(essay));
}

常见错误

1. 在非 UTF-8 边界切片

let s = String::from("你好");
// let slice = &s[0..1];  // ❌ panic: byte index 1 is not a char boundary

2. 可变切片和不可变切片冲突

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];       // 不可变借用
v.push(6);                // ❌ 不能在借用期间修改
println!("{}", first);

3. 切片越界

let arr = [1, 2, 3];
// let slice = &arr[0..10];  // ❌ panic: index out of bounds
let slice = &arr[0..arr.len()];  // ✅ 安全

小结

类型说明
&str字符串切片
&[T]数组切片
&s[a..b]切片语法(不含 b)
&s[a..=b]切片语法(含 b)

切片是对连续内存的借用,不拥有数据,是最轻量的引用方式。

练习编辑器

rust
Loading...