From c664cf923fda4c1bec91c1f9c0c16272e1510614 Mon Sep 17 00:00:00 2001 From: yang yue Date: Fri, 24 Feb 2017 00:21:29 +0800 Subject: [PATCH] wip --- docs/ch08-01-vectors.html | 97 ++++++++- docs/ch08-02-strings.html | 174 +++++++++++++++- docs/ch08-03-hash-maps.html | 128 +++++++++++- docs/print.html | 393 ++++++++++++++++++++++++++++++++++++ src/ch08-01-vectors.md | 140 +++++++++++++ src/ch08-02-strings.md | 286 ++++++++++++++++++++++++++ src/ch08-03-hash-maps.md | 179 ++++++++++++++++ 7 files changed, 1394 insertions(+), 3 deletions(-) diff --git a/docs/ch08-01-vectors.html b/docs/ch08-01-vectors.html index 1499015..a54583a 100644 --- a/docs/ch08-01-vectors.html +++ b/docs/ch08-01-vectors.html @@ -67,7 +67,102 @@
- +

vector

+
+

ch08-01-vectors.md +
+commit 0d229cc5a3da341196e15a6761735b2952281569

+
+

我们要讲到的第一个类型是Vec<T>,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。

+

新建 vector

+

为了创建一个新的,空的 vector,可以调用Vec::new函数:

+
let v: Vec<i32> = Vec::new();
+
+

注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是同质的(homogeneous):他们可以储存很多值,不过这些值必须都是相同类型的。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用他们。现在,所有你需要知道的就是Vec是一个由标准库提供的类型,它可以存放任何类型,而当Vec存放某个特定类型时,那个类型位于尖括号中。这里我们告诉 Rust v这个Vec将存放i32类型的元素。

+

在实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用初始值来创建一个Vec,而且为了方便 Rust 提供了vec!宏。这个宏会根据我们提供的值来创建一个新的Vec。如下代码会新建一个拥有值123Vec<i32>

+
let v = vec![1, 2, 3];
+
+

因为我们提供了i32类型的初始值,Rust 可以推断出v的类型是Vec<i32>,因此类型注解就不是必须的。接下来让我们看看如何修改一个 vector。

+

更新 vector

+

对于新建一个 vector 并向其增加元素,可以使用push方法:

+
let mut v = Vec::new();
+
+v.push(5);
+v.push(6);
+v.push(7);
+v.push(8);
+
+

如第三章中讨论的任何变量一样,如果想要能够改变它的值,必须使用mut关键字使其可变。放入其中的所有值都是i32类型的,而且 Rust 也根据数据如此判断,所以不需要Vec<i32>注解。

+

丢弃 vector 时也会丢弃其所有元素

+

类似于任何其他的struct,vector 在其离开作用域时会被释放:

+
{
+    let v = vec![1, 2, 3, 4];
+
+    // do stuff with v
+
+} // <- v goes out of scope and is freed here
+
+

当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。这可能看起来非常直观,不过一旦开始使用 vector 元素的引用情况就变得有些复杂了。下面让我们处理这种情况!

+

读取 vector 的元素

+

现在你知道如何创建、更新和销毁 vector 了,接下来的一步最好了解一下如何读取他们的内容。有两种方法引用 vector 中储存的值。为了更加清楚的说明这个例子,我们标注这些函数返回的值的类型。

+

这个例子展示了访问 vector 中一个值的两种方式,索引语法或者get方法:

+
let v = vec![1, 2, 3, 4, 5];
+
+let third: &i32 = &v[2];
+let third: Option<&i32> = v.get(2);
+
+

这里有一些需要注意的地方。首先,我们使用索引值2来获取第三个元素,索引是从 0 开始的。其次,这两个不同的获取第三个元素的方式分别为:使用&[]返回一个引用;或者使用get方法以索引作为参数来返回一个Option<&T>

+

Rust 有两个引用元素的方法的原因是程序可以选择如何处理当索引值在 vector 中没有对应值的情况。例如如下情况,如果有一个有五个元素的 vector 接着尝试访问索引为 100 的元素,程序该如何处理:

+
let v = vec![1, 2, 3, 4, 5];
+
+let does_not_exist = &v[100];
+let does_not_exist = v.get(100);
+
+

当运行这段代码,你会发现对于第一个[]方法,当引用一个不存在的元素时 Rust 会造成panic!。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。

+

get方法被传递了一个数组外的索引时,它不会 panic 而是返回None。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理Some(&element)None的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果他们不慎输入了一个过大的数字那么程序就会得到None值,你可以告诉用户Vec当前元素的数量并再请求他们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多!

+

无效引用

+

一旦程序获取了一个有效的引用,借用检查器将会执行第四章讲到的所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于这个例子,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候:

+
let mut v = vec![1, 2, 3, 4, 5];
+
+let first = &v[0];
+
+v.push(6);
+
+

编译会给出这个错误:

+
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
+immutable
+  |
+4 | let first = &v[0];
+  |              - immutable borrow occurs here
+5 |
+6 | v.push(6);
+  | ^ mutable borrow occurs here
+7 | }
+  | - immutable borrow ends here
+
+

这些代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式。在 vector 的结尾增加新元素是,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。

+
+

注意:关于更多内容,查看 Nomicon https://doc.rust-lang.org/stable/nomicon/vec.html

+
+

使用枚举来储存多种类型

+

在本章的开始,我们提到 vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!

+

例如,假如我们想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串。我们可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型,那个枚举的类型。接着可以创建一个储存枚举值的 vector,这样最终就能够储存不同类型的值了:

+
enum SpreadsheetCell {
+    Int(i32),
+    Float(f64),
+    Text(String),
+}
+
+let row = vec![
+    SpreadsheetCell::Int(3),
+    SpreadsheetCell::Text(String::from("blue")),
+    SpreadsheetCell::Float(10.12),
+];
+
+

Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个优点是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加match意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。

+

如果在编写程序时不能确切无遗的知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。

+

现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中Vec定义的很多其他实用方法的 API 文档。例如,除了push之外还有一个pop方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:String

+
diff --git a/docs/ch08-02-strings.html b/docs/ch08-02-strings.html index a76d0b6..e461c77 100644 --- a/docs/ch08-02-strings.html +++ b/docs/ch08-02-strings.html @@ -67,7 +67,179 @@
- +

字符串

+
+

ch08-02-strings.md +
+commit 4dc0001ccba4189e210ba47d6fe6c3c5fa729da6

+
+

第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解一下它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。

+

字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到String那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论String于其他集合不一样的地方,例如索引String是很复杂的,由于人和计算机理解String数据的不同方式。

+

什么是字符串?

+

在开始深入这些方面之前,我们需要讨论一下术语字符串的具体意义。Rust 的核心语言中事实上就只有一种字符串类型:str,字符串 slice,它通常以被借用的形式出现,&str。第四章讲到了字符串 slice:他们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。

+

称作String的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的“字符串”时,他们通常指的是String和字符串 slice &str类型,而不是其中一个。这一部分大部分是关于String的,不过这些类型在 Rust 标准库中都被广泛使用。String和字符串 slice 都是 UTF-8 编码的。

+

