Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

8.3 String类型 Pt.1:字符串的创建、更新与拼接

8.3.0. 本章内容

第八章主要讲的是 Rust 中常见的集合。Rust 提供了很多集合类型的数据结构,这些集合可以包含很多值。但是第八章所讲的集合与数组和元组有所不同。

第八章中的集合是存储在堆内存上而非栈内存上的,这也意味着这些集合的数据大小无需在编译时就确定,在运行时它们可以动态地变大或变小。

本章主要会讲三种集合:Vector、String(本文) 和 HashMap。

8.3.1. 为什么字符串会困扰 Rust 开发者

Rust 开发者(尤其是新手)经常会被字符串困扰,原因如下:

  • Rust 倾向于暴露可能的错误
  • 字符串数据结构复杂
  • Rust 字符串使用了 UTF-8 编码

8.3.2. 字符串是什么

字符串是基于字节的集合,并且它提供了一些方法,这些方法能将字节解析为文本。

在 Rust 的核心语言层面,只有一个字符串类型——字符串切片 str,通常是以借用的形式出现的,也就是 &str

字符串切片是对存储在其他地方、采用 UTF-8 编码的字符串的引用。例如字符串字面值就是直接存储在 Rust 的二进制文件中,所以它也是一种字符串切片。

String 类型来自于标准库,而不是核心语言。 它是一种可增长、可变、可拥有所有权的类型,同样采用 UTF-8 编码。

8.3.3. “字符串”到底是指谁?

通常说的“字符串”就是指 String&str 这两种类型,而不是其中的一种。这两种类型在标准库里都用得非常频繁,也都使用了 UTF-8 编码,但这里主要还是讲 String 类型,因为它更复杂。

8.3.4. 其他的字符串类型

Rust 标准库还提供了其他的字符串类型,例如:OsStringOsStrCStringCStr。注意这些类型都以 String 或者 Str 结尾,这与前面提到的 String 和字符串切片这两种类型的命名方式有关。

通常来说,以 String 结尾的字符串类型是可拥有所有权的,以 Str 结尾的类型通常是可借用的。

这些不同的字符串类型可以存储不同编码的文本,或以不同的内存布局来表示数据。

某些 library crate 针对字符串提供了更多选项,这里就不介绍了。

8.3.5. 创建一个新的字符串

由于 String 类型的本质是字节的集合,所以很多 Vec<T> 的操作都可以用于 String

String::new() 可以用来创建一个空的字符串。看个例子:

fn main(){
	let mut s = String::new();
}

但一般而言,都是使用初始值来创建 String。这时可以使用 to_string 方法来创建 String。这个方法可用于实现了 Display trait 的类型,包括字符串字面值。如下例:

fn main() {
    let data = "wjq";
    let s = data.to_string();
    let s1 = "wjq".to_string();
}

data 是一个字符串字面值,使用 to_string 把它转为 String 类型,存储在 s 里。也可以直接写字符串字面值,然后调用 .to_string(),也就是给 s1 赋值的操作。这两个操作效果相同。

to_string 也不是唯一的方法,另一种方式是使用 String::from 函数:

#![allow(unused)]
fn main() {
let s = String::from("wjq");
}

这个函数和 to_string 方法的效果是一样的。

由于字符串用得非常多,所以 Rust 提供了很多不同的通用 API 供我们选择。有些函数可能看着很多余,但实际上它们都有各自的用处。在实际编码时可以根据喜好来选择。

8.3.6. 更新 String

之前提到了,String 类型的大小是可以增减的。由于其本质是字节的集合,里面的内容也可以修改。它的操作和 Vector 类似,此外还可以对 String 进行拼接。

1. push_str()

首先讲 push_str(),它是一个把字符串切片附加到 String 的方法。如下例:

fn main() {
    let mut s = String::from("6657");
    s.push_str("up up");
    println!("{}", s);
}

输出:

6657up up

push_str 的签名是 push_str(&mut self, string: &str),它的参数是借用的字符串切片,而字符串字面值就是切片,所以 "up up" 可以传进去。并且这个方法不会获得参数的所有权,所以传进去的值仍然有效,还能继续使用。

2. push

第二个方法叫 push(),它能把单个字符附加到 String 里面。如下例:

fn main() {
    let mut s = String::from("665");
    s.push('7');
    println!("{}", s);
}

注意:字符得使用单引号。

输出:

6657

3. +

Rust 允许使用 + 来拼接字符串。如下例:

fn main() {
    let s1 = String::from("6657");
    let s2 = String::from("up up");
    let s3 = s1 + &s2;
    println!("{}", s3);
}

注意:加号前必须是 String 类型,加号后必须是字符串切片。

但在这个例子中,加号后的实际类型是 &String 而不是 &str。这是因为这里 Rust 使用了解引用强制转换(deref coercion),把 &String 强制转换为 &str

当然,因为 s2 传进去的是引用,所以 s2 在拼接后仍然有效;而 s1 是把本身的所有权交给了 s3,所以 s1 在拼接后就无效了。

输出:

6657up up

4. format!

format! 这个宏可以更加灵活地拼接字符串。如下例:

fn main() {
    let s1 = String::from("cn");
    let s2 = String::from("Niko");
    let s3 = String::from("fan club");
    let s = format!("{} {} {}", s1, s2, s3);
    println!("{}", s);
}

它使用占位符来代替变量,这点和 println! 很像。区别在于:println! 会把结果打印出来,而 format! 则返回拼接好的字符串。

输出:

cn Niko fan club

当然使用 + 也能实现一样的效果,只不过写起来稍微麻烦一些:

fn main() {
    let s1 = String::from("cn");
    let s2 = String::from("Niko");
    let s3 = String::from("fan club");
    let s = s1 + " " + &s2 + " " + &s3;
    println!("{}", s);
}

format! 最好的一点是它不会取得任何参数的所有权,这些参数在后续都可以继续使用。