这节课我们把字符串单独拿出来讲,是因为字符串太常见了,甚至有些应用的主要工作就是处理字符串。比如 Web 开发、解析器等。而 Rust 里的字符串内容相比于其他语言来说还要多一些。是否熟练掌握 Rust 的字符串的使用,对 Rust 代码开发效率有很大影响,所以这节课我们就来重点攻克它。
可怕的字符串?
我们在 Rust 里常常会见到一些字符串相关的内容,比如下面这些。
String, &String,
str, &str, &'static str
[u8], &[u8], &[u8; N], Vec<u8>
as_str(), as_bytes()
OsStr, OsString
Path, PathBuf
CStr, CString
首先,我们来看 C 语言里的字符串。图里显示,C 中的字符串统一叫做 char *,这确实很简洁,相当于是统一的抽象。但是这个统一的抽象也付出了代价,就是丢失了很多额外的信息。
为什么会这样呢?我们从计算机结构说起。我们都知道,计算机 CPU 执行的指令都是二进制序列,所有语言写的程序最后执行时都会归结为二进制序列来执行。但是为什么不直接写二进制打孔开发,而是出现了几百上千种计算机语言呢?没错,就是因为抽象。
抽象是用来解决现实问题建模的工具。在 Rust 里也一样,之所以 Rust 有那么多看上去都是字符串的类型,就是因为 Rust 把字符串在各种场景下的使用给模型化、抽象化了。相比 C 语言的 char *,多了建模的过程,在这个模型里面多了很多额外的信息。
下面我们就来看看前面提到的那些字符串类型各自有什么具体含义。
不同类型的字符串
示例:
fn main() {let s1: &'static str = "More Powerful,Choose Rust"; let s2: String = s1.to_string(); let s3: &String = &s2;let s4: &str = &s2[..];let s5: &str = &s2[..6];
}
上述示例中,s1、s2、s3、s4、s5 看起来好像是 4 种不同类型的字符串表示。为了让你更容易理解,我画出它们在内存中的结构图。
我来详细解释一下这张图片的意思。
“More Powerful,Choose Rust” 这个用双引号括起来的部分是字符串的字面量,存放在静态数据区。而 s1 是指向静态数据区中的这个字符串的切片引用,形式是 &'static str,这是静态数据区中的字符串的表示方法。
通过执行 s1.to_string(),Rust 将静态数据区中的字符串字面量拷贝了一份到堆内存中,通过 s2 指向,s2 具有这个堆内存字符串的所有权,String 在 Rust 中就代表具有所有权的字符串。
s3 就是对 s2 的不可变引用,因此类型为 &String。
s4 是对 s2 的切片引用,类型是 &str。切片就是一块连续内存的某种视图,它可以提取目标对象的全部或一部分。这里 s4 就是取的目标对象字符串的全部。
s5 是对 s2 的另一个切片引用,类型也是 &str。与 s4 不同的是,s5 是 s2 的部分视图。具体来说,就是 “I am a” 这一部分。
相信你通过上面的例子对这几种不同类型的字符串已经有了一个简单直观的认识了,下面我来给你详细解释下。
String 是字符串的所有权形式,常常在堆中分配。String 字符串的内容大小是可以动态变化的。而 str 是字符串的切片类型,通常以切片引用 &str 形式出现,是字符串的视图的借用形式。
字符串字面量默认会存放在静态数据区里,而静态数据区中的字符串总是贯穿程序运行的整个生命期,直到程序结束的时候才会被释放。因此不需要某一个变量对其拥有所有权,也没有哪个变量能够拥有这个字符串的所有权(也就是这个资源的分配责任)。因此对于字符串字面量这种数据类型,我们只能拿到它的借用形式 &'static str。这里 'static 表示这个引用可以贯穿整个程序的生命期,直到这个程序运行结束。
&String 仅仅是对 String 类型的字符串的普通引用。
对 String 做字符串切片操作后,可以得到 &str。这里这个 &str 就是指向由 String 管理的内存资源的切片引用,是目标字符串资源的借用形式,不会再把字符串内容复制一份。
从上面的图示里可以看到,&str 既可以引用堆中的字符串,也可以引用静态数据区中的字符串(&'static str 是 &str 的一种特殊形式)。其实内存本来就是一个线性空间,一个指针(引用是指针的一种)理论上来说可以指向这个线性空间中的任何地址。
&str 也可转换为 String。你可以通过示例,看一下它们之间是如何转换的。
let s: String = “More Powerful,Choose Rust”.to_string();
let a_slice: &str = &s[…];
let another_String: String = a_slice.to_string();
切片
上面提到了切片,这里我再补充一点关于切片(slice)的背景知识。切片是一段连续内存的一个视图(view),在 Rust 中由 [T] 表示,T 为元素类型。这个视图可以是这块连续内存的全部或一部分。切片一般通过切片的引用来访问,你可以看一下我给出的这个字符串示例。
let s = String::from(“abcdefg”);
let s1 = &s[…]; // s1 内容是 “abcdefg”
let s2 = &s[0…4]; // s2 内容是 “abcd”
let s3 = &s[2…5]; // s3 内容是 “cde”
上面示例中,s 是堆内存中所有权型字符串类型。s1 作为 s 的一个切片引用,它也指向堆内存中那个字符串的头部,表示 s 的完整内容。s2 与 s1 指向的堆内存地址是相同的,但是内容不同,s2 是 “abcd”,而 s1 是 “abcdefg”。s3 则是 s 的中间位置的一段切片引用,内容是 “cde”。s3 指向的地址与 s、s1、s2 不同。
我画了一张图来表示它们之间的关系。