第3章 | 基本数据类型 | 字符串类型

2024-05-08 15:17:32 浏览数 (1)

特拉法尔加·罗 - 蒙奇·D·路飞 - 尤斯塔斯·基德

3.7 字符串类型

熟悉 C 的程序员应该还记得该语言中有两种字符串类型。字符串字面量的指针类型为 const char *。标准库还提供了一个 std::string 类,用于在运行期动态创建字符串。

Rust 中也有类似的设计。本节将首先展示所有编写字符串字面量的方法,然后介绍 Rust 的两种字符串类型。第 17 章会介绍有关字符串和文本处理的更多信息。

3.7.1 字符串字面量

字符串字面量要用双引号括起来,它们使用与 char 字面量相同的反斜杠转义序列:

代码语言:javascript复制
    let speech = ""Ouch!" said the well.n";

但与 char 字面量不同,在字符串字面量中单引号不需要用反斜杠转义,而双引号需要。

一个字符串可能跨越多行:

代码语言:javascript复制
    println!("In the room the women come and go,
        Singing of Mount Abora");

该字符串字面量中的换行符是字符串的一部分,因此也会包含在输出中。第 2 行开头的空格也是如此。

如果字符串的一行以反斜杠结尾,那么就会丢弃其后的换行符和前导空格:

代码语言:javascript复制
    println!("It was a bright, cold day in April, and 
        there were four of us—
        more or less.");

这会打印出单行文本。该字符串在“and”和“there”之间会有一个空格,因为在本程序中,第一个反斜杠之前有一个空格,而在破折号和“more”之间则没有空格。

在少数情况下,需要双写字符串中的每一个反斜杠,这让人不胜其烦。(经典的例子是正则表达式和 Windows 路径。)对于这些情况,Rust 提供了原始字符串。原始字符串用小写字母 r 进行标记。原始字符串中的所有反斜杠和空白字符都会逐字包含在字符串中。原始字符串不识别任何转义序列:

代码语言:javascript复制
    let default_win_install_path = r"C:Program FilesGorillas";

    let pattern = Regex::new(r"d (.d )*");

不能简单地在双引号前面放置一个反斜杠来包含原始字符串——别忘了,前面说过它不识别转义序列。但是,仍有办法解决。可以在原始字符串的开头和结尾添加 # 标记:

代码语言:javascript复制
    println!(r###"
        This raw string started with 'r###"'.
        Therefore it does not end until we reach a quote mark ('"')
        followed immediately by three pound signs ('###'):
    "###);

可以根据需要添加任意多个井号,以标明原始字符串的结束位置。4

4原始字符串不要求井号的具体数量,只要求前后数量一致,且不会和内容相混淆。另外,原始字符串不会去掉前导空格。——译者注

3.7.2 字节串

带有 b 前缀的字符串字面量都是字节串。这样的字节串是 u8 值(字节)的切片而不是 Unicode 文本:

代码语言:javascript复制
    let method = b"GET";
    assert_eq!(method, &[b'G', b'E', b'T']);

method 的类型是 &[u8; 3]:它是对 3 字节数组的引用,没有刚刚讨论过的任何字符串方法,最像字符串的地方就是其书写语法,仅此而已。

字节串可以使用前面展示过的所有其他的字符串语法:可以跨越多行、可以使用转义序列、可以使用反斜杠来连接行等。不过原始字节串要以 br" 开头。

字节串不能包含任意 Unicode 字符,它们只能使用 ASCII 和 xHH 转义序列。

3.7.3 内存中的字符串

Rust 字符串是 Unicode 字符序列,但它们并没有以 char 数组的形式存储在内存中,而是使用了 UTF-8(一种可变宽度编码)的形式。字符串中的每个 ASCII 字符都会存储在单字节中,而其他字符会占用多字节。

图 3-3 展示了由以下代码创建的 String 值和 &str 值。

代码语言:javascript复制
    let noodles = "noodles".to_string();
    let oodles = &noodles[1..];
    let poodles = "ಠ_ಠ";

图 3-3:String&strstr

String 有一个可以调整大小的缓冲区,其中包含 UTF-8 文本。缓冲区是在堆上分配的,因此它可以根据需要或请求来调整大小。在示例中,noodles 是一个 String,它拥有一个 8 字节的缓冲区,其中 7 字节正在使用中。可以将 String 视为 Vec<u8>,它可以保证包含格式良好的 UTF-8,实际上,String 就是这样实现的。

&str(发音为 /stɜːr/ 或 string slice)是对别人拥有的一系列 UTF-8 文本的引用,即它“借用”了这个文本。在示例中,oodles 是对 noodles 拥有的文本的最后 6 字节的一个 &str 引用,因此它表示文本“oodles”。与其他切片引用一样,&str 也是一个胖指针,包含实际数据的地址及其长度。可以认为 &str 就是 &[u8],但它能保证包含的是格式良好的 UTF-8。

字符串字面量是指预分配文本的 &str,它通常与程序的机器码一起存储在只读内存区。在前面的示例中,poodles 是一个字符串字面量,指向一块 7 字节的内存,它在程序开始执行时就已创建并一直存续到程序退出。

String&str.len() 方法会返回其长度。这个长度以字节而不是字符为单位:

代码语言:javascript复制
assert_eq!("ಠ_ಠ".len(), 7);
assert_eq!("ಠ_ಠ".chars().count(), 3);

不能修改 &str

代码语言:javascript复制
let mut s = "hello";
s[0] = 'c';    // 错误:无法修改`&str`,并给出错误原因
s.push('n');  // 错误:`&str`引用上没有找到名为`push`的方法

要在运行期创建新字符串,可以使用 String

&mut str 类型确实存在,但它没什么用,因为对 UTF-8 的几乎所有操作都会更改其字节总长度,但切片不能重新分配其引用目标的缓冲区。事实上,&mut str 上唯一可用的操作是 make_ascii_uppercasemake_ascii_lowercase,根据定义,它们会就地修改文本并且只影响单字节字符。

3.7.4 String

&str 非常像 &[T],是一个指向某些数据的胖指针。而 String 则类似于 Vec<T>,如表 3-11 所示。

表 3-11:Vec<T>String 对比

Vec<T>

String

自动释放缓冲区

可增长

类型关联函数 ::new() 和 ::with_capacity()

.reserve() 方法和 .capacity() 方法

.push() 方法和 .pop() 方法

范围语法 v[start..stop]

是,返回 &[T]

是,返回 &str

自动转换

&Vec<T> 到 &[T]

&String 到 &str

继承的方法

来自 &[T]

来自 &str

Vec 一样,每个 String 都在堆上分配了自己的缓冲区,不会与任何其他 String 共享。当 String 变量超出作用域时,缓冲区将自动释放,除非这个 String 已经被移动。

以下是创建 String 的几种方法。

.to_string() 方法会将 &str 转换为 String。这会复制此字符串。

代码语言:javascript复制
let error_message = "too many pets".to_string();

.to_owned() 方法会做同样的事,也会以同样的方式使用。这种命名风格也适用于另一些类型,第 13 章中会讨论。

format!() 宏的工作方式与 println!() 类似,但它会返回一个新的 String,而不是将文本写入标准输出,并且不会在末尾自动添加换行符。

代码语言:javascript复制
assert_eq!(format!("{}°{:02}′{:02}″N", 24, 5, 23),
               "24°05′23″N".to_string());

字符串的数组、切片和向量都有两个方法(.concat().join(sep)),它们会从许多字符串中形成一个新的 String

代码语言:javascript复制
    let bits = vec!["veni", "vidi", "vici"];
    assert_eq!(bits.concat(), "venividivici");
    assert_eq!(bits.join(", "), "veni, vidi, vici");

有时要选择是使用 &str 类型还是使用 String 类型。第 5 章会详细讨论这个问题。这里仅指出一点:&str 可以引用任意字符串的任意切片,无论它是字符串字面量(存储在可执行文件中)还是 String(在运行期分配和释放)。这意味着如果希望允许调用者传递任何一种字符串,那么 &str 更适合作为函数参数。

3.7.5 使用字符串

字符串支持 == 运算符和 != 运算符。如果两个字符串以相同的顺序包含相同的字符(无论是否指向内存中的相同位置),则认为它们是相等的:

代码语言:javascript复制
    assert!("ONE".to_lowercase() == "one");

字符串还支持比较运算符 <<=>>=,以及许多有用的方法和函数,你可以在“str(原始类型)”或“std::str”模块下的在线文档中找到它们(或直接翻到第 17 章)。下面是一些例子:

代码语言:javascript复制
    assert!("peanut".contains("nut"));
    assert_eq!("ಠ_ಠ".replace("ಠ", "■"), "■_■");
    assert_eq!("    cleann".trim(), "clean");

    for word in "veni, vidi, vici".split(", ") {
        assert!(word.starts_with("v"));
    }

要记住,考虑到 Unicode 的性质,简单的逐字符比较并不总能给出预期的答案。例如,Rust 字符串 "thu{e9}""theu{301}" 都是 thé(在法语中是“茶”的意思)的有效 Unicode 表示。Unicode 规定它们应该以相同的方式显示和处理,但 Rust 会将它们视为两个完全不同的字符串。类似地,Rust 的排序运算符(如 <)也使用基于字符码点值的简单字典顺序。这种排序方式只能说近似于在用户的语言和文化环境中对文本的正确排序方式。5第 17 章会更详细地讨论这些问题。

5比如汉语就有拼音、笔画等排序方式,所以不能靠它做那些需要严格本地化场景下的排序。——译者注

3.7.6 其他类似字符串的类型

Rust 保证字符串是有效的 UTF-8。有时程序确实需要处理并非有效 Unicode 的字符串。这种情况通常发生在 Rust 程序不得不与不强制执行此类规则的其他系统进行互操作时,例如,在大多数操作系统中,很容易创建一个名字不符合 Unicode 规则的文件。当 Rust 程序遇到这种文件名时应该怎么办呢?

Rust 的解决方案是为这些情况提供一些类似字符串的类型。

  • 对于 Unicode 文本,坚持使用 String&str
  • 当使用文件名时,请改用 std::path::PathBuf&Path
  • 当处理根本不是 UTF-8 编码的二进制数据时,请使用 Vec<u8>&[u8]
  • 当使用操作系统提供的原生形式的环境变量名和命令行参数时,请使用 OsString&OsStr
  • 当和使用 null 结尾字符串的 C 语言库进行互操作时,请使用 std::ffi::CString&CStr

3.8 类型别名

与 C 中的 typedef 用法类似,可以使用 type 关键字来为现有类型声明一个新名称:

代码语言:javascript复制
    type Bytes = Vec<u8>;

这里声明的类型 Bytes 就是这种特定 Vec 的简写形式。

代码语言:javascript复制
    fn decode(data: &Bytes) {
        ...
    }

3.9 前路展望

类型是 Rust 的核心部分。接下来本书会继续讨论类型并引入一些新的类型。特别是,Rust 的用户定义类型赋予了该语言很多特色,因为各种方法都是在此基础上定义的。用户定义类型共有 3 种,我们将用连续 3 章(第 9 章、第 10 章和第 11 章)介绍它们。

函数和闭包都有自己的类型,第 14 章中会介绍。构成标准库的类型贯穿全书。例如,第 16 章会介绍标准的集合类型。

不过,所有这些都还得再等等。在继续前进之前,我们必须先着手处理 Rust 安全规则的核心概念。

笔记 - 想法 看到现在,也把对应章节里的代码在本地敲了一遍,运行了看了效果,对于一个前端业务性开发选手来说,有些东西还是没彻底明白理解透彻,只是了解了,但是又不知道怎么用,何时用 本来上手难度就高,学习时间少还比较散,最终效果又打折扣了,目前大部分东西都是一知半解状态,后面的路还很长,加油...

0 人点赞