|
|
|
@ -1,6 +1,6 @@
|
|
|
|
|
# 字符串
|
|
|
|
|
|
|
|
|
|
在其他语言,字符串往往是送分题,因为实在是太简单了,例如 `"hello, world"` 就是字符串章节的几乎全部内容了,但是如果你带着同样的想法来学 Rust,我保证,绝对会栽跟头,**因此这一章大家一定要重视,仔细阅读,这里有很多其它 Rust 书籍中没有的内容**。
|
|
|
|
|
在其他语言,字符串往往是送分题,因为实在是太简单了,例如 `"hello, world"` 就是字符串章节的几乎全部内容了,但是如果你带着同样的想法来学 Rust,我保证,绝对会栽跟头, **因此这一章大家一定要重视,仔细阅读,这里有很多其它 Rust 书籍中没有的内容**。
|
|
|
|
|
|
|
|
|
|
首先来看段很简单的代码:
|
|
|
|
|
```rust
|
|
|
|
@ -53,7 +53,7 @@ let world = &s[6..11];
|
|
|
|
|
|
|
|
|
|
<img alt="" src="https://pic1.zhimg.com/80/v2-69da917741b2c610732d8526a9cc86f5_1440w.jpg" class="center" style="width: 50%;" />
|
|
|
|
|
|
|
|
|
|
在使用 Rust 的 `..` [range序列](../base-type/numbers.md#序列(Range))语法时,如果你想从索引 0 开始,可以使用如下的方式,这两个是等效的:
|
|
|
|
|
在使用 Rust 的 `..` [range序列](https://course.rs/base-type/numbers.html#序列range)语法时,如果你想从索引 0 开始,可以使用如下的方式,这两个是等效的:
|
|
|
|
|
```rust
|
|
|
|
|
let s = String::from("hello");
|
|
|
|
|
|
|
|
|
@ -81,14 +81,14 @@ let slice = &s[0..len];
|
|
|
|
|
let slice = &s[..];
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
>在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是UTF8字符的边界,例如中文在UTF8中占用三个字节,下面的代码就会崩溃:
|
|
|
|
|
>在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是UTF-8字符的边界,例如中文在UTF-8中占用三个字节,下面的代码就会崩溃:
|
|
|
|
|
>```rust
|
|
|
|
|
> let s = "中国人";
|
|
|
|
|
> let a = &s[0..2];
|
|
|
|
|
> println!("{}",a);
|
|
|
|
|
>```
|
|
|
|
|
>因为我们只取 `s` 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&s[0..3]`,则可以正常通过编译。
|
|
|
|
|
> 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF8 字符串,参见[这里](#操作UTF8字符串)
|
|
|
|
|
> 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF-8 字符串,参见[这里](#操作-UTF8-字符串)
|
|
|
|
|
|
|
|
|
|
字符串切片的类型标识是 `&str`,因此我们可以这样声明一个函数,输入 `String` 类型,返回它的切片: `fn first_word(s: &String) -> &str `。
|
|
|
|
|
|
|
|
|
@ -155,11 +155,11 @@ let s: &str = "Hello, world!";
|
|
|
|
|
|
|
|
|
|
## 什么是字符串?
|
|
|
|
|
|
|
|
|
|
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4)**,这样有助于大幅降低字符串所占用的内存空间。
|
|
|
|
|
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4)**,这样有助于大幅降低字符串所占用的内存空间。
|
|
|
|
|
|
|
|
|
|
Rust 在语言级别,只有一种字符串类型: `str`,它通常是以引用类型出现 `&str`,也就是上文提到的字符串切片。虽然语言级别只有上述的 `str` 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 `String` 类型。
|
|
|
|
|
|
|
|
|
|
`str` 类型是硬编码进可执行文件,也无法被修改,但是 `String` 则是一个可增长、可改变且具有所有权的 UTF8 编码字符串,**当 Rust 用户提到字符串时,往往指的就是 `String` 类型和 `&str` 字符串切片类型,这两个类型都是 UTF8 编码**。
|
|
|
|
|
`str` 类型是硬编码进可执行文件,也无法被修改,但是 `String` 则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,**当 Rust 用户提到字符串时,往往指的就是 `String` 类型和 `&str` 字符串切片类型,这两个类型都是 UTF-8 编码**。
|
|
|
|
|
|
|
|
|
|
除了 `String` 类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 `OsString`, `OsStr`, `CsString` 和` CsStr` 等,注意到这些名字都以 `String` 或者 `Str` 结尾了吗?它们分别对应的是具有所有权和被借用的变量。
|
|
|
|
|
|
|
|
|
@ -185,7 +185,7 @@ fn main() {
|
|
|
|
|
assert_eq!(s,"hello,world!");
|
|
|
|
|
|
|
|
|
|
// 从现有的&str切片创建String类型
|
|
|
|
|
// String与&str都是UTF8编码,因此支持中文
|
|
|
|
|
// String与&str都是UTF-8编码,因此支持中文
|
|
|
|
|
let mut s = String::from("你好,世界");
|
|
|
|
|
// 将字符'!'推入s中
|
|
|
|
|
s.push('!');
|
|
|
|
@ -243,7 +243,7 @@ fn say_hello(s: &str) {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
实际上这种灵活用法是因为 `deref` 隐式强制转换,具体我们会在 [Deref特征](../../traits/deref.md)进行详细讲解。
|
|
|
|
|
实际上这种灵活用法是因为 `deref` 隐式强制转换,具体我们会在 [`Deref` 特征](https://course.rs/advance/smart-pointer/deref.html)进行详细讲解。
|
|
|
|
|
|
|
|
|
|
## 字符串索引
|
|
|
|
|
|
|
|
|
@ -262,11 +262,11 @@ fn say_hello(s: &str) {
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 深入字符串内部
|
|
|
|
|
字符串的底层的数据存储格式实际上是[ `u8` ],一个字节数组。对于 `let hello = String::from("Hola");` 这行代码来说, `hello` 的长度是 `4` 个字节,因为 `"hola"` 中的每个字母在 UTF8 编码中仅占用 1 个字节,但是对于下面的代码呢?
|
|
|
|
|
字符串的底层的数据存储格式实际上是[ `u8` ],一个字节数组。对于 `let hello = String::from("Hola");` 这行代码来说, `hello` 的长度是 `4` 个字节,因为 `"hola"` 中的每个字母在 UTF-8 编码中仅占用 1 个字节,但是对于下面的代码呢?
|
|
|
|
|
```rust
|
|
|
|
|
let hello = String::from("中国人");
|
|
|
|
|
```
|
|
|
|
|
如果问你该字符串多长,你可能会说 `3`,但是实际上是 `9` 个字节的长度,因为大部分常用汉字在 UTF8 中的长度是 `3` 个字节,因此这种情况下对 `hello` 进行索引,访问 `&hello[0]` 没有任何意义,因为你取不到 `中` 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
|
|
|
|
|
如果问你该字符串多长,你可能会说 `3`,但是实际上是 `9` 个字节的长度,因为大部分常用汉字在 UTF-8 中的长度是 `3` 个字节,因此这种情况下对 `hello` 进行索引,访问 `&hello[0]` 没有任何意义,因为你取不到 `中` 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
|
|
|
|
|
|
|
|
|
|
#### 字符串的不同表现形式
|
|
|
|
|
现在看一下用梵文写的字符串 `“नमस्ते”`, 它底层的字节数组如下形式:
|
|
|
|
@ -288,7 +288,7 @@ let hello = String::from("中国人");
|
|
|
|
|
还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 `String` 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。
|
|
|
|
|
|
|
|
|
|
## 字符串切片
|
|
|
|
|
前文提到过,字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如:
|
|
|
|
|
前文提到过,字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如:
|
|
|
|
|
```rust
|
|
|
|
|
let hello = "中国人";
|
|
|
|
|
|
|
|
|
@ -303,8 +303,8 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
|
|
|
|
|
|
|
|
|
因此在通过索引区间来访问字符串时,**需要格外的小心**,一不注意,就会导致你程序的崩溃!
|
|
|
|
|
|
|
|
|
|
## 操作UTF8字符串
|
|
|
|
|
前文提到了几种使用 UTF8 字符串的方式,下面来一一说明。
|
|
|
|
|
## 操作 UTF8 字符串
|
|
|
|
|
前文提到了几种使用 UTF-8 字符串的方式,下面来一一说明。
|
|
|
|
|
|
|
|
|
|
#### 字符
|
|
|
|
|
如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 `chars` 方法,例如:
|
|
|
|
@ -341,7 +341,7 @@ for b in "中国人".bytes() {
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 获取子串
|
|
|
|
|
想要准确的从UTF8字符串中获取子串是较为复杂的事情,例如想要从 `holla中国人नमस्ते` 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。
|
|
|
|
|
想要准确的从UTF-8字符串中获取子串是较为复杂的事情,例如想要从 `holla中国人नमस्ते` 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。
|
|
|
|
|
你需要在 `crates.io` 上搜索 `utf8` 来寻找想要的功能。
|
|
|
|
|
|
|
|
|
|
可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice)。
|
|
|
|
@ -355,6 +355,7 @@ for b in "中国人".bytes() {
|
|
|
|
|
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。
|
|
|
|
|
|
|
|
|
|
对于 `String` 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的:
|
|
|
|
|
|
|
|
|
|
- 首先向操作系统请求内存来存放 `String` 对象
|
|
|
|
|
- 在使用完成后,将内存释放,归还给操作系统
|
|
|
|
|
|
|
|
|
|