Rust-Learning-2
Rust学习篇,介绍Rust的基础知识。
结构体
1 |
|
一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 实例。
1 |
|
特殊写法
变量与字段同名时的字段初始化简写语法
1 |
|
结构体更新语法(struct update syntax)
1 |
|
可以简单写成:
1 |
|
无命名字段 我们叫做元组结构体(tuple structs)
1 |
|
我们也可以定义一个没有任何字段的结构体!它们被称为 类单元结构体(unit-like structs)
结构体例子
1 |
|
让我们来试试!现在 println!
宏调用看起来像
println!("rect1 is {:?}", rect1);
这样。在 {}
中加入 :?
指示符告诉 println!
我们想要使用叫做
Debug
的输出格式。Debug
是一个
trait,它允许我们以一种对开发者有帮助的方式打印结构体,以便当我们调试代码时能看到它的值。为此,在结构体定义之前加上
# derive(Debug)]
注解。
1 |
|
1 |
|
方法语法
方法
方法 与函数类似:它们使用 fn
关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或
trait
对象的上下文,将分别在第六章和第十七章讲解),并且它们第一个参数总是
self
,它代表调用该方法的结构体实例。
1 |
|
方法语法(method syntax)在
Rectangle
实例上调用 area
方法。方法语法获取一个实例并加上一个点号,后跟方法名、圆括号以及任何参数。
关联
impl
块的另一个有用的功能是:允许在 impl
块中定义 不 以 self
作为参数的函数。这被称为 关联函数(associated
functions),因为它们与结构体相关联。它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。你已经使用过
String::from
关联函数了。
1 |
|
使用结构体名和 ::
语法来调用这个关联函数:比如
let sq = Rectangle::square(3);
。这个方法位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间。第七章会讲到模块。
枚举类型
可以通过在代码中定义一个 IpAddrKind
枚举来表现这个概念并列出可能的 IP 地址类型,V4
和
V6
。这被称为枚举的
成员(variants):
1 |
|
枚举值
可以像这样创建 IpAddrKind
两个不同成员的实例:
1 |
|
注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在
IpAddrKind::V4
和 IpAddrKind::V6
都是
IpAddrKind
类型的。例如,接着可以定义一个函数来获取任何
IpAddrKind
:
1 |
|
现在可以使用任一成员来调用这个函数:
1 |
|
使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个存储实际 IP 地址 数据 的方法;只知道它是什么 类型 的。
1 |
|
用枚举替代结构体还有另一个优势:每个成员可以处理不同类型和数量的数据。IPv4
版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将
V4
地址存储为四个 u8
值而 V6
地址仍然表现为一个
String
,这就不能使用结构体了。枚举则可以轻易处理的这个情况:
1 |
|
1 |
|
有一个非常重要的枚举类型:Option<T>
match控制流
1 |
|
一个分支有两个部分:一个模式和一些代码。第一个分支的模式是值
Coin::Penny
而之后的 =>
运算符将模式和将要运行的代码分开。这里的代码就仅仅是值
1
。每一个分支之间使用逗号分隔。
1 |
|
如果想要在分支中运行多行代码,可以使用大括号。
1 |
|
如果调用
value_in_cents(Coin::Quarter(UsState::Alaska))
,coin
将是
Coin::Quarter(UsState::Alaska)
。当将值与每个分支相比较时,没有分支会匹配,直到遇到
Coin::Quarter(state)
。这时,state
绑定的将会是值 UsState::Alaska
。接着就可以在
println!
表达式中使用这个绑定了,像这样就可以获取
Coin
枚举的 Quarter
成员中内部的州的值。
match枚举必须写齐全
match
还有另一方面需要讨论。考虑一下
plus_one
函数的这个版本,它有一个 bug 并不能编译:
1 |
|
我们没有处理 None
的情况,所以这些代码会造成一个
bug。幸运的是,这是一个 Rust 知道如何处理的
bug。如果尝试编译这段代码,会得到这个错误:
1 |
|
Rust 知道我们没有覆盖所有可能的情况甚至知道哪些模式被忘记了!Rust
中的匹配是
穷尽的(exhaustive):必须穷举到最后的可能性来使代码有效。特别的在这个
Option<T>
的例子中,Rust 防止我们忘记明确的处理
None
的情况,这使我们免于假设拥有一个实际上为空的值,这造成了之前提到过的价值亿万的错误。
match通配符
Rust
也提供了一个模式用于不想列举出所有可能值的场景。例如,u8
可以拥有 0 到 255 的有效的值,如果我们只关心 1、3、5 和 7
这几个值,就并不想必须列出 0、2、4、6、8、9 一直到 255
的值。所幸我们不必这么做:可以使用特殊的模式 _
替代:
1 |
|
_
模式会匹配所有的值。通过将其放置于其他分支之后,_
将会匹配所有之前没有指定的可能的值。()
就是 unit 值,所以
_
的情况什么也不会发生。因此,可以说我们想要对
_
通配符之前没有列出的所有可能的值不做任何处理。
if let 简洁控制流
if let
语法让我们以一种不那么冗长的方式结合
if
和
let
,来处理只匹配一个模式的值而忽略其他模式的情况。考虑示例
6-6 中的程序,它匹配一个 Option<u8>
值并只希望当值为
3 时执行代码:
1 |
|
等价于下面的代码
1 |
|
也可以加上一处else以对操作进行扩展
可以在 if let
中包含一个
else
。else
块中的代码与 match
表达式中的 _
分支块中的代码相同,这样的 match
表达式就等同于 if let
和 else
。回忆一下示例
6-4 中 Coin
枚举的定义,其 Quarter
成员也包含一个 UsState
值。如果想要计数所有不是 25
美分的硬币的同时也报告 25 美分硬币所属的州,可以使用这样一个
match
表达式:
1 |
|
或者可以使用这样的 if let
和 else
表达式:
1 |
|
包、crate、模块
让我们聊聊 模块 与 crate。下面是一个总结:
- crate 是一个二进制或库项目。
- crate 根(crate root)是一个用来描述如何构建 crate 的文件。
- 带有 Cargo.toml 文件的 包 用以描述如何构建一个或多个 crate。一个包中至多可以有一个库项目。
一个包可以带有零个或一个库 crate 和任意多个二进制 crate。一个包中必须带有至少一个(库或者二进制)crate。
如果包同时包含 src/main.rs 和 src/lib.rs,那么它带有两个 crate:一个库和一个二进制项目,同名。如果只有其中之一,则包将只有一个库或者二进制 crate。包可以带有多个二进制 crate,需将其文件置于 src/bin 目录;每个文件将是一个单独的二进制 crate。
模块
首先讲讲模块。模块允许我们将代码组织起来。下面的代码定义了名为
sound
的模块,其包含名为 guitar
的函数。
1 |
|
这里定义了两个函数,guitar
和
main
。guitar
函数定义于 mod
块中。这个块定义了 sound
模块。
为了将代码组织到模块层次体系中,可以将模块嵌套进其他模块,如示例 7-2 所示:
文件名: src/main.rs
1 |
|
在 “包和 crate 用来创建库和二进制项目” 部分提到 src/main.rs
和 src/lib.rs 被称为 crate 根。他们被称为
crate 根是因为这两个文件在 crate 模块树的根组成了名为 crate
模块。所以示例 7-2 中,有如示例 7-3 所示的模块树:
1 |
|
如果想要调用函数,需要知道其 路径。“路径” 是 “名称”(“name”) 的同义词,不过它用于文件系统语境。另外,函数、结构体和其他项可能会有多个指向相同项的路径,所以 “名称” 这个概念不太准确。
路径 可以有两种形式:
- 绝对路径(absolute path)从 crate
根开始,以 crate 名或者字面值
crate
开头。 - 相对路径(relative
path)从当前模块开始,以
self
、super
或当前模块的标识符开头。
绝对路径和相对路径都后跟一个或多个由双冒号(::
)分割的标识符。
模块私有性
之前我们讨论到模块的语法和组织代码的用途。Rust 采用模块还有另一个原因:模块是 Rust 中的 私有性边界(privacy boundary)。如果你希望函数或结构体是私有的,将其放入模块。私有性规则有如下:
- 所有项(函数、方法、结构体、枚举、模块和常量)默认是私有的。
- 可以使用
pub
关键字使项变为公有。 - 不允许使用定义于当前模块的子模块中的私有代码。
- 允许使用任何定义于父模块或当前模块中的代码。
换句话说,对于没有 pub
关键字的项,当你从当前模块向 “下”
看时是私有的,不过当你向 “上”
看时是公有的。再一次想象一下文件系统:如果你没有某个目录的权限,则无法从父目录中查看其内容。如果有该目录的权限,则可以查看其中的目录和任何父目录。
现在的错误表明 clarinet
函数是私有的。私有性规则适用于结构体、枚举、函数和方法以及模块。
在 clarinet
函数前增加 pub
关键字使其变为公有,如示例 7-8 所示:
文件名: src/main.rs
1 |
|
也可以使用 super
开头来构建相对路径。这么做类似于文件系统中以 ..
开头:该路径从 父 模块开始而不是当前模块。这在例如示例
7-9 这样的情况下有用处,在这里 clarinet
函数通过指定以
super
开头的路径调用 breathe_in
函数:
文件名: src/lib.rs
1 |
|
clarinet
函数位于 instrument
模块中,所以可以使用 super
进入 instrument
的父模块,也就是根 crate
。从这里可以找到
breathe_in
。成功!
你可能想要使用 super
开头的相对路而不是以
crate
开头的绝对路径的原因是 super
可能会使修改有着不同模块层级结构的代码变得更容易,如果定义项和调用项的代码被一同移动的话。例如,如果我们决定将
instrument
模块和 breathe_in
函数放入
sound
模块中,这时我们只需增加 sound
模块即可,如示例 7-10 所示。
文件名: src/lib.rs
1 |
|
结构体和枚举类型的pub
1 |
|
1 |
|
use 缩短关键路径
1 |
|
1 |
|
示例 7-13 中,你可能会好奇为什么指定
use crate::sound::instrument
接着在 main
中调用 instrument::clarinet
,而不是如示例 7-16
所示的有相同行为的代码:
文件名: src/main.rs
1 |
|
示例 7-16: 通过 use
将 clarinet
函数引入作用域,这是不推荐的
对于函数来说,通过 use
指定函数的父模块接着指定父模块来调用方法被认为是习惯用法。这么做而不是像示例
7-16 那样通过 use
指定函数的路径,清楚的表明了函数不是本地定义的,同时仍最小化了指定全路径时的重复。
对于结构体、枚举和其它项,通过 use
指定项的全路径是习惯用法。例如,示例 7-17 展示了将标准库中
HashMap
结构体引入作用域的习惯用法。
as关键字
将两个同名类型引入同一作用域这个问题还有另一个解决办法:可以通过在
use
后加上 as
和一个新名称来为此类型指定一个新的本地名称。示例 7-20
展示了另一个编写示例 7-19 中代码的方法,通过 as
重命名了其中一个 Result
类型。
文件名: src/lib.rs
1 |
|
pub use
当使用 use
关键字将名称导入作用域时,在新作用域中可用的名称是私有的。如果希望调用你编写的代码的代码能够像你一样在其自己的作用域内引用这些类型,可以结合
pub
和 use
。这个技术被称为
“重导出”(re-exporting),因为这样做将项引入作用域并同时使其可供其他代码引入自己的作用域。
例如,示例 7-21 展示了示例 7-15 中的代码将
performance_group
的 use
变为
pub use
的版本。
1 |
|
外部包
在 Cargo.toml 中加入 rand
依赖告诉了 Cargo 要从
https://crates.io 下载 rand
和其依赖,并使其可在项目代码中使用。
接着,为了将 rand
定义引入项目包的作用域,加入一行
use
,它以 rand
包名开头并列出了需要引入作用域的项。回忆一下第二章的 “生成一个随机数”
部分,我们曾将 Rng
trait 引入作用域并调用了
rand::thread_rng
函数:
1 |
|
注意标准库(std
)对于你的包来说也是外部
crate。因为标准库随 Rust 语言一同分发,无需修改 Cargo.toml
来引入 std
,不过需要通过 use
将标准库中定义的项引入项目包的作用域中来引用它们,比如
HashMap
:
1 |
|
这是一个以标注库 crate 名 std
开头的绝对路径。
通用集合类型
vector
vector 允许我们在一个单独的数据结构中储存多于一个的值,它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。
创建vector
1 |
|
1 |
|
vec!为Rust自带的宏
更新vector
1 |
|
vector作用域
丢弃 vector 时也会丢弃其所有元素
类似于任何其他的 struct
,vector
在其离开作用域时会被释放,如示例 8-4 所标注的:
1 |
|
示例 8-4:展示 vector 和其元素于何处被丢弃
当 vector 被丢弃时,所有其内容也会被丢弃,这意味着这里它包含的整数将被清理。这可能看起来非常直观,不过一旦开始使用 vector 元素的引用,情况就变得有些复杂了。
读取vector元素的方法
现在你知道如何创建、更新和销毁 vector 了,接下来的一步最好了解一下如何读取它们的内容。有两种方法引用 vector 中储存的值。为了更加清楚的说明这个例子,我们标注这些函数返回的值的类型。
展示了访问 vector 中一个值的两种方式,索引语法或者 get
方法:
1 |
|
这里有两个需要注意的地方。首先,我们使用索引值 2
来获取第三个元素,索引是从 0
开始的。其次,这两个不同的获取第三个元素的方式分别为:使用
&
和 []
返回一个引用;或者使用
get
方法以索引作为参数来返回一个
Option<&T>
。
遍历 vector 中的元素
如果想要依次访问 vector
中的每一个元素,我们可以遍历其所有的元素而无需通过索引一次一个的访问。示例
8-8 展示了如何使用 for
循环来获取 i32
值的
vector 中的每一个元素的不可变引用并将其打印:
1 |
|
示例 8-8:通过 for
循环遍历 vector 的元素并打印
我们也可以遍历可变 vector 的每一个元素的可变引用以便能改变他们。示例
8-9 中的 for
循环会给每一个元素加 50
:
1 |
|
示例8-9:遍历 vector 中元素的可变引用
为了修改可变引用所指向的值,在使用 +=
运算符之前必须使用解引用运算符(*
)获取 i
中的值。第十五章会详细介绍 *
。
使用枚举来储存多种类型
在本章的开始,我们提到 vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举!
例如,假如我们想要从电子表格的一行中获取值,而这一行的有些列包含数字,有些包含浮点值,还有些是字符串。我们可以定义一个枚举,其成员会存放这些不同类型的值,同时所有这些枚举成员都会被当作相同类型,那个枚举的类型。接着可以创建一个储存枚举值的 vector,这样最终就能够储存不同类型的值了。示例 8-10 展示了其用例:
1 |
|
String
新建String
1 |
|
这新建了一个叫做 s
的空的字符串,接着我们可以向其中装载数据。通常字符串会有初始数据,因为我们希望一开始就有这个字符串。为此,可以使用
to_string
方法,它能用于任何实现了 Display
trait 的类型,字符串字面值也实现了它。示例 8-12 展示了两个例子。
1 |
|
1 |
|
实际上,from方法和to_string方法并没有本质区别
更新String
1 |
|
示例 8-15:使用 push_str
方法向 String
附加字符串 slice
1 |
|
示例 8-16:将字符串 slice 的内容附加到 String
后使用它
1 |
|
示例 8-17:使用 push
将一个字符加入 String
值中
使用 +
运算符或 format!
宏拼接字符串
通常你会希望将两个已知的字符串合并在一起。一种办法是像这样使用
+
运算符,如示例 8-18 所示。
1 |
|
示例 8-18:使用 +
运算符将两个 String
值合并到一个新的 String
值中
如果想要级联多个字符串,+
的行为就显得笨重了:
1 |
|
这时 s
的内容会是 “tic-tac-toe”。在有这么多
+
和 "
字符的情况下,很难理解具体发生了什么。对于更为复杂的字符串链接,可以使用
format!
宏:
1 |
|
索引字符串
在很多语言中,通过索引来引用字符串中的单独字符是有效且常见的操作。然而在
Rust 中,如果你尝试使用索引语法访问 String
的一部分,会出现一个错误。考虑一下如示例 8-19 中所示的无效代码。
1 |
|
示例 8-19:尝试对字符串使用索引语法
会导致如下错误:
1 |
|
错误和提示说明了全部问题:Rust 的字符串不支持索引。
字节、标量值和字形簇!天呐!
这引起了关于 UTF-8 的另外一个问题:从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇(最接近人们眼中 字母 的概念)。
比如这个用梵文书写的印度语单词 “नमस्ते”,最终它储存在 vector 中的
u8
值看起来像这样:
1 |
|
这里有 18 个字节,也就是计算机最终会储存的数据。如果从 Unicode
标量值的角度理解它们,也就像 Rust 的 char
类型那样,这些字节看起来像这样:
1 |
|
这里有六个
char
,不过第四个和第六个都不是字母,它们是发音符号本身并没有任何意义。最后,如果以字形簇的角度理解,就会得到人们所说的构成这个单词的四个字母:
1 |
|
字符串 slice
索引字符串通常是一个坏点子,因为字符串索引应该返回的类型是不明确的:字节值、字符、字形簇或者字符串
slice。因此,如果你真的希望使用索引创建字符串 slice 时 Rust
会要求你更明确一些。为了更明确索引并表明你需要一个字符串 slice,相比使用
[]
和单个值的索引,可以使用 []
和一个 range
来创建含特定字节的字符串 slice:
1 |
|
这里,s
会是一个
&str
,它包含字符串的头四个字节。早些时候,我们提到了这些字母都是两个字节长的,所以这意味着
s
将会是 “Зд”。
如果获取 &hello[0..1]
会发生什么呢?答案是:在运行时会 panic,就跟访问 vector
中的无效索引时一样:
1 |
|
你应该小心谨慎的使用这个操作,因为这么做可能会使你的程序崩溃。
遍历字符串的方法
幸运的是,这里还有其他获取字符串元素的方式。
如果你需要操作单独的 Unicode 标量值,最好的选择是使用
chars
方法。对 “नमस्ते” 调用 chars
方法会将其分开并返回六个 char
类型的值,接着就可以遍历其结果来访问每一个元素了:
1 |
|
这些代码会打印出如下内容:
1 |
|
bytes
方法返回每一个原始字节,这可能会适合你的使用场景:
1 |
|
这些代码会打印出组成 String
的 18 个字节:
1 |
|
不过请记住有效的 Unicode 标量值可能会由不止一个字节组成。
从字符串中获取字形簇是很复杂的,所以标准库并没有提供这个功能。crates.io 上有些提供这样功能的 crate。
Hash map
新建Hash
1 |
|
新建哈希 map 并插入一些键值对
另一个构建哈希 map 的方法是使用一个元组的 vector 的
collect
方法
1 |
|
这里 HashMap<_, _>
类型注解是必要的,因为可能
collect
很多不同的数据结构,而除非显式指定否则 Rust
无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而
Rust 能够根据 vector 中数据的类型推断出 HashMap
所包含的类型。
哈希 map 和所有权
对于像 i32
这样的实现了 Copy
trait
的类型,其值可以拷贝进哈希 map。对于像 String
这样拥有所有权的值,其值将被移动而哈希 map 会成为这些值的所有者
1 |
|
访问哈希 map 中的值
可以通过 get
方法并提供对应的键来从哈希 map 中获取值
1 |
|
这里,score
是与蓝队分数相关的值,应为
Some(10)
。因为 get
返回
Option<V>
,所以结果被装进
Some
;如果某个键在哈希 map 中没有对应的值,get
会返回 None
。
可以使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是
for
循环:
1 |
|
更新哈希 map
覆盖一个值
如果我们插入了一个键值对,接着用相同的键插入一个不同的值,与这个键相关联的旧值将被替换。即便示例
8-24 中的代码调用了两次 insert
,哈希 map
也只会包含一个键值对,因为两次都是对蓝队的键插入的值:
1 |
|
示例 8-24:替换以特定键储存的值
这会打印出 {"Blue": 25}
。原始的值 10
则被覆盖了。
没有对应值时插入
我们经常会检查某个特定的键是否有值,如果没有就插入一个值。为此哈希
map 有一个特有的 API,叫做
entry
,它获取我们想要检查的键作为参数。entry
函数的返回值是一个枚举,Entry
,它代表了可能存在也可能不存在的值。比如说我们想要检查黄队的键是否关联了一个值。如果没有,就插入值
50,对于蓝队也是如此。使用 entry API 的代码看起来像示例 8-25 这样:
1 |
|