Rust 标准库中还包含一系列其他字符串类型,比如OsStringOsStrCStringCStr。相关库 crate 甚至会提供更多储存字符串数据的选择。与*String/*Str的命名类似,他们通常也提供有所有权和可借用的变体,就比如说String/&str。这些字符串类型在储存的编码或内存表现形式上可能有所不同。本章将不会讨论其他这些字符串类型;查看 API 文档来更多的了解如何使用他们以及各自适合的场景。

+

新建字符串

+

很多Vec可用的操作在String中同样可用,从以new函数创建字符串开始,像这样:

+
let s = String::new();
+
+

这新建了一个叫做s的空的字符串,接着我们可以向其中装载数据。

+

通常字符串会有初始数据因为我们希望一开始就有这个字符串。为此,使用to_string方法,它能用于任何实现了Display trait 的类型,对于字符串字面值是这样:

+
let data = "initial contents";
+
+let s = data.to_string();
+
+// the method also works on a literal directly:
+let s = "initial contents".to_string();
+
+

这会创建一个包好initial contents的字符串。

+

也可以使用String::from函数来从字符串字面值创建String。如下等同于使用to_string

+
let s = String::from("initial contents");
+
+

因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多于,不过都有其用武之地!在这个例子中,String::from.to_string最终做了完全相同的工作,所以如何选择就是风格问题了。

+

记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据:

+
let hello = "السلام عليكم";
+let hello = "Dobrý den";
+let hello = "Hello";
+let hello = "שָׁלוֹם";
+let hello = "नमस्ते";
+let hello = "こんにちは";
+let hello = "안녕하세요";
+let hello = "你好";
+let hello = "Olá";
+let hello = "Здравствуйте";
+let hello = "Hola";
+
+

更新字符串

+

String的大小可以增长其内容也可以改变,就像可以放入更多数据来改变Vec的内容一样。另外,String实现了+运算符作为级联运算符以便于使用。

+

附加字符串

+

可以通过push_str方法来附加字符串 slice,从而使String变长:

+
let mut s = String::from("foo");
+s.push_str("bar");
+
+

执行这两行代码之后s将会包含“foobar”。push_str方法获取字符串 slice,因为并不需要获取参数的所有权。例如,如果将s2的内容附加到s1中后自身不能被使用就糟糕了:

+
let mut s1 = String::from("foo");
+let s2 = String::from("bar");
+s1.push_str(&s2);
+
+

push方法被定义为获取一个单独的字符作为参数,并附加到String中:

+
let mut s = String::from("lo");
+s.push('l');
+
+

执行这些代码之后,s将会包含“lol”。

+

使用 + 运算符或format!宏级联字符串

+

通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用+运算符:

+
let s1 = String::from("Hello, ");
+let s2 = String::from("world!");
+let s3 = s1 + &s2; // Note that s1 has been moved here and can no longer be used
+
+

执行完这些代码之后字符串s3将会包含Hello, world!s1在相加后不再有效的原因,和使用s2的引用的原因与使用+运算符时调用的方法签名有关,这个函数签名看起来像这样:

+
fn add(self, s: &str) -> String {
+
+

这并不是标准库中实际的签名;那个add使用泛型定义。这里的签名使用具体类型代替了泛型,这也正是当使用String值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解+运算那奇怪的部分的线索。

+

首先,s2使用了&,意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为add函数的s参数:只能将&strString相加,不能将两个String值相加。回忆之前第四章我们讲到&String是如何被强转为&str的:写成&s2的话String将会被强转成一个合适的类型&str。又因为方法没有获取参数的所有权,所以s2在这个操作后仍然有效。

+

其次,可以发现签名中add获取了self的所有权,因为self没有使用&。这意味着上面例子中的s1的所有权将被移动到add调用中,之后就不再有效。所以虽然let s3 = s1 + &s2;看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取s1的所有权,附加上从s2中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。

+

如果想要级联多个字符串,+的行为就显得笨重了:

+
let s1 = String::from("tic");
+let s2 = String::from("tac");
+let s3 = String::from("toe");
+
+let s = s1 + "-" + &s2 + "-" + &s3;
+
+

这时s的内容会是“tic-tac-toe”。在有这么多+"字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用format!宏:

+
let s1 = String::from("tic");
+let s2 = String::from("tac");
+let s3 = String::from("toe");
+
+let s = format!("{}-{}-{}", s1, s2, s3);
+
+

这些代码也会将s设置为“tic-tac-toe”。format!println!的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果的String。这个版本就好理解的多,并且不会获取任何参数的所有权。

+

索引字符串

+

在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果我们尝试使用索引语法访问String的一部分,会出现一个错误。比如如下代码:

+
let s1 = String::from("hello");
+let h = s1[0];
+
+

会导致如下错误:

+
error: the trait bound `std::string::String: std::ops::Index<_>` is not
+satisfied [--explain E0277]
+  |>
+  |>     let h = s1[0];
+  |>             ^^^^^
+note: the type `std::string::String` cannot be indexed by `_`
+
+

错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 如何在内存中储存字符串。

+

内部表示

+

String是一个Vec<u8>的封装。让我们看看之前一些正确编码的字符串的例子。首先是这一个:

+
let len = String::from("Hola").len();
+
+

在这里,len的值是四,这意味着储存字符串“Hola”的Vec的长度是四个字节:每一个字符的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?

+
let len = String::from("Здравствуйте").len();
+
+

当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。

+

作为演示,考虑如下无效的 Rust 代码:

+
let hello = "Здравствуйте";
+let answer = &hello[0];
+
+

answer的值应该是什么呢?它应该是第一个字符З吗?当使用 UTF-8 编码时,З的第一个字节是208,第二个是151,所以answer实际上应该是208,不过208自身并不是一个有效的字母。返回208可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引零位置所能提供的唯一数据。返回字节值可能不是人们希望看到的,即便是只有拉丁字母时:&"hello"[0]会返回104而不是h。为了避免返回意想不到值并造成不能立刻发现的 bug。Rust 选择不编译这些代码并及早杜绝了误会的放生。

+

字节、标量值和字形簇!天呐!

+

这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中字母的概念)。

+

比如这个用梵文书写的印度语单词“नमस्ते”,最终它储存在Vec中的u8值看起来像这样:

+
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
+224, 165, 135]
+
+

这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解他们,也就像 Rust 的char类型那样,这些字节看起来像这样:

+
['न', 'म', 'स', '्', 'त', 'े']
+
+

这里有六个char,不过第四个和第六个都不是字母,他们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:

+
["न", "म", "स्", "ते"]
+
+

Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。

+

最后一个 Rust 不允许使用索引获取String字符的原因是索引操作预期总是需要常数时间 (O(1))。但是对于String不可能保证这样的性能,因为 Rust 不得不检查从字符串的开头到索引位置的内容来确定这里有多少有效的字符。

+

字符串 slice

+

因为字符串索引应该返回的类型是不明确的,而且索引字符串通常也是一个坏点子,所以 Rust 不建议这么做,而如果你确实需要它的话则需要更加明确一些。比使用[]和单个值的索引更加明确的方式是使用[]和一个 range 来创建包含特定字节的字符串 slice:

+
let hello = "Здравствуйте";
+
+let s = &hello[0..4];
+
+

这里,s是一个&str,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着s将会是“Зд”。

+

那么如果获取&hello[0..1]会发生什么呢?回答是:在运行时会 panic,就跟访问 vector 中的无效索引时一样:

+
thread 'main' panicked at 'index 0 and/or 1 in `Здравствуйте` do not lie on
+character boundary', ../src/libcore/str/mod.rs:1694
+
+

你应该小心谨慎的使用这个操作,因为它可能会使你的程序崩溃。

+

遍历字符串的方法

+

幸运的是,这里还有其他获取字符串元素的方式。

+

如果你需要操作单独的 Unicode 标量值,最好的选择是使用chars方法。堆“नमस्ते”调用chars方法会将其分开并返回六个char类型的值,接着就可以遍历结果来访问每一个元素了:

+
for c in "नमस्ते".chars() {
+    println!("{}", c);
+}
+
+

这些代码会打印出如下内容:

+
न
+म
+स
+्
+त
+े
+
+

bytes方法返回每一个原始字节,这可能会适合你的使用场景:

+
for b in "नमस्ते".bytes() {
+    println!("{}", b);
+}
+
+

这些代码会打印出组成String的 18 个字节,开头是这样的:

+
224
+164
+168
+224
+// ... etc
+
+

不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。

+

从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。

+

字符串并不简单

+

总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理String数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何在前台处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期中免于处理涉及非 ASCII 字符的错误。

+

现在让我们转向一些不太复杂的集合:哈希 map!

+
diff --git a/docs/ch08-03-hash-maps.html b/docs/ch08-03-hash-maps.html index ba7e3e0..34d50c6 100644 --- a/docs/ch08-03-hash-maps.html +++ b/docs/ch08-03-hash-maps.html @@ -67,7 +67,133 @@
- +

哈希 map

+
+

ch08-03-hash-maps.md +
+commit 0d229cc5a3da341196e15a6761735b2952281569

+
+

最后要介绍的常用集合类型是哈希 maphash map)。HashMap<K, V>类型储存了一个键类型K对应一个值类型V的映射。它通过一个哈希函数hashing function)来实现映射,它决定了如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。

+

哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。

+

本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中的HashMap定义的函数中。请一如既往地查看标准库文档来了解更多信息。

+

新建一个哈希 map

+

可以使用new创建一个空的HashMap,并使用insert来增加元素。这里我们记录两支队伍的分数,分别是蓝队和黄队。蓝队开始有 10 分而黄队开始有 50 分:

+
use std::collections::HashMap;
+
+let mut scores = HashMap::new();
+
+scores.insert(String::from("Blue"), 10);
+scores.insert(String::from("Yellow"), 50);
+
+

注意必须首先use标准库中集合部分的HashMap。在这三个常用集合中,这个是最不常用的,所以并不包含在被 prelude 自动引用的功能中。标准库中对哈希 map 的支持也相对较少;例如,并没有内建的用于构建的宏。

+

就像 vector 一样,哈希 map 将他们的数据储存在堆上。这个HashMap的键类型是String而值类型是i32。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。

+

另一个构建哈希 map 的方法是使用一个元组的 vector 的collect方法,其中每个元组包含一个键值对。collect方法可以将数据收集进一系列的集合类型,包括HashMap。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用zip方法来创建一个元组的 vector,其中“Blue”与 10 是一对,依此类推。接着就可以使用collect方法将这个元组 vector 转换成一个HashMap

+
use std::collections::HashMap;
+
+let teams  = vec![String::from("Blue"), String::from("Yellow")];
+let initial_scores = vec![10, 50];
+
+let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
+
+

这里HashMap<_, _>类型注解是必要的,因为可能collect进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的参数来说,可以使用下划线而 Rust 可以根据 vector 中数据的类型推断出哈希 map 所包含的类型。

+

哈希 map 和所有权

+

对于像i32这样的实现了Copy trait 的类型,其值可以拷贝进哈希 map。对于像String这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者:

+
use std::collections::HashMap;
+
+let field_name = String::from("Favorite color");
+let field_value = String::from("Blue");
+
+let mut map = HashMap::new();
+map.insert(field_name, field_value);
+// field_name and field_value are invalid at this point
+
+

insert调用将field_namefield_value移动到哈希 map 中后,将不能使用这两个绑定。

+

如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第十章生命周期部分将会更多的讨论这个问题。

+

访问哈希 map 中的值

+

可以通过get方法并提供对应的键来从哈希 map 中获取值:

+
use std::collections::HashMap;
+
+let mut scores = HashMap::new();
+
+scores.insert(String::from("Blue"), 10);
+scores.insert(String::from("Yellow"), 50);
+
+let team_name = String::from("Blue");
+let score = scores.get(&team_name);
+
+

这里,score将会是与蓝队分数相关的值,而这个值将是Some(10)。因为get返回Option<V>所以结果被封装进Some;如果某个键在哈希 map 中没有对应的值,get会返回None。程序将需要采用第六章提到的方法中之一来处理Option

+

可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是for循环:

+
use std::collections::HashMap;
+
+let mut scores = HashMap::new();
+
+scores.insert(String::from("Blue"), 10);
+scores.insert(String::from("Yellow"), 50);
+
+for (key, value) in &scores {
+    println!("{}: {}", key, value);
+}
+
+

这会以任意顺序打印出每一个键值对:

+
Yellow: 50
+Blue: 10
+
+

更新哈希 map

+

虽然键值对的数量是可以增长的,不过每个单独的键同时只能关联一个值。当你想要改变哈希 map 中的数据时,必须选择是用新值替代旧值,还是完全无视旧值。我们也可以选择保留旧值而忽略新值,并只在键没有对应一个值时增加新值。或者可以结合新值和旧值。让我们看看着每一种方式是如何工作的!

+

覆盖一个值

+

如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次insert,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值:

+
use std::collections::HashMap;
+
+let mut scores = HashMap::new();
+
+scores.insert(String::from("Blue"), 10);
+scores.insert(String::from("Blue"), 25);
+
+println!("{:?}", scores);
+
+

这会打印出{"Blue": 25}。原始的值 10 将被覆盖。

+

只在键没有对应值时插入

+

我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API,叫做entry,它获取我们想要检查的键作为参数。entry函数的返回值是一个枚举,Entry,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。使用 entry API 的代码看起来像这样:

+
use std::collections::HashMap;
+
+let mut scores = HashMap::new();
+scores.insert(String::from("Blue"), 10);
+
+scores.entry(String::from("Yellow")).or_insert(50);
+scores.entry(String::from("Blue")).or_insert(50);
+
+println!("{:?}", scores);
+
+

Entryor_insert方法在键对应的值存在时就返回这个值的Entry,如果不存在则将参数作为新值插入并返回修改过的Entry。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。

+

这段代码会打印出{"Yellow": 50, "Blue": 10}。第一个entry调用会插入黄队的键和值 50,因为黄队并没有一个值。第二个entry调用不会改变哈希 map 因为蓝队已经有了值 10。

+

根据旧值更新一个值

+

另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,如果我们想要计数一些文本中每一个单词分别出现了多少次,就可以使用哈希 map,以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值0

+
use std::collections::HashMap;
+
+let text = "hello world wonderful world";
+
+let mut map = HashMap::new();
+
+for word in text.split_whitespace() {
+    let count = map.entry(word).or_insert(0);
+    *count += 1;
+}
+
+println!("{:?}", map);
+
+

这会打印出{"world": 2, "hello": 1, "wonderful": 1}or_insert方法事实上会返回这个键的值的一个可变引用(&mut V)。这里我们将这个可变引用储存在count变量中,所以为了赋值必须首先使用星号(*)解引用count。这个可变引用在for循环的结尾离开作用域,这样所有这些改变都是安全的并被借用规则所允许。

+

哈希函数

+

HashMap默认使用一个密码学上是安全的哈希函数,它可以提供抵抗拒绝服务(Denial of Service, DoS)攻击的能力。这并不是现有最快的哈希函数,不过为了更好的安全性带来一些性能下降也是值得的。如果你监控你的代码并发现默认哈希函数对你来说非常慢,可以通过指定一个不同的 hasher 来切换为另一个函数。hasher 是一个实现了BuildHasher trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。

+

总结

+

vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题:

+ +

标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!

+

我们已经开始解除可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!

+
diff --git a/docs/print.html b/docs/print.html index efb9feb..b8dcacb 100644 --- a/docs/print.html +++ b/docs/print.html @@ -3716,6 +3716,399 @@ commit 0d229cc5a3da341196e15a6761735b2952281569

对于标准库提供的其他类型的集合,请查看文档

我们将讨论如何创建和更新 vector、字符串和哈希 map,以及他们何以如此特殊。

+

vector

+
+

ch08-01-vectors.md +
+commit 0d229cc5a3da341196e15a6761735b2952281569

+
+

我们要讲到的第一个类型是Vec<T>,也被称为 vector。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。

+

新建 vector

+

为了创建一个新的,空的 vector,可以调用Vec::new函数:

+
let v: Vec<i32> = Vec::new();
+
+

注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是同质的(homogeneous):他们可以储存很多值,不过这些值必须都是相同类型的。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用他们。现在,所有你需要知道的就是Vec是一个由标准库提供的类型,它可以存放任何类型,而当Vec存放某个特定类型时,那个类型位于尖括号中。这里我们告诉 Rust v这个Vec将存放i32类型的元素。

+

在实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用初始值来创建一个Vec,而且为了方便 Rust 提供了vec!宏。这个宏会根据我们提供的值来创建一个新的Vec。如下代码会新建一个拥有值123Vec<i32>

+
let v = vec![1, 2, 3];
+
+

因为我们提供了i32类型的初始值,Rust 可以推断出v的类型是Vec<i32>,因此类型注解就不是必须的。接下来让我们看看如何修改一个 vector。

+

更新 vector

+

对于新建一个 vector 并向其增加元素,可以使用push方法:

+
let mut v = Vec::new();
+
+v.push(5);
+v.push(6);
+v.push(7);
+v.push(8);
+
+

如第三章中讨论的任何变量一样,如果想要能够改变它的值,必须使用mut关键字使其可变。放入其中的所有值都是i32类型的,而且 Rust 也根据数据如此判断,所以不需要Vec<i32>注解。

+

丢弃 vector 时也会丢弃其所有元素

+

类似于任何其他的struct,vector 在其离开作用域时会被释放:

+
{
+    let v = vec![1, 2, 3, 4];
+
+    // do stuff with v
+
+} // <- v goes out of scope and is freed here
+
+

当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。这可能看起来非常直观,不过一旦开始使用 vector 元素的引用情况就变得有些复杂了。下面让我们处理这种情况!

+

读取 vector 的元素

+

现在你知道如何创建、更新和销毁 vector 了,接下来的一步最好了解一下如何读取他们的内容。有两种方法引用 vector 中储存的值。为了更加清楚的说明这个例子,我们标注这些函数返回的值的类型。

+

这个例子展示了访问 vector 中一个值的两种方式,索引语法或者get方法:

+
let v = vec![1, 2, 3, 4, 5];
+
+let third: &i32 = &v[2];
+let third: Option<&i32> = v.get(2);
+
+

这里有一些需要注意的地方。首先,我们使用索引值2来获取第三个元素,索引是从 0 开始的。其次,这两个不同的获取第三个元素的方式分别为:使用&[]返回一个引用;或者使用get方法以索引作为参数来返回一个Option<&T>

+

Rust 有两个引用元素的方法的原因是程序可以选择如何处理当索引值在 vector 中没有对应值的情况。例如如下情况,如果有一个有五个元素的 vector 接着尝试访问索引为 100 的元素,程序该如何处理:

+
let v = vec![1, 2, 3, 4, 5];
+
+let does_not_exist = &v[100];
+let does_not_exist = v.get(100);
+
+

当运行这段代码,你会发现对于第一个[]方法,当引用一个不存在的元素时 Rust 会造成panic!。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。

+

get方法被传递了一个数组外的索引时,它不会 panic 而是返回None。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理Some(&element)None的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果他们不慎输入了一个过大的数字那么程序就会得到None值,你可以告诉用户Vec当前元素的数量并再请求他们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多!

+

无效引用

+

一旦程序获取了一个有效的引用,借用检查器将会执行第四章讲到的所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于这个例子,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候:

+
let mut v = vec![1, 2, 3, 4, 5];
+
+let first = &v[0];
+
+v.push(6);
+
+

编译会给出这个错误:

+
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
+immutable
+  |
+4 | let first = &v[0];
+  |              - immutable borrow occurs here
+5 |
+6 | v.push(6);
+  | ^ mutable borrow occurs here
+7 | }
+  | - immutable borrow ends here
+
+

这些代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式。在 vector 的结尾增加新元素是,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。

+
+

注意:关于更多内容,查看 Nomicon https://doc.rust-lang.org/stable/nomicon/vec.html

+
+

使用枚举来储存多种类型

+

在本章的开始,我们提到 vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!

+

例如,假如我们想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串。我们可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型,那个枚举的类型。接着可以创建一个储存枚举值的 vector,这样最终就能够储存不同类型的值了:

+
enum SpreadsheetCell {
+    Int(i32),
+    Float(f64),
+    Text(String),
+}
+
+let row = vec![
+    SpreadsheetCell::Int(3),
+    SpreadsheetCell::Text(String::from("blue")),
+    SpreadsheetCell::Float(10.12),
+];
+
+

Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个优点是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加match意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。

+

如果在编写程序时不能确切无遗的知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。

+

现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中Vec定义的很多其他实用方法的 API 文档。例如,除了push之外还有一个pop方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:String

+

字符串

+
+

ch08-02-strings.md +
+commit 4dc0001ccba4189e210ba47d6fe6c3c5fa729da6

+
+

第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解一下它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。

+

字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到String那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论String于其他集合不一样的地方,例如索引String是很复杂的,由于人和计算机理解String数据的不同方式。

+

什么是字符串?

+

在开始深入这些方面之前,我们需要讨论一下术语字符串的具体意义。Rust 的核心语言中事实上就只有一种字符串类型:str,字符串 slice,它通常以被借用的形式出现,&str。第四章讲到了字符串 slice:他们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。

+

称作String的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的“字符串”时,他们通常指的是String和字符串 slice &str类型,而不是其中一个。这一部分大部分是关于String的,不过这些类型在 Rust 标准库中都被广泛使用。String和字符串 slice 都是 UTF-8 编码的。

+

Rust 标准库中还包含一系列其他字符串类型,比如OsStringOsStrCStringCStr。相关库 crate 甚至会提供更多储存字符串数据的选择。与*String/*Str的命名类似,他们通常也提供有所有权和可借用的变体,就比如说String/&str。这些字符串类型在储存的编码或内存表现形式上可能有所不同。本章将不会讨论其他这些字符串类型;查看 API 文档来更多的了解如何使用他们以及各自适合的场景。

+

新建字符串

+

很多Vec可用的操作在String中同样可用,从以new函数创建字符串开始,像这样:

+
let s = String::new();
+
+

这新建了一个叫做s的空的字符串,接着我们可以向其中装载数据。

+

通常字符串会有初始数据因为我们希望一开始就有这个字符串。为此,使用to_string方法,它能用于任何实现了Display trait 的类型,对于字符串字面值是这样:

+
let data = "initial contents";
+
+let s = data.to_string();
+
+// the method also works on a literal directly:
+let s = "initial contents".to_string();
+
+

这会创建一个包好initial contents的字符串。

+

也可以使用String::from函数来从字符串字面值创建String。如下等同于使用to_string

+
let s = String::from("initial contents");
+
+

因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多于,不过都有其用武之地!在这个例子中,String::from.to_string最终做了完全相同的工作,所以如何选择就是风格问题了。

+

记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据:

+
let hello = "السلام عليكم";
+let hello = "Dobrý den";
+let hello = "Hello";
+let hello = "שָׁלוֹם";
+let hello = "नमस्ते";
+let hello = "こんにちは";
+let hello = "안녕하세요";
+let hello = "你好";
+let hello = "Olá";
+let hello = "Здравствуйте";
+let hello = "Hola";
+
+

更新字符串

+

String的大小可以增长其内容也可以改变,就像可以放入更多数据来改变Vec的内容一样。另外,String实现了+运算符作为级联运算符以便于使用。

+

附加字符串

+

可以通过push_str方法来附加字符串 slice,从而使String变长:

+
let mut s = String::from("foo");
+s.push_str("bar");
+
+

执行这两行代码之后s将会包含“foobar”。push_str方法获取字符串 slice,因为并不需要获取参数的所有权。例如,如果将s2的内容附加到s1中后自身不能被使用就糟糕了:

+
let mut s1 = String::from("foo");
+let s2 = String::from("bar");
+s1.push_str(&s2);
+
+

push方法被定义为获取一个单独的字符作为参数,并附加到String中:

+
let mut s = String::from("lo");
+s.push('l');
+
+

执行这些代码之后,s将会包含“lol”。

+

使用 + 运算符或format!宏级联字符串

+

通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用+运算符:

+
let s1 = String::from("Hello, ");
+let s2 = String::from("world!");
+let s3 = s1 + &s2; // Note that s1 has been moved here and can no longer be used
+
+

执行完这些代码之后字符串s3将会包含Hello, world!s1在相加后不再有效的原因,和使用s2的引用的原因与使用+运算符时调用的方法签名有关,这个函数签名看起来像这样:

+
fn add(self, s: &str) -> String {
+
+

这并不是标准库中实际的签名;那个add使用泛型定义。这里的签名使用具体类型代替了泛型,这也正是当使用String值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解+运算那奇怪的部分的线索。

+

首先,s2使用了&,意味着我们使用第二个字符串的引用与第一个字符串相加。这是因为add函数的s参数:只能将&strString相加,不能将两个String值相加。回忆之前第四章我们讲到&String是如何被强转为&str的:写成&s2的话String将会被强转成一个合适的类型&str。又因为方法没有获取参数的所有权,所以s2在这个操作后仍然有效。

+

其次,可以发现签名中add获取了self的所有权,因为self没有使用&。这意味着上面例子中的s1的所有权将被移动到add调用中,之后就不再有效。所以虽然let s3 = s1 + &s2;看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取s1的所有权,附加上从s2中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。

+

如果想要级联多个字符串,+的行为就显得笨重了:

+
let s1 = String::from("tic");
+let s2 = String::from("tac");
+let s3 = String::from("toe");
+
+let s = s1 + "-" + &s2 + "-" + &s3;
+
+

这时s的内容会是“tic-tac-toe”。在有这么多+"字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用format!宏:

+
let s1 = String::from("tic");
+let s2 = String::from("tac");
+let s3 = String::from("toe");
+
+let s = format!("{}-{}-{}", s1, s2, s3);
+
+

这些代码也会将s设置为“tic-tac-toe”。format!println!的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果的String。这个版本就好理解的多,并且不会获取任何参数的所有权。

+

索引字符串

+

在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果我们尝试使用索引语法访问String的一部分,会出现一个错误。比如如下代码:

+
let s1 = String::from("hello");
+let h = s1[0];
+
+

会导致如下错误:

+
error: the trait bound `std::string::String: std::ops::Index<_>` is not
+satisfied [--explain E0277]
+  |>
+  |>     let h = s1[0];
+  |>             ^^^^^
+note: the type `std::string::String` cannot be indexed by `_`
+
+

错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 如何在内存中储存字符串。

+

内部表示

+

String是一个Vec<u8>的封装。让我们看看之前一些正确编码的字符串的例子。首先是这一个:

+
let len = String::from("Hola").len();
+
+

在这里,len的值是四,这意味着储存字符串“Hola”的Vec的长度是四个字节:每一个字符的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢?

+
let len = String::from("Здравствуйте").len();
+
+

当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。

+

作为演示,考虑如下无效的 Rust 代码:

+
let hello = "Здравствуйте";
+let answer = &hello[0];
+
+

answer的值应该是什么呢?它应该是第一个字符З吗?当使用 UTF-8 编码时,З的第一个字节是208,第二个是151,所以answer实际上应该是208,不过208自身并不是一个有效的字母。返回208可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引零位置所能提供的唯一数据。返回字节值可能不是人们希望看到的,即便是只有拉丁字母时:&"hello"[0]会返回104而不是h。为了避免返回意想不到值并造成不能立刻发现的 bug。Rust 选择不编译这些代码并及早杜绝了误会的放生。

+

字节、标量值和字形簇!天呐!

+

这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中字母的概念)。

+

比如这个用梵文书写的印度语单词“नमस्ते”,最终它储存在Vec中的u8值看起来像这样:

+
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
+224, 165, 135]
+
+

这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解他们,也就像 Rust 的char类型那样,这些字节看起来像这样:

+
['न', 'म', 'स', '्', 'त', 'े']
+
+

这里有六个char,不过第四个和第六个都不是字母,他们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:

+
["न", "म", "स्", "ते"]
+
+

Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。

+

最后一个 Rust 不允许使用索引获取String字符的原因是索引操作预期总是需要常数时间 (O(1))。但是对于String不可能保证这样的性能,因为 Rust 不得不检查从字符串的开头到索引位置的内容来确定这里有多少有效的字符。

+

字符串 slice

+

因为字符串索引应该返回的类型是不明确的,而且索引字符串通常也是一个坏点子,所以 Rust 不建议这么做,而如果你确实需要它的话则需要更加明确一些。比使用[]和单个值的索引更加明确的方式是使用[]和一个 range 来创建包含特定字节的字符串 slice:

+
let hello = "Здравствуйте";
+
+let s = &hello[0..4];
+
+

这里,s是一个&str,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着s将会是“Зд”。

+

那么如果获取&hello[0..1]会发生什么呢?回答是:在运行时会 panic,就跟访问 vector 中的无效索引时一样:

+
thread 'main' panicked at 'index 0 and/or 1 in `Здравствуйте` do not lie on
+character boundary', ../src/libcore/str/mod.rs:1694
+
+

你应该小心谨慎的使用这个操作,因为它可能会使你的程序崩溃。

+

遍历字符串的方法

+

幸运的是,这里还有其他获取字符串元素的方式。

+

如果你需要操作单独的 Unicode 标量值,最好的选择是使用chars方法。堆“नमस्ते”调用chars方法会将其分开并返回六个char类型的值,接着就可以遍历结果来访问每一个元素了:

+
for c in "नमस्ते".chars() {
+    println!("{}", c);
+}
+
+

这些代码会打印出如下内容:

+
न
+म
+स
+्
+त
+े
+
+

bytes方法返回每一个原始字节,这可能会适合你的使用场景:

+
for b in "नमस्ते".bytes() {
+    println!("{}", b);
+}
+
+

这些代码会打印出组成String的 18 个字节,开头是这样的:

+
224
+164
+168
+224
+// ... etc
+
+

不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。

+

从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。

+

字符串并不简单

+

总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理String数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何在前台处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期中免于处理涉及非 ASCII 字符的错误。

+

现在让我们转向一些不太复杂的集合:哈希 map!

+

哈希 map

+
+

ch08-03-hash-maps.md +
+commit 0d229cc5a3da341196e15a6761735b2952281569

+
+

最后要介绍的常用集合类型是哈希 maphash map)。HashMap<K, V>类型储存了一个键类型K对应一个值类型V的映射。它通过一个哈希函数hashing function)来实现映射,它决定了如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。

+

哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。

+

本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中的HashMap定义的函数中。请一如既往地查看标准库文档来了解更多信息。

+

新建一个哈希 map

+

可以使用new创建一个空的HashMap,并使用insert来增加元素。这里我们记录两支队伍的分数,分别是蓝队和黄队。蓝队开始有 10 分而黄队开始有 50 分:

+
use std::collections::HashMap;
+
+let mut scores = HashMap::new();
+
+scores.insert(String::from("Blue"), 10);
+scores.insert(String::from("Yellow"), 50);
+
+

注意必须首先use标准库中集合部分的HashMap。在这三个常用集合中,这个是最不常用的,所以并不包含在被 prelude 自动引用的功能中。标准库中对哈希 map 的支持也相对较少;例如,并没有内建的用于构建的宏。

+

就像 vector 一样,哈希 map 将他们的数据储存在堆上。这个HashMap的键类型是String而值类型是i32。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。

+

另一个构建哈希 map 的方法是使用一个元组的 vector 的collect方法,其中每个元组包含一个键值对。collect方法可以将数据收集进一系列的集合类型,包括HashMap。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用zip方法来创建一个元组的 vector,其中“Blue”与 10 是一对,依此类推。接着就可以使用collect方法将这个元组 vector 转换成一个HashMap

+
use std::collections::HashMap;
+
+let teams  = vec![String::from("Blue"), String::from("Yellow")];
+let initial_scores = vec![10, 50];
+
+let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
+
+

这里HashMap<_, _>类型注解是必要的,因为可能collect进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的参数来说,可以使用下划线而 Rust 可以根据 vector 中数据的类型推断出哈希 map 所包含的类型。

+

哈希 map 和所有权

+

对于像i32这样的实现了Copy trait 的类型,其值可以拷贝进哈希 map。对于像String这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者:

+
use std::collections::HashMap;
+
+let field_name = String::from("Favorite color");
+let field_value = String::from("Blue");
+
+let mut map = HashMap::new();
+map.insert(field_name, field_value);
+// field_name and field_value are invalid at this point
+
+

insert调用将field_namefield_value移动到哈希 map 中后,将不能使用这两个绑定。

+

如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第十章生命周期部分将会更多的讨论这个问题。

+

访问哈希 map 中的值

+

可以通过get方法并提供对应的键来从哈希 map 中获取值:

+
use std::collections::HashMap;
+
+let mut scores = HashMap::new();
+
+scores.insert(String::from("Blue"), 10);
+scores.insert(String::from("Yellow"), 50);
+
+let team_name = String::from("Blue");
+let score = scores.get(&team_name);
+
+

这里,score将会是与蓝队分数相关的值,而这个值将是Some(10)。因为get返回Option<V>所以结果被封装进Some;如果某个键在哈希 map 中没有对应的值,get会返回None。程序将需要采用第六章提到的方法中之一来处理Option

+

可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是for循环:

+
use std::collections::HashMap;
+
+let mut scores = HashMap::new();
+
+scores.insert(String::from("Blue"), 10);
+scores.insert(String::from("Yellow"), 50);
+
+for (key, value) in &scores {
+    println!("{}: {}", key, value);
+}
+
+

这会以任意顺序打印出每一个键值对:

+
Yellow: 50
+Blue: 10
+
+

更新哈希 map

+

虽然键值对的数量是可以增长的,不过每个单独的键同时只能关联一个值。当你想要改变哈希 map 中的数据时,必须选择是用新值替代旧值,还是完全无视旧值。我们也可以选择保留旧值而忽略新值,并只在键没有对应一个值时增加新值。或者可以结合新值和旧值。让我们看看着每一种方式是如何工作的!

+

覆盖一个值

+

如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次insert,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值:

+
use std::collections::HashMap;
+
+let mut scores = HashMap::new();
+
+scores.insert(String::from("Blue"), 10);
+scores.insert(String::from("Blue"), 25);
+
+println!("{:?}", scores);
+
+

这会打印出{"Blue": 25}。原始的值 10 将被覆盖。

+

只在键没有对应值时插入

+

我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API,叫做entry,它获取我们想要检查的键作为参数。entry函数的返回值是一个枚举,Entry,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。使用 entry API 的代码看起来像这样:

+
use std::collections::HashMap;
+
+let mut scores = HashMap::new();
+scores.insert(String::from("Blue"), 10);
+
+scores.entry(String::from("Yellow")).or_insert(50);
+scores.entry(String::from("Blue")).or_insert(50);
+
+println!("{:?}", scores);
+
+

Entryor_insert方法在键对应的值存在时就返回这个值的Entry,如果不存在则将参数作为新值插入并返回修改过的Entry。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。

+

这段代码会打印出{"Yellow": 50, "Blue": 10}。第一个entry调用会插入黄队的键和值 50,因为黄队并没有一个值。第二个entry调用不会改变哈希 map 因为蓝队已经有了值 10。

+

根据旧值更新一个值

+

另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,如果我们想要计数一些文本中每一个单词分别出现了多少次,就可以使用哈希 map,以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值0

+
use std::collections::HashMap;
+
+let text = "hello world wonderful world";
+
+let mut map = HashMap::new();
+
+for word in text.split_whitespace() {
+    let count = map.entry(word).or_insert(0);
+    *count += 1;
+}
+
+println!("{:?}", map);
+
+

这会打印出{"world": 2, "hello": 1, "wonderful": 1}or_insert方法事实上会返回这个键的值的一个可变引用(&mut V)。这里我们将这个可变引用储存在count变量中,所以为了赋值必须首先使用星号(*)解引用count。这个可变引用在for循环的结尾离开作用域,这样所有这些改变都是安全的并被借用规则所允许。

+

哈希函数

+

HashMap默认使用一个密码学上是安全的哈希函数,它可以提供抵抗拒绝服务(Denial of Service, DoS)攻击的能力。这并不是现有最快的哈希函数,不过为了更好的安全性带来一些性能下降也是值得的。如果你监控你的代码并发现默认哈希函数对你来说非常慢,可以通过指定一个不同的 hasher 来切换为另一个函数。hasher 是一个实现了BuildHasher trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。

+

总结

+

vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题:

+ +

标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习!

+

我们已经开始解除可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机!

diff --git a/src/ch08-01-vectors.md b/src/ch08-01-vectors.md index e69de29..f61f186 100644 --- a/src/ch08-01-vectors.md +++ b/src/ch08-01-vectors.md @@ -0,0 +1,140 @@ +## vector + +> [ch08-01-vectors.md](https://github.com/rust-lang/book/blob/master/src/ch08-01-vectors.md) +>
+> commit 0d229cc5a3da341196e15a6761735b2952281569 + +我们要讲到的第一个类型是`Vec`,也被称为 *vector*。vector 允许我们在一个单独的数据结构中储存多于一个值,它在内存中彼此相邻的排列所有的值。vector 只能储存相同类型的值。他们在拥有一系列的场景下非常实用,例如文件中的文本行或是购物车中商品的价格。 + +### 新建 vector + +为了创建一个新的,空的 vector,可以调用`Vec::new`函数: + +```rust +let v: Vec = Vec::new(); +``` + +注意这里我们增加了一个类型注解。因为没有向这个 vector 中插入任何值,Rust 并不知道我们想要储存什么类型的元素。这是一个非常重要的点。vector 是同质的(homogeneous):他们可以储存很多值,不过这些值必须都是相同类型的。vector 是用泛型实现的,第十章会涉及到如何对你自己的类型使用他们。现在,所有你需要知道的就是`Vec`是一个由标准库提供的类型,它可以存放任何类型,而当`Vec`存放某个特定类型时,那个类型位于尖括号中。这里我们告诉 Rust `v`这个`Vec`将存放`i32`类型的元素。 + +在实际的代码中,一旦插入值 Rust 就可以推断出想要存放的类型,所以你很少会需要这些类型注解。更常见的做法是使用初始值来创建一个`Vec`,而且为了方便 Rust 提供了`vec!`宏。这个宏会根据我们提供的值来创建一个新的`Vec`。如下代码会新建一个拥有值`1`、`2`和`3`的`Vec`: + +```rust +let v = vec![1, 2, 3]; +``` + +因为我们提供了`i32`类型的初始值,Rust 可以推断出`v`的类型是`Vec`,因此类型注解就不是必须的。接下来让我们看看如何修改一个 vector。 + +### 更新 vector + +对于新建一个 vector 并向其增加元素,可以使用`push`方法: + +```rust +let mut v = Vec::new(); + +v.push(5); +v.push(6); +v.push(7); +v.push(8); +``` + +如第三章中讨论的任何变量一样,如果想要能够改变它的值,必须使用`mut`关键字使其可变。放入其中的所有值都是`i32`类型的,而且 Rust 也根据数据如此判断,所以不需要`Vec`注解。 + +### 丢弃 vector 时也会丢弃其所有元素 + +类似于任何其他的`struct`,vector 在其离开作用域时会被释放: + +```rust +{ + let v = vec![1, 2, 3, 4]; + + // do stuff with v + +} // <- v goes out of scope and is freed here +``` + +当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。这可能看起来非常直观,不过一旦开始使用 vector 元素的引用情况就变得有些复杂了。下面让我们处理这种情况! + +### 读取 vector 的元素 + +现在你知道如何创建、更新和销毁 vector 了,接下来的一步最好了解一下如何读取他们的内容。有两种方法引用 vector 中储存的值。为了更加清楚的说明这个例子,我们标注这些函数返回的值的类型。 + +这个例子展示了访问 vector 中一个值的两种方式,索引语法或者`get`方法: + +```rust +let v = vec![1, 2, 3, 4, 5]; + +let third: &i32 = &v[2]; +let third: Option<&i32> = v.get(2); +``` + +这里有一些需要注意的地方。首先,我们使用索引值`2`来获取第三个元素,索引是从 0 开始的。其次,这两个不同的获取第三个元素的方式分别为:使用`&`和`[]`返回一个引用;或者使用`get`方法以索引作为参数来返回一个`Option<&T>`。 + +Rust 有两个引用元素的方法的原因是程序可以选择如何处理当索引值在 vector 中没有对应值的情况。例如如下情况,如果有一个有五个元素的 vector 接着尝试访问索引为 100 的元素,程序该如何处理: + +```rust,should_panic +let v = vec![1, 2, 3, 4, 5]; + +let does_not_exist = &v[100]; +let does_not_exist = v.get(100); +``` + +当运行这段代码,你会发现对于第一个`[]`方法,当引用一个不存在的元素时 Rust 会造成`panic!`。这个方法更适合当程序认为尝试访问超过 vector 结尾的元素是一个严重错误的情况,这时应该使程序崩溃。 + +当`get`方法被传递了一个数组外的索引时,它不会 panic 而是返回`None`。当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它。接着你的代码可以有处理`Some(&element)`或`None`的逻辑,如第六章讨论的那样。例如,索引可能来源于用户输入的数字。如果他们不慎输入了一个过大的数字那么程序就会得到`None`值,你可以告诉用户`Vec`当前元素的数量并再请求他们输入一个有效的值。这就比因为输入错误而使程序崩溃要友好的多! + +#### 无效引用 + +一旦程序获取了一个有效的引用,借用检查器将会执行第四章讲到的所有权和借用规则来确保 vector 内容的这个引用和任何其他引用保持有效。回忆一下不能在相同作用域中同时存在可变和不可变引用的规则。这个规则适用于这个例子,当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候: + +```rust,ignore +let mut v = vec![1, 2, 3, 4, 5]; + +let first = &v[0]; + +v.push(6); +``` + +编译会给出这个错误: + +``` +error[E0502]: cannot borrow `v` as mutable because it is also borrowed as +immutable + | +4 | let first = &v[0]; + | - immutable borrow occurs here +5 | +6 | v.push(6); + | ^ mutable borrow occurs here +7 | } + | - immutable borrow ends here +``` + +这些代码看起来应该能够运行:为什么第一个元素的引用会关心 vector 结尾的变化?不能这么做的原因是由于 vector 的工作方式。在 vector 的结尾增加新元素是,在没有足够空间将所有所有元素依次相邻存放的情况下,可能会要求分配新内存并将老的元素拷贝到新的空间中。这时,第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。 + +> 注意:关于更多内容,查看 Nomicon *https://doc.rust-lang.org/stable/nomicon/vec.html* + +### 使用枚举来储存多种类型 + +在本章的开始,我们提到 vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举! + +例如,假如我们想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串。我们可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型,那个枚举的类型。接着可以创建一个储存枚举值的 vector,这样最终就能够储存不同类型的值了: + +```rust +enum SpreadsheetCell { + Int(i32), + Float(f64), + Text(String), +} + +let row = vec![ + SpreadsheetCell::Int(3), + SpreadsheetCell::Text(String::from("blue")), + SpreadsheetCell::Float(10.12), +]; +``` + +Rust 在编译时就必须准确的知道 vector 中类型的原因是它需要知道储存每个元素到底需要多少内存。第二个优点是可以准确的知道这个 vector 中允许什么类型。如果 Rust 允许 vector 存放任意类型,那么当对 vector 元素执行操作时一个或多个类型的值就有可能会造成错误。使用枚举外加`match`意味着 Rust 能在编译时就保证总是会处理所有可能的情况,正如第六章讲到的那样。 + +如果在编写程序时不能确切无遗的知道运行时会储存进 vector 的所有类型,枚举技术就行不通了。相反,你可以使用 trait 对象,第十七章会讲到它。 + +现在我们了解了一些使用 vector 的最常见的方式,请一定去看看标准库中`Vec`定义的很多其他实用方法的 API 文档。例如,除了`push`之外还有一个`pop`方法,它会移除并返回 vector 的最后一个元素。让我们继续下一个集合类型:`String`! \ No newline at end of file diff --git a/src/ch08-02-strings.md b/src/ch08-02-strings.md index e69de29..222f9d2 100644 --- a/src/ch08-02-strings.md +++ b/src/ch08-02-strings.md @@ -0,0 +1,286 @@ +## 字符串 + +> [ch08-02-strings.md](https://github.com/rust-lang/book/blob/master/src/ch08-02-strings.md) +>
+> commit 4dc0001ccba4189e210ba47d6fe6c3c5fa729da6 + +第四章已经讲过一些字符串的内容,不过现在让我们更深入地了解一下它。字符串是新晋 Rustacean 们通常会被困住的领域。这是由于三方面内容的结合:Rust 倾向于确保暴露出可能的错误,字符串是比很多程序员所想象的要更为复杂的数据结构,以及 UTF-8。所有这些结合起来对于来自其他语言背景的程序员就可能显得很困难了。 + +字符串出现在集合章节的原因是,字符串是作为字节的集合外加一些方法实现的,当这些字节被解释为文本时,这些方法提供了实用的功能。在这一部分,我们会讲到`String`那些任何集合类型都有的操作,比如创建、更新和读取。也会讨论`String`于其他集合不一样的地方,例如索引`String`是很复杂的,由于人和计算机理解`String`数据的不同方式。 + +### 什么是字符串? + +在开始深入这些方面之前,我们需要讨论一下术语**字符串**的具体意义。Rust 的核心语言中事实上就只有一种字符串类型:`str`,字符串 slice,它通常以被借用的形式出现,`&str`。第四章讲到了**字符串 slice**:他们是一些储存在别处的 UTF-8 编码字符串数据的引用。比如字符串字面值被储存在程序的二进制输出中,字符串 slice 也是如此。 + +称作`String`的类型是由标准库提供的,而没有写进核心语言部分,它是可增长的、可变的、有所有权的、UTF-8 编码的字符串类型。当 Rustacean 们谈到 Rust 的“字符串”时,他们通常指的是`String`和字符串 slice `&str`类型,而不是其中一个。这一部分大部分是关于`String`的,不过这些类型在 Rust 标准库中都被广泛使用。`String`和字符串 slice 都是 UTF-8 编码的。 + +Rust 标准库中还包含一系列其他字符串类型,比如`OsString`、`OsStr`、`CString`和`CStr`。相关库 crate 甚至会提供更多储存字符串数据的选择。与`*String`/`*Str`的命名类似,他们通常也提供有所有权和可借用的变体,就比如说`String`/`&str`。这些字符串类型在储存的编码或内存表现形式上可能有所不同。本章将不会讨论其他这些字符串类型;查看 API 文档来更多的了解如何使用他们以及各自适合的场景。 + +### 新建字符串 + +很多`Vec`可用的操作在`String`中同样可用,从以`new`函数创建字符串开始,像这样: + +```rust +let s = String::new(); +``` + +这新建了一个叫做`s`的空的字符串,接着我们可以向其中装载数据。 + +通常字符串会有初始数据因为我们希望一开始就有这个字符串。为此,使用`to_string`方法,它能用于任何实现了`Display` trait 的类型,对于字符串字面值是这样: + +```rust +let data = "initial contents"; + +let s = data.to_string(); + +// the method also works on a literal directly: +let s = "initial contents".to_string(); +``` + +这会创建一个包好`initial contents`的字符串。 + +也可以使用`String::from`函数来从字符串字面值创建`String`。如下等同于使用`to_string`: + +```rust +let s = String::from("initial contents"); +``` + +因为字符串使用广泛,这里有很多不同的用于字符串的通用 API 可供选择。他们有些可能显得有些多于,不过都有其用武之地!在这个例子中,`String::from`和`.to_string`最终做了完全相同的工作,所以如何选择就是风格问题了。 + +记住字符串是 UTF-8 编码的,所以可以包含任何可以正确编码的数据: + +```rust +let hello = "السلام عليكم"; +let hello = "Dobrý den"; +let hello = "Hello"; +let hello = "שָׁלוֹם"; +let hello = "नमस्ते"; +let hello = "こんにちは"; +let hello = "안녕하세요"; +let hello = "你好"; +let hello = "Olá"; +let hello = "Здравствуйте"; +let hello = "Hola"; +``` + +### 更新字符串 + +`String`的大小可以增长其内容也可以改变,就像可以放入更多数据来改变`Vec`的内容一样。另外,`String`实现了`+`运算符作为级联运算符以便于使用。 + +#### 附加字符串 + +可以通过`push_str`方法来附加字符串 slice,从而使`String`变长: + +```rust +let mut s = String::from("foo"); +s.push_str("bar"); +``` + +执行这两行代码之后`s`将会包含“foobar”。`push_str`方法获取字符串 slice,因为并不需要获取参数的所有权。例如,如果将`s2`的内容附加到`s1`中后自身不能被使用就糟糕了: + +```rust +let mut s1 = String::from("foo"); +let s2 = String::from("bar"); +s1.push_str(&s2); +``` + +`push`方法被定义为获取一个单独的字符作为参数,并附加到`String`中: + +```rust +let mut s = String::from("lo"); +s.push('l'); +``` + +执行这些代码之后,`s`将会包含“lol”。 + +#### 使用 + 运算符或`format!`宏级联字符串 + +通常我们希望将两个已知的字符串合并在一起。一种办法是像这样使用`+`运算符: + +```rust +let s1 = String::from("Hello, "); +let s2 = String::from("world!"); +let s3 = s1 + &s2; // Note that s1 has been moved here and can no longer be used +``` + +执行完这些代码之后字符串`s3`将会包含`Hello, world!`。`s1`在相加后不再有效的原因,和使用`s2`的引用的原因与使用`+`运算符时调用的方法签名有关,这个函数签名看起来像这样: + +```rust,ignore +fn add(self, s: &str) -> String { +``` + +这并不是标准库中实际的签名;那个`add`使用泛型定义。这里的签名使用具体类型代替了泛型,这也正是当使用`String`值调用这个方法会发生的。第十章会讨论泛型。这个签名提供了理解`+`运算那奇怪的部分的线索。 + +首先,`s2`使用了`&`,意味着我们使用第二个字符串的**引用**与第一个字符串相加。这是因为`add`函数的`s`参数:只能将`&str`和`String`相加,不能将两个`String`值相加。回忆之前第四章我们讲到`&String`是如何被强转为`&str`的:写成`&s2`的话`String`将会被强转成一个合适的类型`&str`。又因为方法没有获取参数的所有权,所以`s2`在这个操作后仍然有效。 + +其次,可以发现签名中`add`获取了`self`的所有权,因为`self`**没有**使用`&`。这意味着上面例子中的`s1`的所有权将被移动到`add`调用中,之后就不再有效。所以虽然`let s3 = s1 + &s2;`看起来就像它会复制两个字符串并创建一个新的字符串,而实际上这个语句会获取`s1`的所有权,附加上从`s2`中拷贝的内容,并返回结果的所有权。换句话说,它看起来好像生成了很多拷贝不过实际上并没有:这个实现比拷贝要更高效。 + +如果想要级联多个字符串,`+`的行为就显得笨重了: + +```rust +let s1 = String::from("tic"); +let s2 = String::from("tac"); +let s3 = String::from("toe"); + +let s = s1 + "-" + &s2 + "-" + &s3; +``` + +这时`s`的内容会是“tic-tac-toe”。在有这么多`+`和`"`字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用`format!`宏: + +```rust +let s1 = String::from("tic"); +let s2 = String::from("tac"); +let s3 = String::from("toe"); + +let s = format!("{}-{}-{}", s1, s2, s3); +``` + +这些代码也会将`s`设置为“tic-tac-toe”。`format!`与`println!`的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果的`String`。这个版本就好理解的多,并且不会获取任何参数的所有权。 + +### 索引字符串 + +在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在 Rust 中,如果我们尝试使用索引语法访问`String`的一部分,会出现一个错误。比如如下代码: + +```rust,ignore +let s1 = String::from("hello"); +let h = s1[0]; +``` + +会导致如下错误: + +``` +error: the trait bound `std::string::String: std::ops::Index<_>` is not +satisfied [--explain E0277] + |> + |> let h = s1[0]; + |> ^^^^^ +note: the type `std::string::String` cannot be indexed by `_` +``` + +错误和提示说明了全部问题:Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?为了回答这个问题,我们必须先聊一聊 Rust 如何在内存中储存字符串。 + +#### 内部表示 + +`String`是一个`Vec`的封装。让我们看看之前一些正确编码的字符串的例子。首先是这一个: + +```rust +let len = String::from("Hola").len(); +``` + +在这里,`len`的值是四,这意味着储存字符串“Hola”的`Vec`的长度是四个字节:每一个字符的 UTF-8 编码都占用一个字节。那下面这个例子又如何呢? + +```rust +let len = String::from("Здравствуйте").len(); +``` + +当问及这个字符是多长的时候有人可能会说是 12。然而,Rust 的回答是 24。这是使用 UTF-8 编码 “Здравствуйте” 所需要的字节数,这是因为每个 Unicode 标量值需要两个字节存储。因此一个字符串字节值的索引并不总是对应一个有效的 Unicode 标量值。 + +作为演示,考虑如下无效的 Rust 代码: + +```rust,ignore +let hello = "Здравствуйте"; +let answer = &hello[0]; +``` + +`answer`的值应该是什么呢?它应该是第一个字符`З`吗?当使用 UTF-8 编码时,`З`的第一个字节是`208`,第二个是`151`,所以`answer`实际上应该是`208`,不过`208`自身并不是一个有效的字母。返回`208`可不是一个请求字符串第一个字母的人所希望看到的,不过它是 Rust 在字节索引零位置所能提供的唯一数据。返回字节值可能不是人们希望看到的,即便是只有拉丁字母时:`&"hello"[0]`会返回`104`而不是`h`。为了避免返回意想不到值并造成不能立刻发现的 bug。Rust 选择不编译这些代码并及早杜绝了误会的放生。 + +#### 字节、标量值和字形簇!天呐! + +这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中**字母**的概念)。 + +比如这个用梵文书写的印度语单词“नमस्ते”,最终它储存在`Vec`中的`u8`值看起来像这样: + +``` +[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, +224, 165, 135] +``` + +这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode 标量值的角度理解他们,也就像 Rust 的`char`类型那样,这些字节看起来像这样: + +``` +['न', 'म', 'स', '्', 'त', 'े'] +``` + +这里有六个`char`,不过第四个和第六个都不是字母,他们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母: + +``` +["न", "म", "स्", "ते"] +``` + +Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式,而无所谓是何种人类语言。 + +最后一个 Rust 不允许使用索引获取`String`字符的原因是索引操作预期总是需要常数时间 (O(1))。但是对于`String`不可能保证这样的性能,因为 Rust 不得不检查从字符串的开头到索引位置的内容来确定这里有多少有效的字符。 + +### 字符串 slice + +因为字符串索引应该返回的类型是不明确的,而且索引字符串通常也是一个坏点子,所以 Rust 不建议这么做,而如果你确实需要它的话则需要更加明确一些。比使用`[]`和单个值的索引更加明确的方式是使用`[]`和一个 range 来创建包含特定字节的字符串 slice: + +```rust +let hello = "Здравствуйте"; + +let s = &hello[0..4]; +``` + +这里,`s`是一个`&str`,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着`s`将会是“Зд”。 + +那么如果获取`&hello[0..1]`会发生什么呢?回答是:在运行时会 panic,就跟访问 vector 中的无效索引时一样: + + +```text +thread 'main' panicked at 'index 0 and/or 1 in `Здравствуйте` do not lie on +character boundary', ../src/libcore/str/mod.rs:1694 +``` + +你应该小心谨慎的使用这个操作,因为它可能会使你的程序崩溃。 + +### 遍历字符串的方法 + +幸运的是,这里还有其他获取字符串元素的方式。 + +如果你需要操作单独的 Unicode 标量值,最好的选择是使用`chars`方法。堆“नमस्ते”调用`chars`方法会将其分开并返回六个`char`类型的值,接着就可以遍历结果来访问每一个元素了: + +```rust +for c in "नमस्ते".chars() { + println!("{}", c); +} +``` + +这些代码会打印出如下内容: + +``` +न +म +स +् +त +े +``` + +`bytes`方法返回每一个原始字节,这可能会适合你的使用场景: + + +```rust +for b in "नमस्ते".bytes() { + println!("{}", b); +} +``` + +这些代码会打印出组成`String`的 18 个字节,开头是这样的: + +``` +224 +164 +168 +224 +// ... etc +``` + +不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。 + +从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。 + +### 字符串并不简单 + +总而言之,字符串还是很复杂的。不同的语言选择了不同的向程序员展示其复杂性的方式。Rust 选择了以准确的方式处理`String`数据作为所有 Rust 程序的默认行为,这意味着程序员们必须更多的思考如何在前台处理 UTF-8 数据。这种权衡取舍相比其他语言更多的暴露出了字符串的复杂性,不过也使你在开发生命周期中免于处理涉及非 ASCII 字符的错误。 + +现在让我们转向一些不太复杂的集合:哈希 map! \ No newline at end of file diff --git a/src/ch08-03-hash-maps.md b/src/ch08-03-hash-maps.md index e69de29..99cedc4 100644 --- a/src/ch08-03-hash-maps.md +++ b/src/ch08-03-hash-maps.md @@ -0,0 +1,179 @@ +## 哈希 map + +> [ch08-03-hash-maps.md](https://github.com/rust-lang/book/blob/master/src/ch08-03-hash-maps.md) +>
+> commit 0d229cc5a3da341196e15a6761735b2952281569 + +最后要介绍的常用集合类型是**哈希 map**(*hash map*)。`HashMap`类型储存了一个键类型`K`对应一个值类型`V`的映射。它通过一个**哈希函数**(*hashing function*)来实现映射,它决定了如何将键和值放入内存中。很多编程语言支持这种数据结构,不过通常有不同的名字:哈希、map、对象、哈希表或者关联数组,仅举几例。 + +哈希 map 可以用于需要任何类型作为键来寻找数据的情况,而不是像 vector 那样通过索引。例如,在一个游戏中,你可以将每个团队的分数记录到哈希 map 中,其中键是队伍的名字而值是每个队伍的分数。给出一个队名,就能得到他们的得分。 + +本章我们会介绍哈希 map 的基本 API,不过还有更多吸引人的功能隐藏于标准库中的`HashMap`定义的函数中。请一如既往地查看标准库文档来了解更多信息。 + +### 新建一个哈希 map + +可以使用`new`创建一个空的`HashMap`,并使用`insert`来增加元素。这里我们记录两支队伍的分数,分别是蓝队和黄队。蓝队开始有 10 分而黄队开始有 50 分: + +```rust +use std::collections::HashMap; + +let mut scores = HashMap::new(); + +scores.insert(String::from("Blue"), 10); +scores.insert(String::from("Yellow"), 50); +``` + +注意必须首先`use`标准库中集合部分的`HashMap`。在这三个常用集合中,这个是最不常用的,所以并不包含在被 prelude 自动引用的功能中。标准库中对哈希 map 的支持也相对较少;例如,并没有内建的用于构建的宏。 + +就像 vector 一样,哈希 map 将他们的数据储存在堆上。这个`HashMap`的键类型是`String`而值类型是`i32`。同样类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。 + +另一个构建哈希 map 的方法是使用一个元组的 vector 的`collect`方法,其中每个元组包含一个键值对。`collect`方法可以将数据收集进一系列的集合类型,包括`HashMap`。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用`zip`方法来创建一个元组的 vector,其中“Blue”与 10 是一对,依此类推。接着就可以使用`collect`方法将这个元组 vector 转换成一个`HashMap`: + +```rust +use std::collections::HashMap; + +let teams = vec![String::from("Blue"), String::from("Yellow")]; +let initial_scores = vec![10, 50]; + +let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect(); +``` + +这里`HashMap<_, _>`类型注解是必要的,因为可能`collect`进很多不同的数据结构,而除非显式指定 Rust 无从得知你需要的类型。但是对于键和值的参数来说,可以使用下划线而 Rust 可以根据 vector 中数据的类型推断出哈希 map 所包含的类型。 + +### 哈希 map 和所有权 + +对于像`i32`这样的实现了`Copy` trait 的类型,其值可以拷贝进哈希 map。对于像`String`这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者: + +```rust +use std::collections::HashMap; + +let field_name = String::from("Favorite color"); +let field_value = String::from("Blue"); + +let mut map = HashMap::new(); +map.insert(field_name, field_value); +// field_name and field_value are invalid at this point +``` + +当`insert`调用将`field_name`和`field_value`移动到哈希 map 中后,将不能使用这两个绑定。 + +如果将值的引用插入哈希 map,这些值本身将不会被移动进哈希 map。但是这些引用指向的值必须至少在哈希 map 有效时也是有效的。第十章生命周期部分将会更多的讨论这个问题。 + +### 访问哈希 map 中的值 + +可以通过`get`方法并提供对应的键来从哈希 map 中获取值: + +```rust +use std::collections::HashMap; + +let mut scores = HashMap::new(); + +scores.insert(String::from("Blue"), 10); +scores.insert(String::from("Yellow"), 50); + +let team_name = String::from("Blue"); +let score = scores.get(&team_name); +``` + +这里,`score`将会是与蓝队分数相关的值,而这个值将是`Some(10)`。因为`get`返回`Option`所以结果被封装进`Some`;如果某个键在哈希 map 中没有对应的值,`get`会返回`None`。程序将需要采用第六章提到的方法中之一来处理`Option`。 + +可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是`for`循环: + +```rust +use std::collections::HashMap; + +let mut scores = HashMap::new(); + +scores.insert(String::from("Blue"), 10); +scores.insert(String::from("Yellow"), 50); + +for (key, value) in &scores { + println!("{}: {}", key, value); +} +``` + +这会以任意顺序打印出每一个键值对: + +``` +Yellow: 50 +Blue: 10 +``` + +### 更新哈希 map + +虽然键值对的数量是可以增长的,不过每个单独的键同时只能关联一个值。当你想要改变哈希 map 中的数据时,必须选择是用新值替代旧值,还是完全无视旧值。我们也可以选择保留旧值而忽略新值,并只在键**没有**对应一个值时增加新值。或者可以结合新值和旧值。让我们看看着每一种方式是如何工作的! + +#### 覆盖一个值 + +如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便下面的代码调用了两次`insert`,哈希 map 也只会包含一个键值对,因为两次都是对蓝队的键插入的值: + +```rust +use std::collections::HashMap; + +let mut scores = HashMap::new(); + +scores.insert(String::from("Blue"), 10); +scores.insert(String::from("Blue"), 25); + +println!("{:?}", scores); +``` + +这会打印出`{"Blue": 25}`。原始的值 10 将被覆盖。 + +#### 只在键没有对应值时插入 + +我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希 map 有一个特有的 API,叫做`entry`,它获取我们想要检查的键作为参数。`entry`函数的返回值是一个枚举,`Entry`,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值 50,对于蓝队也是如此。使用 entry API 的代码看起来像这样: + +```rust +use std::collections::HashMap; + +let mut scores = HashMap::new(); +scores.insert(String::from("Blue"), 10); + +scores.entry(String::from("Yellow")).or_insert(50); +scores.entry(String::from("Blue")).or_insert(50); + +println!("{:?}", scores); +``` + +`Entry`的`or_insert`方法在键对应的值存在时就返回这个值的`Entry`,如果不存在则将参数作为新值插入并返回修改过的`Entry`。这比编写自己的逻辑要简明的多,另外也与借用检查器结合得更好。 + +这段代码会打印出`{"Yellow": 50, "Blue": 10}`。第一个`entry`调用会插入黄队的键和值 50,因为黄队并没有一个值。第二个`entry`调用不会改变哈希 map 因为蓝队已经有了值 10。 + +#### 根据旧值更新一个值 + +另一个常见的哈希 map 的应用场景是找到一个键对应的值并根据旧的值更新它。例如,如果我们想要计数一些文本中每一个单词分别出现了多少次,就可以使用哈希 map,以单词作为键并递增其值来记录我们遇到过几次这个单词。如果是第一次看到某个单词,就插入值`0`。 + + +```rust +use std::collections::HashMap; + +let text = "hello world wonderful world"; + +let mut map = HashMap::new(); + +for word in text.split_whitespace() { + let count = map.entry(word).or_insert(0); + *count += 1; +} + +println!("{:?}", map); +``` + +这会打印出`{"world": 2, "hello": 1, "wonderful": 1}`,`or_insert`方法事实上会返回这个键的值的一个可变引用(`&mut V`)。这里我们将这个可变引用储存在`count`变量中,所以为了赋值必须首先使用星号(`*`)解引用`count`。这个可变引用在`for`循环的结尾离开作用域,这样所有这些改变都是安全的并被借用规则所允许。 + +### 哈希函数 + +`HashMap`默认使用一个密码学上是安全的哈希函数,它可以提供抵抗拒绝服务(Denial of Service, DoS)攻击的能力。这并不是现有最快的哈希函数,不过为了更好的安全性带来一些性能下降也是值得的。如果你监控你的代码并发现默认哈希函数对你来说非常慢,可以通过指定一个不同的 *hasher* 来切换为另一个函数。hasher 是一个实现了`BuildHasher` trait 的类型。第十章会讨论 trait 和如何实现他们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。 + +## 总结 + +vector、字符串和哈希 map 会在你的程序需要储存、访问和修改数据时帮助你。这里有一些你应该能够解决的练习问题: + +* 给定一系列数字,使用 vector 并返回这个列表的平均数(mean, average)、中位数(排列数组后位于中间的值)和众数(mode,出现次数最多的值;这里哈希函数会很有帮助)。 +* 将字符串转换为 Pig Latin,也就是每一个单词的第一个辅音字母被移动到单词的结尾并增加“ay”,所以“first”会变成“irst-fay”。元音字母开头的单词则在结尾增加 “hay”(“apple”会变成“apple-hay”)。牢记 UTF-8 编码! +* 使用哈希 map 和 vector,创建一个文本接口来允许用户向公司的部门中增加员工的名字。例如,“Add Sally to Engineering”或“Add Amir to Sales”。接着让用户获取一个部门的所有员工的列表,或者公司每个部门的所有员工按照字母顺排序的列表。 + +标准库 API 文档中描述的这些类型的方法将有助于你进行这些练习! + +我们已经开始解除可能会有失败操作的复杂程序了,这也意味着接下来是一个了解错误处理的绝佳时机! \ No newline at end of file