字符串与切片是所有新手遇到的第一个门槛,不同于 java、python 等语言对字符串的高度封装,rust 的字符串与切片深度绑定了所有权、借用、生命周期与 utf-8 编码,从编译期就将乱码、内存安全等问题解决。
什么是切片(slice)
字符串是切片的特殊场景,想要理解字符串,必须先搞懂切片。在 rust 里,切片是描述一组属于同一类型、长度不确定的、在内存中连续存放的数据结构,用 [t] 来表述。因为长度不确定,切片属于动态大小类型(dst, dynamically sized type),无法直接在栈上存储,必须通过引用(&[t],不可变切片)或可变引用(&mut [t],可变切片)来使用。
切片通过 rust 的范围语法 [start..end] 创建,遵循左闭右开原则,支持多种简写形式:
[a..b]:从索引a到b-1的切片[a..]:从索引a到集合末尾的切片[..b]:从集合开头到索引b-1的切片[..]:覆盖整个集合的全切片
示例如下:
fn main() {
// 原数组:切片的底层数据所有者
let arr = [1, 2, 3, 4, 5];
// 创建切片:引用数组的第1到第3个元素
let slice: &[i32] = &arr[1..4];
println!("切片内容: {:?}", slice); // 输出 [2, 3, 4]
println!("切片长度: {}", slice.len()); // 输出 3
println!("切片是否为空: {}", slice.is_empty()); // 输出 false
}
rust 的切片从编译期就杜绝了两类经典内存问题:
- 越界访问:编译期会校验切片范围,运行时也会做边界检查,直接拒绝非法访问
- 悬垂引用:生命周期规则保证,只要切片有效,底层数据的所有者就一定不会被释放或修改
示例如下:
// 获取数组中第一个非零元素的切片
fn first_non_zero(arr: &[i32]) -> &[i32] {
for (i, &num) in arr.iter().enumerate() {
if num != 0 {
return &arr[i..];
}
}
&arr[..0]
}
fn main() {
let arr = [0, 0, 3, 5, 7];
let non_zero_slice = first_non_zero(&arr);
// 编译报错:无法修改原数组,因为它已经被切片借用
// arr[2] = 0;
println!("非零切片: {:?}", non_zero_slice); // 输出 [3, 5, 7]
}
这个例子体现了 rust 的安全哲学:在切片的生命周期内,原数据无法被修改,彻底避免了数据竞争与悬垂引用。
rust 字符串的两大核心类型
rust 的字符串体系看似复杂,核心只有两个类型,其他都是面向特定场景的扩展:
&str:字符串切片,无所有权,只读,是切片的 utf-8 版本string:可拥有、可修改的字符串类型,底层是堆上分配的vec<u8>封装
两者的关系如同 &[t] 与 vec<t>:&str 是数据的“视图”,string 是数据的“所有者”。
rust 字符串的核心设计:强制 utf-8 编码
很多新手可能会困惑为什么 python、go 可以直接用索引 s[i] 取字符,而 rust 却不行?原因在于:rust 的字符串强制使用 utf-8 编码,而 utf-8 是变长编码。
- ascii 字符占 1 个字节
- 中文、日文等东亚字符占 3 个字节
- emoji 等特殊字符占 4 个字节
如果允许直接按索引访问,会带来两个致命问题:
- 索引访问的时间复杂度不再是
o(1),必须遍历字符串才能定位到对应字符 - 极易访问到字符的中间字节,生成非法 utf-8 序列,引发未定义行为(ub)
因此 rust 从语法层面禁止了直接通过索引访问字符串字符,只允许通过合法的方式遍历和操作。
字节与字符的正确操作
fn main() {
let s = "你好rust";
println!("字节长度: {}", s.len()); // 输出 10(2个中文*3字节 + 4个ascii字符=10)
println!("字符数量: {}", s.chars().count()); // 输出 6
// 正确方式1:遍历所有字符(unicode标量值)
println!("=== 字符遍历 ===");
for c in s.chars() {
println!("字符: {}", c);
}
// 正确方式2:遍历所有字节
println!("=== 字节遍历 ===");
for b in s.bytes() {
println!("字节: {}", b);
}
}
字符串切片的致命坑:字符边界问题
rust 编译期不会检查切片范围是否符合 utf-8 字符边界,只有运行时会校验,一旦切到字符中间,会直接触发 panic。
fn main() {
let s = "你好";
// 反例:运行时 panic!"你"占3个字节,[0..2]切到了字符中间
// let sub = &s[0..2];
// 正确示例:严格按字符边界切片
let sub = &s[0..3];
println!("{}", sub); // 输出 你
}
所以在实际开发中最佳实践是:不要硬编码索引切片字符串,优先通过 chars().enumerate() 定位字符位置,再进行切片操作。
字符串与切片的实战常用操作
string 的修改操作(仅所有者可用)
只有持有所有权的 string 可以修改内容,常用方法如下:
fn main() {
let mut s = string::from("hello");
// 追加字符串切片
s.push_str(" world");
// 追加单个字符
s.push('!');
println!("{}", s); // 输出 hello world!
// 插入字符
s.insert(5, ',');
println!("{}", s); // 输出 hello, world!
// 弹出最后一个字符
let last_char = s.pop();
println!("弹出的字符: {:?}", last_char); // 输出 some('!')
// 清空字符串
s.clear();
println!("清空后是否为空: {}", s.is_empty()); // 输出 true
}
字符串拼接的三种方式与选型
| 方法 | 用法 | 所有权影响 | 适用场景 |
|---|---|---|---|
| + 运算符 | string + &str | 左侧 string 所有权被转移 | 简单拼接,无需保留原 string |
| format! 宏 | format!("{} {}", a, b) | 不转移任何所有权 | 复杂拼接、多变量拼接,可读性优先 |
| push_str 方法 | s.push_str(&str) | 仅修改原 string,不转移 | 循环追加、动态构建字符串 |
示例如下:
fn main() {
let s1 = string::from("hello");
let s2 = "rust";
// + 运算符:s1所有权被转移,后续无法使用
let s3 = s1 + " " + s2;
println!("+ 拼接结果: {}", s3); // 输出 hello rust
// format! 宏:不转移所有权,最灵活
let a = string::from("你好");
let b = string::from("世界");
let c = format!("{},{}!", a, b);
println!("format! 结果: {}", c); // 输出 你好,世界!
println!("a仍可用: {}", a); // 所有权未转移,可正常使用
}
字符串切片的常用工具方法
fn main() {
let s = " hello rust ";
// 去除首尾空白
println!("trim后: '{}'", s.trim()); // 输出 'hello rust'
// 前缀/后缀判断
let trimmed = s.trim();
println!("是否以hello开头: {}", trimmed.starts_with("hello")); // 输出 true
println!("是否以rust结尾: {}", trimmed.ends_with("rust")); // 输出 true
// 包含判断与查找
println!("是否包含rust: {}", s.contains("rust")); // 输出 true
println!("rust的起始位置: {:?}", s.find("rust")); // 输出 some(8)
// 分割字符串
let parts: vec<&str> = trimmed.split_whitespace().collect();
println!("分割结果: {:?}", parts); // 输出 ["hello", "rust"]
}
最常用的转换:string 与 & str 的互转
rust 提供了非常便捷的转换能力,核心是 deref 强制转换机制:&string 可以自动被编译器转换为 &str,无需手动处理。
这也是实际开发中的一个最佳实践:函数参数优先使用 &str,而非 &string。因为 &str 可以同时接受字符串字面量、&string、其他字符串切片,通用性最大。
// 函数参数使用&str,获得最大通用性
fn print_info(s: &str) {
println!("内容: {},字节长度: {}", s, s.len());
}
fn main() {
let s = string::from("hello rust");
// 直接传&string,自动转换为&str
print_info(&s);
// 传字符串字面量,完全兼容
print_info("你好世界");
// 手动创建全切片,效果完全一致
print_info(&s[..]);
}
扩展:其他字符串相关类型
除了核心的 &str 和 string,rust 标准库还提供了面向特定场景的字符串类型,无需深入学习,了解其用途即可:
osstr/osstring:操作系统兼容的字符串,支持非 utf-8 的路径、系统参数,位于std::ffi模块cstr/cstring:与 c 语言交互的字符串,以\0结尾,符合 c 语言字符串规范,位于std::ffi模块path/pathbuf:路径专用类型,基于osstr封装,提供路径相关的专用方法,位于std::path模块
总结
rust 的字符串与切片设计,看似严苛,实则是围绕内存安全、utf-8 原生支持和零成本抽象三个核心目标,它把其他语言中运行时才会暴露的字符串乱码、越界、悬垂引用等问题,提前到了编译期解决。
理解这些设计背后的逻辑,你就能真正掌握 rust 字符串与切片,写出既安全又高效的代码。
以上就是一文带你掌握rust中的字符串与切片的详细内容,更多关于rust字符串与切片的资料请关注代码网其它相关文章!
发表评论