Rust语言

关注公众号 jb51net

关闭
首页 > 软件编程 > Rust语言 > Rust 字符串与切片

详解Rust 中的字符串与切片(String 与 &str)

作者:小道士写程序

本文围绕 Rust的字符串与切片,把这两者的关系、UTF-8 编码、连续 / 不连续切片以及常见报错一次性讲清楚,看完即可对齐Rust语言圣经「字符串与切片」这一节,感兴趣的朋友跟随小编一起看看吧

写 Rust 时,几乎每个新手都会在 String 和 &str 之间反复纠结:到底什么时候用哪个?为什么 &s[3..5] 会 panic?String 和字符串字面量又是什么关系?
本文围绕 Rust 的字符串与切片,把这两者的关系、UTF-8 编码、连续 / 不连续切片以及常见报错一次性讲清楚,看完即可对齐 Rust 语言圣经「字符串与切片」这一节。

对照这张图记忆要点:String = ptr+len+cap(拥有堆、可增长);&str = ptr+len(借用、无所有权)。

一、为什么 Rust 有两套字符串类型

和大多数语言「一个 string 打天下」不同,Rust 把字符串拆成了两个角色:

可以用一个类比来记:

类型类比所有权存储位置可否增长
StringVec<T>拥有数据堆可以 push
&str&[T]借用一段指向别处不可以

也就是说,&str 之于 String,就像 &[T] 之于 Vec<T>——是对一段连续内存的只读视图。

二、创建 String 的几种方式

fn main() {
    // 1. 从字符串字面量创建(推荐)
    let s1 = String::from("rust is good!");
    // 2. 使用 to_string(字面量调方法)
    let s2 = "rust is good!".to_string();
    // 3. 用类型转换
    let s3: String = "rust is good!".into();
    // 4. 先创建空串,再追加
    let mut s4 = String::new();
    s4.push_str("rust");
    s4.push(' ');
    s4.push_str("is good!");
}

顺带一提:当你需要批量构造若干个 String 时,std::array::from_fn 是非常优雅的手段。比如生成 8 个相同内容的 String:

let array: [String; 8] = std::array::from_fn(|_i| String::from("rust is good!"));

因为 String 实现了 Drop(不是 Copy),不能用 [String::from("..."); 8] 这种语法,而 from_fn 会为每个位置单独调用闭包生成新值,正好绕开了这个限制。

三、String 与 &str 的相互转换

这是日常开发中最频繁的操作,记住「String 随时可以降级为 &str,反过来要重新分配内存」即可。

fn main() {
    let s = String::from("hello world");
    // String -> &str:三种等价写法
    let a: &str = &s;          // 借用,触发 Deref 自动转换
    let b: &str = s.as_str();  // 显式方法
    let c: &str = &s[..];      // 取整个字符串的切片
    // &str -> String:需要分配新内存
    let owned: String = String::from("hi");
    let owned2: String = "hi".to_string();
}

函数参数上的最佳实践:参数优先写 &str,这样 String(会自动借成 &str)和字符串字面量都能传进来,通用性最好:

fn greet(name: &str) {
    println!("hello, {name}");
}
fn main() {
    let s = String::from("Rust");
    greet(&s);          // String 自动借为 &str
    greet("Course");    // 字面量本身就是 &str
    greet(s.as_str());  // 显式转换
}

四、底层真相:UTF-8 变长编码

Rust 的字符串始终按 UTF-8 编码存储。UTF-8 是变长编码:英文 1 字节,部分欧洲字符 2 字节,中文通常 3 字节,少量字符 4 字节。

由此带来两个重要结论:

  1. String::len() 返回的是字节长度,不是字符个数;
  2. 不能用字节下标去索引「第几个字符」——下标必须落在字符边界上,否则 panic。
fn main() {
    let s = String::from("你好rust");
    println!("{}", s.len());      // 11,不是 6:你好=6字节 + rust=4字节,再 +1? 见说明
    // 想拿「字符个数」要用 chars().count()
    println!("{}", s.chars().count());
}

说明:你好 各占 3 字节共 6 字节,rust 各占 1 字节共 4 字节,所以 len() 是 10。字符数 chars().count() 才是 6。

要正确遍历内容,按需用 chars()(遍历 Unicode 字符)或 bytes()(遍历字节):

fn main() {
    let s = String::from("a中b");
    for c in s.chars() { println!("char: {}", c); }   // a、中、b
    for b in s.bytes()  { println!("byte: {}", b); }    // 97, 228,184,173, 98
}

五、字符串切片:连续 vs 不连续

1. 连续切片(合法)

当切片的起止下标都落在字符边界上时,得到的是一个合法的字符串切片:

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];   // "hello"
    let world = &s[6..11];  // "world"
    let head  = &s[..5];    // "hello"(省略起始下标)
    let tail  = &s[6..];    // "world"(省略结束下标)
    println!("{}, {}", hello, world);
}

2. 不连续切片(编译期或运行期报错)

对 String 而言,s[n..m] 切出来的类型是 str,但它是不 Sized 的切片类型,必须配合 & 借用以确定长度;而对 &str 直接使用切片语法且类型不匹配时会触发如下典型错误:

let s = String::from("hello");
let slice = s[3..5];   // 报错:the size for values of type `str` cannot be known

正确写法是加上 &,把它变成一个有长度的借用切片:

let s = String::from("hello");
let slice = &s[3..5];  // 正确:得到 &str

3. 边界 panic(运行期)

即便语法正确,如果切片范围不在 UTF-8 字符边界上,运行时会 panic:

fn main() {
    let s = String::from("你好");
    let bad = &s[0..2];  // panic:byte index 2 is not a char boundary
}

「你好」中每个字符占 3 字节,合法边界是 0、3、6;0..2 落在了「你」字内部,于是触发边界检查 panic。这是 Rust 在运行期对 UTF-8 完整性做的保护。

六、常见报错速查

报错信息原因解决方式
the size for values of type str cannot be knowns[3..5] 切出的是未大小的 str改为 &s[3..5] 借用为 &str
byte index N is not a char boundary切片下标落在 UTF-8 字符中间把下标挪到合法字符边界
expected String, found &str函数要 String 却传了 &str用 .to_string() / String::from() 转换
expected &str, found String(个别场景)借用生命周期或类型不匹配传 &s 或 s.as_str()

七、总结

  1. String 拥有堆上的可增长数据,&str 是对一段合法 UTF-8 的借用视图,两者关系如同 Vec<T> 与 &[T]。
  2. String 可无损降级为 &str(&s、s.as_str()、&s[..]);反向转换需要重新分配内存。
  3. 函数参数尽量写 &str,通用性最好。
  4. UTF-8 变长编码决定了 len() 返回字节数,切片下标必须落在字符边界,否则 panic。
  5. 切片记得加 &(&s[3..5])以获得确定长度,避免 str 未大小的编译错误。

掌握了这套「拥有 vs 借用、字节 vs 字符、连续 vs 不连续」的心智模型,Rust 字符串这块最劝退的部分基本就被你拿下了。

到此这篇关于详解Rust 中的字符串与切片(String 与 &amp;str)的文章就介绍到这了,更多相关Rust 字符串与切片内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文