切片(Slice)
学习目标
- 理解字符串切片
&str - 掌握数组切片
&[T] - 理解切片与所有权的关系
- 掌握切片的常用方法
核心概念
字符串切片
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) |
切片是对连续内存的借用,不拥有数据,是最轻量的引用方式。