Skip to content

6.2 字符串

字符串是非常重要的常见类型。相比其他很多语言,Rust 的字符串显得有点复杂,主要是跟所有权有关。 在 Rust 中,String 的底层实现使用了 UTF-8 编码,这意味着,在对 String 进行操作时,需要考虑到 Unicode 字符可能占用多个字节的情况。 Rust 的字符串涉及两种类型,一种是&str,另外一种是String

rust
// strings.rs
fn main () {
    let question = "How are you?";              // a &str type
    let person: String = "Bob".to_string();     // a String type
    let namaste = String::from("नमे");           // unicodes yay!
    println!("{}! {} {}", namaste, question, person);
}

创建字符串类型的方法有多种。String类型是在堆上分配的,而&str类型通常是指向现有字符串的指针,该字符串可以是堆栈、堆、已编译对象代码的数据段中的字符串

6.2.1 &str

str是 Rust 的内置类型,开发中基本不会使用。通常使用&str类型,是对str的借用,其实际上指向了一段 UTF-8 编码的字节数组的指针,因此可以看作是[u8]类型的切片形式&[u8],是一种固定大小的字符串类型。

常见的 字符串字面值(string literal) 就是&'static str类型,这是一种带有'static生命周期的&str类型。

rust
// 字符串字面值
let hello = "Hello, world!";

// 附带显式类型标识
let hello: &'static str = "Hello, world!";

而内置的 char 类型是4字节长度的,存储的内容是 Unicode 标量值。 Rust 中的字符串处理是以 Unicode 标量值为基础的,每个标量值都代表一个字符。

实际上str类型有一种方法:fn as_ptr(&self) -> *const u8。它内部无须做任何计算,只需做一个强制类型转换即可:

rust
self as *const str as *const u8

这样设计有一个缺点,就是不能支持O(1)时间复杂度的索引操作。 如果我们要找一个字符串s内部的第n个字符,不能直接通过s[n]得到,这一点跟其他许多语言不一样。 在 Rust 中,这样的需求可以通过下面的语句实现:

rust
s.chars().nth(n)

它的时间复杂度是O(n),因为 utf-8 是变长编码,如果我们不从头开始过一遍,根本不知道第n个字符的地址在什么地方。

但是,综合来看,选择 utf-8 作为内部默认编码格式是缺陷最少的一种方式了。相比其他的编码格式,它有相当多的优点。 比如:它是大小端无关的,它跟 ASCII 码兼容,它是互联网上的首选编码,等等。

关于各种编码格式之间的详细优劣对比,强烈建议大家参考这个网站 utf8everywhere

跟上一章讲过的数组类似,[T]是 DST 类型,对应的str是 DST 类型。 &[T]是数组切片类型,对应的&str是字符串切片类型。

示例如下:

rust
fn main() {
    let greeting : &str = "Hello";
    let substr : &str = &greeting[2..];
    println!("{}", substr);
}

编译,执行,可见它跟数组切片的行为很相似。

&str类型也是一个胖指针,可以用下面的示例证明:

rust
fn main() {
    println!("Size of pointer: {}", std::mem::size_of::<*const ()>());
    println!("Size of &str   : {}", std::mem::size_of::<&str>());
}

编译,执行,结果为:

txt
Size of pointer: 8
Size of &str   : 16

它内部实际上包含了一个指向字符串片段头部的指针和一个长度。 所以,它跟 C/C++ 的字符串不同:C/C++ 里面的字符串以\\0结尾,而 Rust 的字符串是可以中间包含\\0字符的。

注意单/双引号的区别

在 Rust 中,双引号括起来的"a"是字符串字面量,而单引号括起来的'a'是字符字面量。虽然它们都表示一个字符a,但是在内部实现和使用上仍有些许差别。

具体而言,双引号括起来的"a"会被存储为 &str 类型,这是一个指向 UTF-8 编码的字节数组的引用,因此可以包含多个字符。 而单引号括起来的'a'则会被存储为 char 类型,这是一个 Unicode 标量值。

在 Rust 中,可以通过以下语法将一个字符字面量转换为对应的 Unicode 标量值:

rust
fn main() {
    let a = 'a'; // 定义一个 Unicode 字符 'a'
    let a_val = a as u32; // 将字符 'a' 转换为对应的 Unicode 标量值
    println!("The Unicode scalar value of 'a' is: {}", a_val);
}

同样地,在 Rust 中,可以通过以下语法将一个字符串字面量转换为对应的 UTF-8 字节数组:

rust
fn main() {
    let string = "Hello, world!"; // 定义一个字符串
    let bytes = string.as_bytes(); // 将字符串转换为字节数组
    println!("{:?}", bytes); // 打印字节数组
}

6.2.2 String

String 类型是 Rust 标准库提供的一种可增长的字符串类型, 其实际上是一个包含了指向堆上分配的 UTF-8 编码字节数组的指针、字节数组的长度和容量等信息的结构体,并且提供了很多字符串相关的函数。

它跟&str类型的主要区别是,它有管理内存空间的权力。 关于“所有权”和“借用”的关系,在本书第二部分会详细讲解。 &str类型是对一块字符串区间的借用,它对所指向的内存空间没有所有权,哪怕&mut str也一样。比如:

rust
let greeting : &str = "Hello";

我们没办法扩大greeting所引用的范围,在它后面增加内容。但是 String 类型可以。示例如下:

rust
fn main() {
    let mut s = String::from("Hello");
    s.push(' ');
    s.push_str("World.");
    s.pop();
    println!("{}", s);
}

这是因为 String 类型在堆上动态申请了一块内存空间,它有权对这块内存空间进行扩容,内部实现类似于std::Vec<u8>类型。所以这个类型就是一种容纳字符的集合,第 24 章会具体讲解集合。

这个类型实现了Deref<Target=str>的 trait。所以在很多情况下,&String类型可以被编译器自动转换为&str类型。 关于 Deref 大家可以参考本书第二部分“解引用”章节。

我们写个小示例演示一下:

rust
fn capitalize(substr: &mut str) {
    substr.make_ascii_uppercase();
}

fn main() {
    let mut s = String::from("Hello World");
    capitalize(&mut s);
    println!("{}", s);
}

在这个例子中,capitalize函数调用的时候,形式参数要求是&mut str类型,而实际参数是&mut String类型,这里编译器给我们做了自动类型转换。 在capitalize函数内部,它有权修改&mut str所指向的内容,但是无权给这个字符串扩容或者释放内存。

Released under the MIT License