Appearance
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
所指向的内容,但是无权给这个字符串扩容或者释放内存。