当前位置: 代码网 > it编程>编程语言>rust > 一文带你掌握Rust中的字符串与切片

一文带你掌握Rust中的字符串与切片

2026年03月30日 rust 我要评论
字符串与切片是所有新手遇到的第一个门槛,不同于 java、python 等语言对字符串的高度封装,rust 的字符串与切片深度绑定了所有权、借用、生命周期与 utf-8 编码,从编译期就将乱码、内存安

字符串与切片是所有新手遇到的第一个门槛,不同于 java、python 等语言对字符串的高度封装,rust 的字符串与切片深度绑定了所有权、借用、生命周期与 utf-8 编码,从编译期就将乱码、内存安全等问题解决。

什么是切片(slice)

字符串是切片的特殊场景,想要理解字符串,必须先搞懂切片。在 rust 里,切片是描述一组属于同一类型、长度不确定的、在内存中连续存放的数据结构,用 [t] 来表述。因为长度不确定,切片属于动态大小类型(dst, dynamically sized type),无法直接在栈上存储,必须通过引用(&[t],不可变切片)或可变引用(&mut [t],可变切片)来使用。

切片通过 rust 的范围语法 [start..end] 创建,遵循左闭右开原则,支持多种简写形式:

  • [a..b]:从索引 ab-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 的切片从编译期就杜绝了两类经典内存问题:

  1. 越界访问:编译期会校验切片范围,运行时也会做边界检查,直接拒绝非法访问
  2. 悬垂引用:生命周期规则保证,只要切片有效,底层数据的所有者就一定不会被释放或修改

示例如下:

// 获取数组中第一个非零元素的切片
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[..]);
}

扩展:其他字符串相关类型

除了核心的 &strstring,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字符串与切片的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com