|
|
@ -1,6 +1,6 @@
|
|
|
|
# 字符串
|
|
|
|
# 字符串
|
|
|
|
|
|
|
|
|
|
|
|
在其他语言,字符串往往是送分题,因为实在是太简单了,例如`"hello, world"`就是字符串章节的几乎全部内容了,对吧?如果你带着这样的想法来学Rust,我保证,绝对会栽跟头,**因此这一章大家一定要重视,仔细阅读,这里有很多其它Rust书籍中没有的内容**。
|
|
|
|
在其他语言,字符串往往是送分题,因为实在是太简单了,例如 `"hello, world"` 就是字符串章节的几乎全部内容了,对吧?如果你带着这样的想法来学 Rust,我保证,绝对会栽跟头,**因此这一章大家一定要重视,仔细阅读,这里有很多其它 Rust 书籍中没有的内容**。
|
|
|
|
|
|
|
|
|
|
|
|
首先来看段很简单的代码:
|
|
|
|
首先来看段很简单的代码:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
@ -14,7 +14,7 @@ fn greet(name: String) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`greet`函数接受一个字符串类型的`name`参数,然后打印到终端控制台中,非常好理解,你们猜猜,这段代码能否通过编译?
|
|
|
|
`greet` 函数接受一个字符串类型的 `name` 参数,然后打印到终端控制台中,非常好理解,你们猜猜,这段代码能否通过编译?
|
|
|
|
|
|
|
|
|
|
|
|
```conole
|
|
|
|
```conole
|
|
|
|
error[E0308]: mismatched types
|
|
|
|
error[E0308]: mismatched types
|
|
|
@ -29,15 +29,15 @@ error[E0308]: mismatched types
|
|
|
|
error: aborting due to previous error
|
|
|
|
error: aborting due to previous error
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Bingo,果然报错了,编译器提示`greet`函数需要一个`String`类型的字符串,却传入了一个`&str`类型的字符串,相信读者心中现在一定有几头草泥马呼啸而过,怎么字符串也能整出这么多花活?
|
|
|
|
Bingo,果然报错了,编译器提示 `greet` 函数需要一个 `String` 类型的字符串,却传入了一个 `&str` 类型的字符串,相信读者心中现在一定有几头草泥马呼啸而过,怎么字符串也能整出这么多花活?
|
|
|
|
|
|
|
|
|
|
|
|
在讲解字符串之前,先来看看什么是切片?
|
|
|
|
在讲解字符串之前,先来看看什么是切片?
|
|
|
|
|
|
|
|
|
|
|
|
## 切片(slice)
|
|
|
|
## 切片(slice)
|
|
|
|
|
|
|
|
|
|
|
|
切片并不是Rust独有的概念,在Go语言中就非常流行,它允许你引用集合中部分连续的元素序列,而不是引用整个集合。
|
|
|
|
切片并不是 Rust 独有的概念,在 Go 语言中就非常流行,它允许你引用集合中部分连续的元素序列,而不是引用整个集合。
|
|
|
|
|
|
|
|
|
|
|
|
对于字符串而言,切片就是对`String`类型中某一部分的引用,它看起来像这样:
|
|
|
|
对于字符串而言,切片就是对 `String` 类型中某一部分的引用,它看起来像这样:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
let s = String::from("hello world");
|
|
|
|
let s = String::from("hello world");
|
|
|
|
|
|
|
|
|
|
|
@ -45,15 +45,15 @@ let hello = &s[0..5];
|
|
|
|
let world = &s[6..11];
|
|
|
|
let world = &s[6..11];
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`hello`没有引用整个`String s`,而是引用了`s`的一部分内容,通过`[0..5]`的方式来指定。
|
|
|
|
`hello` 没有引用整个 `String s`,而是引用了 `s` 的一部分内容,通过 `[0..5]` 的方式来指定。
|
|
|
|
|
|
|
|
|
|
|
|
这就是创建切片的语法,使用方括号包括的一个序列: **[开始索引..终止索引]**,其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个`右半开区间`。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过`终止索引` - `开始索引`的方式计算得来的。
|
|
|
|
这就是创建切片的语法,使用方括号包括的一个序列: **[开始索引..终止索引]**,其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 `右半开区间`。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过 `终止索引` - `开始索引` 的方式计算得来的。
|
|
|
|
|
|
|
|
|
|
|
|
对于`let world = &s[6..11];`来说,`world`是一个切片,该切片的指针指向`s`的第7个字节(索引从0开始,6是第7个字节),且该切片的长度是`5`个字节。
|
|
|
|
对于 `let world = &s[6..11];` 来说,`world` 是一个切片,该切片的指针指向 `s` 的第 7 个字节(索引从 0 开始, 6 是第 7 个字节),且该切片的长度是 `5` 个字节。
|
|
|
|
|
|
|
|
|
|
|
|
<img alt="" src="/img/string-01.svg" class="center" style="width: 50%;" />
|
|
|
|
<img alt="" src="/img/string-01.svg" class="center" style="width: 50%;" />
|
|
|
|
|
|
|
|
|
|
|
|
在使用Rust的`..`[range序列](../base-type/numbers.md#序列(Range))语法时,如果你想从索引0开始,可以使用如下的方式,这两个是等效的:
|
|
|
|
在使用 Rust 的 `..` [range序列](../base-type/numbers.md#序列(Range))语法时,如果你想从索引 0 开始,可以使用如下的方式,这两个是等效的:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
let s = String::from("hello");
|
|
|
|
let s = String::from("hello");
|
|
|
|
|
|
|
|
|
|
|
@ -61,7 +61,7 @@ let slice = &s[0..2];
|
|
|
|
let slice = &s[..2];
|
|
|
|
let slice = &s[..2];
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
同样的,如果你的切片想要包含`String`的最后一个字节,则可以这样使用:
|
|
|
|
同样的,如果你的切片想要包含 `String` 的最后一个字节,则可以这样使用:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
let s = String::from("hello");
|
|
|
|
let s = String::from("hello");
|
|
|
|
|
|
|
|
|
|
|
@ -71,7 +71,7 @@ let slice = &s[4..len];
|
|
|
|
let slice = &s[4..];
|
|
|
|
let slice = &s[4..];
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
你也可以截取完整的`String`切片:
|
|
|
|
你也可以截取完整的 `String` 切片:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
let s = String::from("hello");
|
|
|
|
let s = String::from("hello");
|
|
|
|
|
|
|
|
|
|
|
@ -87,10 +87,10 @@ let slice = &s[..];
|
|
|
|
> let a = &s[0..2];
|
|
|
|
> let a = &s[0..2];
|
|
|
|
> println!("{}",a);
|
|
|
|
> println!("{}",a);
|
|
|
|
>```
|
|
|
|
>```
|
|
|
|
>因为我们只取`s`字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连`中`字都取不完整,此时程序会直接崩溃退出,如果改成`&a[0..3]`,则可以正常通过编译.
|
|
|
|
>因为我们只取 `s` 字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&a[0..3]`,则可以正常通过编译。
|
|
|
|
> 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作UTF8字符串,参见[这里](#操作UTF8字符串)
|
|
|
|
> 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF8 字符串,参见[这里](#操作UTF8字符串)
|
|
|
|
|
|
|
|
|
|
|
|
字符串切片的类型标识是`&str`,因此我们可以这样声明一个函数,输入`String`类型,返回它的切片: `fn first_word(s: &String) -> &str `.
|
|
|
|
字符串切片的类型标识是 `&str`,因此我们可以这样声明一个函数,输入 `String` 类型,返回它的切片: `fn first_word(s: &String) -> &str `。
|
|
|
|
|
|
|
|
|
|
|
|
有了切片就可以写出这样的安全代码:
|
|
|
|
有了切片就可以写出这样的安全代码:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
@ -119,12 +119,12 @@ error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immuta
|
|
|
|
| ---- immutable borrow later used here
|
|
|
|
| ---- immutable borrow later used here
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
回忆一下借用的规则:当我们已经有了可变借用时,就无法再拥有不可变的借用。因为`clear`需要清空改变`String`,因此它需要一个可变借用,而之后的`println!`又使用了不可变借用,因此编译无法通过。
|
|
|
|
回忆一下借用的规则:当我们已经有了可变借用时,就无法再拥有不可变的借用。因为 `clear` 需要清空改变 `String`,因此它需要一个可变借用,而之后的 `println!` 又使用了不可变借用,因此编译无法通过。
|
|
|
|
|
|
|
|
|
|
|
|
从上述代码可以看出,Rust不仅让我们的`API`更加容易使用,而且也在编译期就消除了大量错误!
|
|
|
|
从上述代码可以看出,Rust 不仅让我们的 `API` 更加容易使用,而且也在编译期就消除了大量错误!
|
|
|
|
|
|
|
|
|
|
|
|
#### 其它切片
|
|
|
|
#### 其它切片
|
|
|
|
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组:
|
|
|
|
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
let a = [1, 2, 3, 4, 5];
|
|
|
|
let a = [1, 2, 3, 4, 5];
|
|
|
|
|
|
|
|
|
|
|
@ -132,7 +132,7 @@ let slice = &a[1..3];
|
|
|
|
|
|
|
|
|
|
|
|
assert_eq!(slice, &[2, 3]);
|
|
|
|
assert_eq!(slice, &[2, 3]);
|
|
|
|
```
|
|
|
|
```
|
|
|
|
该数组切片的类型是`&[i32]`,数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。
|
|
|
|
该数组切片的类型是 `&[i32]`,数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 字符串字面量是切片
|
|
|
|
## 字符串字面量是切片
|
|
|
@ -142,27 +142,27 @@ assert_eq!(slice, &[2, 3]);
|
|
|
|
let s = "Hello, world!";
|
|
|
|
let s = "Hello, world!";
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
实际上,`s`的类型是`&str`,因此你也可以这样声明:
|
|
|
|
实际上,`s` 的类型是 `&str`,因此你也可以这样声明:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
let s: &str = "Hello, world!";
|
|
|
|
let s: &str = "Hello, world!";
|
|
|
|
```
|
|
|
|
```
|
|
|
|
该切片指向了程序可执行文件中的某个点,这也是为什么字符串字面量是不可变的,因为`&str`是一个不可变引用。
|
|
|
|
该切片指向了程序可执行文件中的某个点,这也是为什么字符串字面量是不可变的,因为 `&str` 是一个不可变引用。
|
|
|
|
|
|
|
|
|
|
|
|
了解完切片,可以进入本节的正题了。
|
|
|
|
了解完切片,可以进入本节的正题了。
|
|
|
|
|
|
|
|
|
|
|
|
## 什么是字符串?
|
|
|
|
## 什么是字符串?
|
|
|
|
|
|
|
|
|
|
|
|
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust中的字符是Unicode类型,因此每个字符占据4个字节内存空间,但是在字符串中不一样,字符串是UTF8编码,也就是字符所占的字节数是变化的(1-4)**,这样有助于大幅降低字符串所占用的内存空间.
|
|
|
|
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF8 编码,也就是字符所占的字节数是变化的(1 - 4)**,这样有助于大幅降低字符串所占用的内存空间。
|
|
|
|
|
|
|
|
|
|
|
|
Rust在语言级别,只有一种字符串类型:`str`,它通常是以引用类型出现`&str`,也就是上文提到的字符串切片。虽然语言级别只有上述的`str`类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是`String`类型。
|
|
|
|
Rust 在语言级别,只有一种字符串类型: `str`,它通常是以引用类型出现 `&str`,也就是上文提到的字符串切片。虽然语言级别只有上述的 `str` 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 `String` 类型。
|
|
|
|
|
|
|
|
|
|
|
|
`str`类型是硬编码进可执行文件,也无法被修改,但是`String`则是一个可增长、可改变且具有所有权的UTF8编码字符串,**当Rust用户提到字符串时,往往指的就是`String`类型和`&str`字符串切片类型,这两个类型都是UTF8编码**.
|
|
|
|
`str` 类型是硬编码进可执行文件,也无法被修改,但是 `String` 则是一个可增长、可改变且具有所有权的 UTF8 编码字符串,**当 Rust 用户提到字符串时,往往指的就是 `String` 类型和 `&str` 字符串切片类型,这两个类型都是 UTF8 编码**。
|
|
|
|
|
|
|
|
|
|
|
|
除了`String`类型的字符串,Rust的标准库还提供了其他类型的字符串,例如`OsString`,`OsStr`,`CsString`和`CsStr`等,注意到这些名字都以`String`或者`Str`结尾了吗?它们分别对应的是具有所有权和被借用的变量。
|
|
|
|
除了 `String` 类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 `OsString`, `OsStr`, `CsString` 和` CsStr` 等,注意到这些名字都以 `String` 或者 `Str` 结尾了吗?它们分别对应的是具有所有权和被借用的变量。
|
|
|
|
|
|
|
|
|
|
|
|
#### 操作字符串
|
|
|
|
#### 操作字符串
|
|
|
|
|
|
|
|
|
|
|
|
由于String是可变字符串,因此我们可以对它进行创建、增删操作,下面的代码汇总了相关的操作方式:
|
|
|
|
由于 `String` 是可变字符串,因此我们可以对它进行创建、增删操作,下面的代码汇总了相关的操作方式:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
fn main() {
|
|
|
|
fn main() {
|
|
|
|
// 创建一个空String
|
|
|
|
// 创建一个空String
|
|
|
@ -199,12 +199,12 @@ fn main() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
在上面代码中,有一处需要解释的地方,就是使用`+`来对字符串进行相加操作, 这里之所以使用`s1 + &s2`的形式,是因为`+`使用了`add`方法,该方法的定义类似:
|
|
|
|
在上面代码中,有一处需要解释的地方,就是使用 `+` 来对字符串进行相加操作, 这里之所以使用 `s1 + &s2` 的形式,是因为 `+` 使用了 `add` 方法,该方法的定义类似:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
fn add(self, s: &str) -> String {
|
|
|
|
fn add(self, s: &str) -> String {
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
因为该方法涉及到更复杂的特征功能,因此我们这里简单说明下,`self`是`String`类型的字符串`s1`,该函数说明,只能将`&str`类型的字符串切片添加到`String`类型的`s1`上,然后返回一个新的`String`类型,所以`let s3 = s1 + &s2;`就很好解释了,将`String`类型的`s1`与`&str`类型的`s2`进行相加,最终得到`String`类型的s3.
|
|
|
|
因为该方法涉及到更复杂的特征功能,因此我们这里简单说明下, `self` 是 `String` 类型的字符串 `s1`,该函数说明,只能将 `&str` 类型的字符串切片添加到 `String` 类型的 `s1` 上,然后返回一个新的 `String` 类型,所以 `let s3 = s1 + &s2;` 就很好解释了,将 `String` 类型的 `s1` 与 `&str` 类型的 `s2` 进行相加,最终得到 `String` 类型的 `s3`。
|
|
|
|
|
|
|
|
|
|
|
|
由此可推,以下代码也是合法的:
|
|
|
|
由此可推,以下代码也是合法的:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
@ -216,17 +216,17 @@ let s3 = String::from("toe");
|
|
|
|
let s = s1 + "-" + &s2 + "-" + &s3;
|
|
|
|
let s = s1 + "-" + &s2 + "-" + &s3;
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`String` + `&str`返回一个`String`,然后再继续跟一个`&str`进行`+`操作,返回一个`String`类型,不断循环,最终生成一个`s`,也是`String`类型。
|
|
|
|
`String + &str`返回一个 `String`,然后再继续跟一个 `&str` 进行 `+` 操作,返回一个 `String` 类型,不断循环,最终生成一个 `s`,也是 `String` 类型。
|
|
|
|
|
|
|
|
|
|
|
|
在上面代码中,我们做了一个有些难以理解的`&String`操作,下面来展开讲讲。
|
|
|
|
在上面代码中,我们做了一个有些难以理解的 `&String` 操作,下面来展开讲讲。
|
|
|
|
|
|
|
|
|
|
|
|
## String与&str的转换
|
|
|
|
## String与&str的转换
|
|
|
|
|
|
|
|
|
|
|
|
在之前的代码中,已经见到好几种从`&str`类型生成`String`类型的操作:
|
|
|
|
在之前的代码中,已经见到好几种从 `&str` 类型生成 `String` 类型的操作:
|
|
|
|
- `String::from("hello,world")`
|
|
|
|
- `String::from("hello,world")`
|
|
|
|
- `"hello,world".to_string()`
|
|
|
|
- `"hello,world".to_string()`
|
|
|
|
|
|
|
|
|
|
|
|
那么如何将`String`类型转为`&str`类型呢?答案很简单,取引用即可:
|
|
|
|
那么如何将 `String` 类型转为 `&str` 类型呢?答案很简单,取引用即可:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
fn main() {
|
|
|
|
fn main() {
|
|
|
|
let s = String::from("hello,world!");
|
|
|
|
let s = String::from("hello,world!");
|
|
|
@ -240,11 +240,11 @@ fn say_hello(s: &str) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
实际上这种灵活用法是因为`deref`隐式强制转换,具体我们会在[Deref特征](../../traits/deref.md)进行详细讲解。
|
|
|
|
实际上这种灵活用法是因为 `deref` 隐式强制转换,具体我们会在 [Deref特征](../../traits/deref.md)进行详细讲解。
|
|
|
|
|
|
|
|
|
|
|
|
## 字符串索引
|
|
|
|
## 字符串索引
|
|
|
|
|
|
|
|
|
|
|
|
在其它语言中,使用索引的方式访问字符串的某个字符或者子串是很正常的行为,但是在Rust中就会报错:
|
|
|
|
在其它语言中,使用索引的方式访问字符串的某个字符或者子串是很正常的行为,但是在 Rust 中就会报错:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
let s1 = String::from("hello");
|
|
|
|
let s1 = String::from("hello");
|
|
|
|
let h = s1[0];
|
|
|
|
let h = s1[0];
|
|
|
@ -259,15 +259,14 @@ 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"` 中的每个字母在 UTF8 编码中仅占用 1 个字节,但是对于下面的代码呢?
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
let hello = String::from("中国人");
|
|
|
|
let hello = String::from("中国人");
|
|
|
|
```
|
|
|
|
```
|
|
|
|
如果问你该字符串多长,你可能会说`3`,但是实际上是`9`个字节的长度,因为每个汉字在UTF8中的长度是`3`个字节,因此这种情况下对`hello`进行索引
|
|
|
|
如果问你该字符串多长,你可能会说 `3`,但是实际上是 `9` 个字节的长度,因为每个汉字在 UTF8 中的长度是 `3` 个字节,因此这种情况下对 `hello` 进行索引,访问 `&hello[0]` 没有任何意义,因为你取不到 `中` 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
|
|
|
|
访问`&hello[0]`没有任何意义,因为你取不到`中`这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 字符串的不同表现形式
|
|
|
|
#### 字符串的不同表现形式
|
|
|
|
现在看一下用梵文写的字符串`“नमस्ते”`, 它底层的字节数组如下形式:
|
|
|
|
现在看一下用梵文写的字符串 `“नमस्ते”`, 它底层的字节数组如下形式:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
|
|
|
|
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
|
|
|
|
224, 165, 135]
|
|
|
|
224, 165, 135]
|
|
|
@ -281,12 +280,12 @@ let hello = String::from("中国人");
|
|
|
|
["न", "म", "स्", "ते"]
|
|
|
|
["न", "म", "स्", "ते"]
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
所以,可以看出来Rust提供了不同的字符串展现方式,这样程序可以挑选自己想要的方式去使用,而无需去管字符串从人类语言角度看长什么样。
|
|
|
|
所以,可以看出来 Rust 提供了不同的字符串展现方式,这样程序可以挑选自己想要的方式去使用,而无需去管字符串从人类语言角度看长什么样。
|
|
|
|
|
|
|
|
|
|
|
|
还有一个原因导致了Rust不允许去索引字符:因为索引操作,我们总是期望它的性能表现是O(1),然而对于`String`类型来说,无法保证这一点,因为Rust可能需要从0开始去遍历字符串来定位合法的字符。
|
|
|
|
还有一个原因导致了 Rust 不允许去索引字符:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 `String` 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。
|
|
|
|
|
|
|
|
|
|
|
|
## 字符串切片
|
|
|
|
## 字符串切片
|
|
|
|
前文提到过,字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是UTF8编码,因此你无法保证索引的字节刚好落在字符的边界上,例如:
|
|
|
|
前文提到过,字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
let hello = "中国人";
|
|
|
|
let hello = "中国人";
|
|
|
|
|
|
|
|
|
|
|
@ -297,15 +296,15 @@ let s = &hello[0..2];
|
|
|
|
thread 'main' panicked at 'byte index 2 is not a char boundary; it is inside '中' (bytes 0..3) of `中国人`', src/main.rs:4:14
|
|
|
|
thread 'main' panicked at 'byte index 2 is not a char boundary; it is inside '中' (bytes 0..3) of `中国人`', src/main.rs:4:14
|
|
|
|
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
|
|
|
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
|
|
|
```
|
|
|
|
```
|
|
|
|
这里提示的很清楚,我们索引的字节落在了`中`字符的内部,这种返回没有任何意义。
|
|
|
|
这里提示的很清楚,我们索引的字节落在了 `中` 字符的内部,这种返回没有任何意义。
|
|
|
|
|
|
|
|
|
|
|
|
因此在通过索引区间来访问字符串时,需要格外的小心,一不注意,就会导致你程序的崩溃!
|
|
|
|
因此在通过索引区间来访问字符串时,**需要格外的小心**,一不注意,就会导致你程序的崩溃!
|
|
|
|
|
|
|
|
|
|
|
|
## 操作UTF8字符串
|
|
|
|
## 操作UTF8字符串
|
|
|
|
前文提到了几种使用UTF8字符串的方式,下面来一一说明。
|
|
|
|
前文提到了几种使用 UTF8 字符串的方式,下面来一一说明。
|
|
|
|
|
|
|
|
|
|
|
|
#### 字符
|
|
|
|
#### 字符
|
|
|
|
如果你想要以Unicode字符的方式遍历字符串,最好的办法是使用`chars`方法,例如:
|
|
|
|
如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 `chars` 方法,例如:
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
for c in "中国人".chars() {
|
|
|
|
for c in "中国人".chars() {
|
|
|
|
println!("{}", c);
|
|
|
|
println!("{}", c);
|
|
|
@ -339,28 +338,28 @@ for b in "中国人".bytes() {
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### 获取子串
|
|
|
|
#### 获取子串
|
|
|
|
想要准确的从UTF8字符串中获取子串是较为复杂的事情,例如想要从`holla中国人नमस्ते`这种变长的字符串中取出某一个子串,使用标准库你是做不到的,
|
|
|
|
想要准确的从UTF8字符串中获取子串是较为复杂的事情,例如想要从 `holla中国人नमस्ते` 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。
|
|
|
|
你需要在`crates.io`上搜索`utf8`来寻找想要的功能。
|
|
|
|
你需要在 `crates.io` 上搜索 `utf8` 来寻找想要的功能。
|
|
|
|
|
|
|
|
|
|
|
|
可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice).
|
|
|
|
可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 字符串深度剖析
|
|
|
|
## 字符串深度剖析
|
|
|
|
那么问题来了,为啥`String`可变,而字符串字面值`str`却不可以?
|
|
|
|
那么问题来了,为啥 `String` 可变,而字符串字面值 `str` 却不可以?
|
|
|
|
|
|
|
|
|
|
|
|
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。
|
|
|
|
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。
|
|
|
|
|
|
|
|
|
|
|
|
对于 `String` 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的:
|
|
|
|
对于 `String` 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的:
|
|
|
|
- 首先向操作系统请求内存来存放`String`对象
|
|
|
|
- 首先向操作系统请求内存来存放 `String` 对象
|
|
|
|
- 在使用完成后,将内存释放,归还给操作系统
|
|
|
|
- 在使用完成后,将内存释放,归还给操作系统
|
|
|
|
|
|
|
|
|
|
|
|
其中第一个由`String::from`完成,它创建了一个全新的String.
|
|
|
|
其中第一个由 `String::from` 完成,它创建了一个全新的 `String`。
|
|
|
|
|
|
|
|
|
|
|
|
重点来了,到了第二部分,就是百家齐放的环节,在有**垃圾回收GC**的语言中,GC来负责标记并清除这些不再使用的内存对象,这个过程都是自动完成,无需开发者关心,非常简单好用;但是在无GC的语言中,需要开发者手动去释放这些内存对象,就像创建对象需要通过编写代码来完成一样,未能正确释放对象造成的后果简直不可估量.
|
|
|
|
重点来了,到了第二部分,就是百家齐放的环节,在有**垃圾回收 GC** 的语言中,GC 来负责标记并清除这些不再使用的内存对象,这个过程都是自动完成,无需开发者关心,非常简单好用;但是在无 GC 的语言中,需要开发者手动去释放这些内存对象,就像创建对象需要通过编写代码来完成一样,未能正确释放对象造成的后果简直不可估量。
|
|
|
|
|
|
|
|
|
|
|
|
对于Rust而言,安全和性能是写到骨子里的核心特性,如果使用GC,那么会牺牲性能;如果使用手动管理内存,那么会牺牲安全,这该怎么办?为此,Rust的开发者想出了一个无比惊艳的办法:变量在离开作用域后,就自动释放其占用的内存:
|
|
|
|
对于 Rust 而言,安全和性能是写到骨子里的核心特性,如果使用 GC,那么会牺牲性能;如果使用手动管理内存,那么会牺牲安全,这该怎么办?为此,Rust 的开发者想出了一个无比惊艳的办法:变量在离开作用域后,就自动释放其占用的内存:
|
|
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
```rust
|
|
|
|
{
|
|
|
|
{
|
|
|
@ -371,7 +370,7 @@ for b in "中国人".bytes() {
|
|
|
|
// s 不再有效,内存被释放
|
|
|
|
// s 不再有效,内存被释放
|
|
|
|
```
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
与其它系统编程语言的`free`函数相同,Rust也提供了一个释放内存的函数:`drop`,但是不同的是,其它语言要手动调用`free`来释放每一个变量占用的内存,而Rust则在变量离开作用域时,自动调用`drop`函数: 上面代码中,Rust 在结尾的 `}` 处自动调用 `drop`。
|
|
|
|
与其它系统编程语言的 `free` 函数相同,Rust 也提供了一个释放内存的函数: `drop`,但是不同的是,其它语言要手动调用 `free` 来释放每一个变量占用的内存,而 Rust 则在变量离开作用域时,自动调用 `drop` 函数: 上面代码中,Rust 在结尾的 `}` 处自动调用 `drop`。
|
|
|
|
|
|
|
|
|
|
|
|
> 其实,在 C++ 中,也有这种概念: *Resource Acquisition Is Initialization (RAII)*。如果你使用过 RAII 模式的话应该对 Rust 的 `drop` 函数并不陌生
|
|
|
|
> 其实,在 C++ 中,也有这种概念: *Resource Acquisition Is Initialization (RAII)*。如果你使用过 RAII 模式的话应该对 Rust 的 `drop` 函数并不陌生
|
|
|
|
|
|
|
|
|
|
|
|