【Rust 学习笔记】Rust 基础数据类型介绍——字符和字符串类型
博主未授权任何人或组织机构转载博主任何原创文章,感谢各位对原创的支持!
博主链接
博客内容主要围绕:
5G/6G协议讲解
高级C语言讲解
Rust语言讲解
文章目录
- Rust 基础数据类型介绍——字符和字符串类型
- 一、字符
- 二、字符串
- 2.1 字符串字面量
- 2.2 字节串
- 2.3 内存中的字符串
- 2.4 String
- 2.5 其他类似字符串的类型
Rust 基础数据类型介绍——字符和字符串类型
一、字符
Rust的字符类型char
会以32位值表示单个Unicode字符。Rust会对单独的字符使用char类型,而对字符串和文本流使用UTF-8编码。因此后面介绍的String会将其文本表示为UTF-8字节序列,而不是字符数组。
字符字面量
是使用单引号括起来的字符,比如 ‘8’ 或这 ‘j’ 。还可以使用全角 Unicode 字符。与字节字面量
一样有些字符需要使用反斜杠转义,如下表所示:
字符 | Rust字符字面量 |
---|---|
单引号(') | ‘\’’ |
反斜杠(\) | ‘\\’ |
换行(lf) | ‘\n’ |
回车(cr) | ‘\r’ |
制表符(tab) | ‘\t’ |
还可以使用十六进制写出字符的Unicode码点:
- 如果字符的码点在 U+0000到 U+007F 范围内,也就是说在ASCII字符集中,就可以把字符写为 ‘\xHH’ ,其中HH是两个十六进制数。例如字符字面量 ‘*’ 和 ‘\x2A’ 是等效的;
- 可以将任何 Unicode字符写为 ‘\u{HHHHHH}’ 形式,其中HHHHHH是最多6个十六进制数,可以像往常一样用下划线进行分组;
Rust不会在char
和任何其他类型之间进行隐式转换。可以使用as
转换运算符将char转换为整型,对于小于32为的类型,字符的高位会被截断。
assert_eq!('*' as i32, 42);
assert_eq!('ಠ' as u16, 0xca0);
assert_eq!('ಠ' as i8, -0x60); // U+0CA0截断到8位,有符号
u8
是唯一能通过as
运算符转换为char
的类型,因为Rust刻意让as运算符只执行开销极低且可靠的转换,但是除 u8 之外的每个整型都可能包含 Unicode 码点之外的值,所以这些转换都需要做运行期检查。例如,标准库函数 std::char::from_u32
可以接受任何 u32 值并返回一个 Option<char> ,如果此 u32 不是允许的 Unicode 码点,那么 from_u32 就会返回 None,否则返回 Some©,其中 c 是转换成 char 之后的结果。下面是标准库为字符提供的一些有用方法:
assert_eq!('*'.is_alphabetic(), false);
assert_eq!('β'.is_alphabetic(), true);
assert_eq!('8'.to_digit(10), Some(8));
assert_eq!('ಠ'.len_utf8(), 3);
assert_eq!(std::char::from_digit(2, 10), Some('2'));
二、字符串
熟悉 C++ 的程序员应该还记得该语言中有两种字符串类型。字符串字面量的指针类型为 const char *。标准库还提供了一个 std::string 类,用于在运行期动态创建字符串,Rust 中也有类似的设计。
2.1 字符串字面量
字符串字面量要使用双引号括起来,它们使用与char字面量相同的反斜杠转义序列:
fn main() {
let speech = "\"Ouch!\" said the well.\n";
println!("{}",speech);
}
但与字符字面量不同的是,在字符字面量中单引号不需要使用反斜杠转义,而双引号需要。
一个字符串可以跨越多行,例如下面的代码:
fn main() {
println!("In the room the women come and go,
Singing of Mount Abora");
}
该字符串字面量中的换行符是字符串的一部分,因此也会包含在输出中。第3行开头的空格也是如此。
如果字符串的一行以反斜杠结尾,那么就会丢弃其后的换行符和前导空格:
fn main() {
println!("In the room the women come and go,\
Singing of Mount Abora");
}
有些情况下,需要双写字符串中的每一个反斜杠,例如,正则表达式和Windows路径。对于这种情况,Rust提供了原始字符串
。原始字符串用小写字母 r 进行标记。原始字符串中的所有反斜杠和空白字符都会逐字包含在字符串中,原始字符串不识别任何转义序列:
fn main() {
let default_win_install_path = r"C:\Program Files\Gorillas";
println!("{}",default_win_install_path);
}
如何在原始字符串中输出双引号呢?可以在原始字符串的开头和结尾添加 # 标记,可以根据需要增加任意多个井号,以标明原始字符串的结束位置。
fn main() {
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 ('###'):
"###);
}
2.2 字节串
带有 b
前缀的字符串字面量都是字节串
。这样的字符串是 u8 值的切片而不是 Unicode 文本:
fn main() {
let method = b"GET";
assert_eq!(method, &[b'G', b'E', b'T']);
}
method 的类型是 &[u8;3]
,它是对 3 字节数组的引用,没有上述讨论过的任何字符串方法,最像字符串的地方就是其书写语法,仅此而已。
字节串可以使用前面展示过的所有其他字符串语法:
- 可以跨越多行;
- 可以使用转义序列;
- 可以使用反斜杠来连接行;
- 但是原始字节串要使用
br
开头;
字节串不能包含任意 Unicode 字符,只能使用 ASCII 和 \xHH 转义序列。
2.3 内存中的字符串
Rust字符串是 Unicode 字符序列,但它们并没有以 char 数组的形式存储在内存中,而是使用了 UTF-8 (一种可变宽度编码)的形式。字符串中的每个 ASCII 字符都会存储在单字节中,而其它字符会占用多字节。
下面的代码创建了一个String值、&str值和字符串字面量:
let noodles = "noodles".to_string();
let oodles = &noodles[1..];
let poodles = "ಠ_ಠ";
其内存模型如下所示:
String
有一个可以调整大小的缓存区,其中包含了 UTF-8 文本。缓存区是在堆上分配的,因此它可以根据需要或请求来动态调整大小。在上面的示例中,noodles 是一个 String,它拥有一个8字节的缓存区,其中7字节正在使用中。可以将String视为 Vec<u8> ,它可以保证包含格式良好的 UTF-8,实际上,String就是这样实现的;&str
是对别人拥有的一些列 UTF-8 文本的引用,即它借用了这个文本。在上面的示例中,oodles是对 noodles拥有的文本的最后6字节的一个 &str 引用,因此他表示文本“oodles”。与其他切片引用一样, &str 也是一个胖指针,包含实际数据的地址及其长度。可以认为 &str 就是 &[u8],但它能保证包含的是格式良好的 UTF-8;字符串字面量
是指预分配文本的 &str,它通常与程序的机器码一起存储在只读内存区。在上面的示例中,poodles是一个字符串字面量,指向一块7字节的内存,它在程序开始执行时就已创建并一直保存到程序结束;
String和&str 的 .len()
方法会返回其长度,这个长度的单位是字节不是字符:
assert_eq!("ಠ_ಠ".len(), 7);
assert_eq!("ಠ_ಠ".chars().count(), 3);
不能修改 &str,例如下面的代码会报错,如果需要在运行期创建和修改字符串,可以使用String:
let mut s = "hello";
s[0] = 'c'; // 错误:无法修改`&str`,并给出错误原因
s.push('\n'); // 错误:`&str`引用上没有找到名为`push`的方法
&mut str
类型确实存在,但是没有什么用,因为对 UTF-8 的几乎所有操作都会更改其字节总长度,但切片不能重新分配其引用目标的缓冲区。事实上,&mut str 上唯一可用的操作是 make_ascii_uppercase 和 make_ascii_lowercase ,根据定义,它们会就地修改文本并且只影响单字节字符。
2.4 String
&str 非常像 &[T],是一个指向某些数据的胖指针。而 String 则类似于 Vec<T> ,如下表所示:
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。这会复制此字符串:let error_message = "too many pets".to_string();
.to_owned()
方法format!()
宏的工作方式与 println!() 类似,但它会返回一个新的 String,而不是将文本写入标准输出,并且不会在末尾自动添加换行符:assert_eq!(format!("{}°{:02}′{:02}″N", 24, 5, 23), "24°05′23″N".to_string());
- 字符串的数组、切片和向量都有两个方法
.concat()
和.join(sep)
,它们会从许多字符串中形成一个新的 String:let bits = vec!["veni", "vidi", "vici"]; assert_eq!(bits.concat(), "venividivici"); assert_eq!(bits.join(", "), "veni, vidi, vici");
字符串支持 == 运算符和 != 运算符。如果两个字符串以相同的顺序包含相同的字符,则认为它们是相等的:
assert!("ONE".to_lowercase() == "one");
字符串还支持比较运算符 <、<=、>和>=, 以及许多有用的方法和函数,下面是几个例子:
assert!("peanut".contains("nut"));
assert_eq!("ಠ_ಠ".replace("ಠ", "■"), "■_■");
assert_eq!(" clean\n".trim(), "clean");
for word in "veni, vidi, vici".split(", ") {
assert!(word.starts_with("v"));
}
要记住,考虑到 Unicode 的性质,简单的逐字符比较并不总能给出预期的答案。例如,Rust 字符串 “th\u{e9}” 和 “the\u{301}” 都是 thé(在法语中是“茶”的意思)的有效 Unicode 表示。Unicode 规定它们应该以相同的方式显示和处理,但 Rust 会将它们视为两个完全不同的字符串。类似地,Rust 的排序运算符(如 <)也使用基于字符码点值的简单字典顺序。这种排序方式只能说近似于在用户的语言和文化环境中对文本的正确排序方式
2.5 其他类似字符串的类型
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;