|
|
|
@ -87,10 +87,10 @@ let slice = &s[..];
|
|
|
|
|
> let a = &s[0..2];
|
|
|
|
|
> println!("{}",a);
|
|
|
|
|
>```
|
|
|
|
|
>因为我们只取`s`字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连`中`字都取不完整,此时程序会直接崩溃退出,如果改成`&a[0..3]`,则可以正常通过编译.
|
|
|
|
|
>因为我们只取 `s` 字符串的前两个字节,但是一个中文占用三个字节,因此没有落在边界处,也就是连 `中` 字都取不完整,此时程序会直接崩溃退出,如果改成 `&a[0..3]`,则可以正常通过编译。
|
|
|
|
|
> 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点, 关于该如何操作 UTF8 字符串,参见[这里](#操作UTF8字符串)
|
|
|
|
|
|
|
|
|
|
字符串切片的类型标识是`&str`,因此我们可以这样声明一个函数,输入`String`类型,返回它的切片: `fn first_word(s: &String) -> &str `.
|
|
|
|
|
字符串切片的类型标识是 `&str`,因此我们可以这样声明一个函数,输入 `String` 类型,返回它的切片: `fn first_word(s: &String) -> &str `。
|
|
|
|
|
|
|
|
|
|
有了切片就可以写出这样的安全代码:
|
|
|
|
|
```rust
|
|
|
|
@ -124,7 +124,7 @@ error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immuta
|
|
|
|
|
从上述代码可以看出,Rust 不仅让我们的 `API` 更加容易使用,而且也在编译期就消除了大量错误!
|
|
|
|
|
|
|
|
|
|
#### 其它切片
|
|
|
|
|
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组:
|
|
|
|
|
因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组:
|
|
|
|
|
```rust
|
|
|
|
|
let a = [1, 2, 3, 4, 5];
|
|
|
|
|
|
|
|
|
@ -152,17 +152,17 @@ let s: &str = "Hello, world!";
|
|
|
|
|
|
|
|
|
|
## 什么是字符串?
|
|
|
|
|
|
|
|
|
|
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust中的字符是Unicode类型,因此每个字符占据4个字节内存空间,但是在字符串中不一样,字符串是UTF8编码,也就是字符所占的字节数是变化的(1-4)**,这样有助于大幅降低字符串所占用的内存空间.
|
|
|
|
|
顾名思义,字符串是由字符组成的连续集合,但是在上一节中我们提到过,**Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF8 编码,也就是字符所占的字节数是变化的(1 - 4)**,这样有助于大幅降低字符串所占用的内存空间。
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
fn main() {
|
|
|
|
|
// 创建一个空String
|
|
|
|
@ -199,12 +199,12 @@ fn main() {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在上面代码中,有一处需要解释的地方,就是使用`+`来对字符串进行相加操作, 这里之所以使用`s1 + &s2`的形式,是因为`+`使用了`add`方法,该方法的定义类似:
|
|
|
|
|
在上面代码中,有一处需要解释的地方,就是使用 `+` 来对字符串进行相加操作, 这里之所以使用 `s1 + &s2` 的形式,是因为 `+` 使用了 `add` 方法,该方法的定义类似:
|
|
|
|
|
```rust
|
|
|
|
|
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
|
|
|
|
@ -216,7 +216,7 @@ let s3 = String::from("toe");
|
|
|
|
|
let s = s1 + "-" + &s2 + "-" + &s3;
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`String` + `&str`返回一个`String`,然后再继续跟一个`&str`进行`+`操作,返回一个`String`类型,不断循环,最终生成一个`s`,也是`String`类型。
|
|
|
|
|
`String + &str`返回一个 `String`,然后再继续跟一个 `&str` 进行 `+` 操作,返回一个 `String` 类型,不断循环,最终生成一个 `s`,也是 `String` 类型。
|
|
|
|
|
|
|
|
|
|
在上面代码中,我们做了一个有些难以理解的 `&String` 操作,下面来展开讲讲。
|
|
|
|
|
|
|
|
|
@ -259,12 +259,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"` 中的每个字母在 UTF8 编码中仅占用 1 个字节,但是对于下面的代码呢?
|
|
|
|
|
```rust
|
|
|
|
|
let hello = String::from("中国人");
|
|
|
|
|
```
|
|
|
|
|
如果问你该字符串多长,你可能会说`3`,但是实际上是`9`个字节的长度,因为每个汉字在UTF8中的长度是`3`个字节,因此这种情况下对`hello`进行索引
|
|
|
|
|
访问`&hello[0]`没有任何意义,因为你取不到`中`这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
|
|
|
|
|
如果问你该字符串多长,你可能会说 `3`,但是实际上是 `9` 个字节的长度,因为每个汉字在 UTF8 中的长度是 `3` 个字节,因此这种情况下对 `hello` 进行索引,访问 `&hello[0]` 没有任何意义,因为你取不到 `中` 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。
|
|
|
|
|
|
|
|
|
|
#### 字符串的不同表现形式
|
|
|
|
|
现在看一下用梵文写的字符串 `“नमस्ते”`, 它底层的字节数组如下形式:
|
|
|
|
@ -299,7 +298,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
|
|
|
|
```
|
|
|
|
|
这里提示的很清楚,我们索引的字节落在了 `中` 字符的内部,这种返回没有任何意义。
|
|
|
|
|
|
|
|
|
|
因此在通过索引区间来访问字符串时,需要格外的小心,一不注意,就会导致你程序的崩溃!
|
|
|
|
|
因此在通过索引区间来访问字符串时,**需要格外的小心**,一不注意,就会导致你程序的崩溃!
|
|
|
|
|
|
|
|
|
|
## 操作UTF8字符串
|
|
|
|
|
前文提到了几种使用 UTF8 字符串的方式,下面来一一说明。
|
|
|
|
@ -339,10 +338,10 @@ for b in "中国人".bytes() {
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 获取子串
|
|
|
|
|
想要准确的从UTF8字符串中获取子串是较为复杂的事情,例如想要从`holla中国人नमस्ते`这种变长的字符串中取出某一个子串,使用标准库你是做不到的,
|
|
|
|
|
想要准确的从UTF8字符串中获取子串是较为复杂的事情,例如想要从 `holla中国人नमस्ते` 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。
|
|
|
|
|
你需要在 `crates.io` 上搜索 `utf8` 来寻找想要的功能。
|
|
|
|
|
|
|
|
|
|
可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice).
|
|
|
|
|
可以考虑尝试下这个库:[utf8_slice](https://crates.io/crates/utf8_slice)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -350,17 +349,17 @@ for b in "中国人".bytes() {
|
|
|
|
|
## 字符串深度剖析
|
|
|
|
|
那么问题来了,为啥 `String` 可变,而字符串字面值 `str` 却不可以?
|
|
|
|
|
|
|
|
|
|
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。
|
|
|
|
|
就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行得过程中动态生成的。
|
|
|
|
|
|
|
|
|
|
对于 `String` 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的:
|
|
|
|
|
- 首先向操作系统请求内存来存放 `String` 对象
|
|
|
|
|
- 在使用完成后,将内存释放,归还给操作系统
|
|
|
|
|
|
|
|
|
|
其中第一个由`String::from`完成,它创建了一个全新的String.
|
|
|
|
|
其中第一个由 `String::from` 完成,它创建了一个全新的 `String`。
|
|
|
|
|
|
|
|
|
|
重点来了,到了第二部分,就是百家齐放的环节,在有**垃圾回收GC**的语言中,GC来负责标记并清除这些不再使用的内存对象,这个过程都是自动完成,无需开发者关心,非常简单好用;但是在无GC的语言中,需要开发者手动去释放这些内存对象,就像创建对象需要通过编写代码来完成一样,未能正确释放对象造成的后果简直不可估量.
|
|
|
|
|
重点来了,到了第二部分,就是百家齐放的环节,在有**垃圾回收 GC** 的语言中,GC 来负责标记并清除这些不再使用的内存对象,这个过程都是自动完成,无需开发者关心,非常简单好用;但是在无 GC 的语言中,需要开发者手动去释放这些内存对象,就像创建对象需要通过编写代码来完成一样,未能正确释放对象造成的后果简直不可估量。
|
|
|
|
|
|
|
|
|
|
对于Rust而言,安全和性能是写到骨子里的核心特性,如果使用GC,那么会牺牲性能;如果使用手动管理内存,那么会牺牲安全,这该怎么办?为此,Rust的开发者想出了一个无比惊艳的办法:变量在离开作用域后,就自动释放其占用的内存:
|
|
|
|
|
对于 Rust 而言,安全和性能是写到骨子里的核心特性,如果使用 GC,那么会牺牲性能;如果使用手动管理内存,那么会牺牲安全,这该怎么办?为此,Rust 的开发者想出了一个无比惊艳的办法:变量在离开作用域后,就自动释放其占用的内存:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
{
|
|
|
|
@ -371,7 +370,7 @@ for b in "中国人".bytes() {
|
|
|
|
|
// 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` 函数并不陌生
|
|
|
|
|
|
|
|
|
|