Rust官方文档学习记录
常用命令
-
rustc
rustc xxx.rs // 编译Rust程序
-
cargo
cargo build // 编译Rust项目,如block在更新lock文件,可执行
rm -rf ~/.cargo/.package-cache
cargo run // 立刻运行Rust项目
cargo doc –open // 生成并打开项目文档
常见概念
变量和可变性
- 使用
let xxx
声明一个变量,此时这个变量是不可变的,但是可以被隐藏 - 使用
let mut xxx
声明一个可变变量,此时变量可以被重新赋值 - 使用
const XXX
声明一个常量,常量不可被重新赋值
隐藏
重复使用let
定义一个与之前变量同名的变量,我们称第一个变量被第二个变量隐藏了,此时使用该名称的变量会使用第二个变量,我们可以重复使用let
来多次隐藏。
|
|
上面例子会输出:
|
|
这个程序首先将 x
绑定到值 5
上。接着通过 let x =
隐藏 x
,获取初始值并加 1
,这样 x
的值就变成 6
了。然后,在内部作用域内,第三个 let
语句也隐藏了 x
,将之前的值乘以 2
,x
得到的值是 12
。当该作用域结束时,内部 shadowing 的作用域也结束了,x
又返回到 6
。
数据类型
标量类型
整型
长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 |
u8 |
16-bit | i16 |
u16 |
32-bit | i32 |
u32 |
64-bit | i64 |
u64 |
128-bit | i128 |
u128 |
arch | isize |
usize |
浮点型
Rust 的浮点数类型是 f32
和 f64
,分别占 32 位和 64 位。默认类型是 f64
布尔型
Rust 中的布尔类型有两个可能的值:true
和 false
。Rust 中的布尔类型使用 bool
表示。
字符型
Rust 的 char
类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值(意味着你可以使用emoji)。
复合类型
元组类型
相当于把一个或者多个类型的值组合成一个类型。元组的长度是固定的:一旦声明。其长度不会改变。
我们可以使用圆括号中逗号分割的值列表来创建一个元组。
|
|
同时可以对元组进行解构,或者只用点号(.
)跟着值的索引(从0开始)直接访问
|
|
数组类型
另一个包含多个值的方式是 数组(array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
可以使用以下两种方式来声明数组:
|
|
函数
Rust 中的函数定义以 fn
开始并在函数名后跟一对圆括号。大括号告诉编译器哪里是函数体的开始和结尾。
在函数签名中,必须 声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器不需要你在代码的其他地方注明类型来指出你的意图。在有多个参数时,使用,
来分割多个参数。
函数可以向调用它的代码返回值。使用(->
)后声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用 return
关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式。
|
|
语句和表达式
Rust是一门基于表达式(expression-based)的语言。
使用let
关键字创建变量并绑定一个值是一个语句(let y = 6;
)。
语句不返回值。不能把let
语句赋值给另外一个变量(let x = (let y = 6)
)。
表达式可以计算出一个值,考虑一个数学运算,比如5+6
,这是一个表达式并计算出值11
。表达式可以是语句的一部分。函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用于的大括号(代码块),{}
,也是一个表达式。
|
|
这个表达式:
|
|
是一个代码块,它的值是 4
。这个值作为 let
语句的一部分被绑定到 y
上。注意结尾没有分号的那一行 x+1
,与你见过的大部分代码行不同。表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。在接下来探索具有返回值的函数和表达式时要谨记这一点。
控制流
if表达式
if
表达式允许根据条件执行不同的代码分支。你提供一个条件并表示 “如果条件满足,运行这段代码;如果条件不满足,不运行这段代码。”
|
|
代码中的条件必须是bool
值。如果不是bool
值,我们会得到一个错误。
因为if
是一个表达式,所以我们可以在let
语句的右侧使用它。注意,if
和else
分支的结果都必须是相同类型
|
|
循环
Rust 中有三种循环:loop
、while
和for
。
loop表达式
loop
就是一个无限循环,需要显式的调用break
来退出这个循环。
如果存在嵌套循环,此时单独只用break
和 continue
只应用于此时语句最内层的循环。可以在循环上指定一个循环标签,然后将标签与break
和 continue
一起使用,此时这些关键字生效的则是已标记的循环。
|
|
loop
的另外一个用法是重试可能会失败的操作,比如检查线程是否完成了任务。如果将返回值加入你用来停止循环的break
表达式,它会被停止的循环返回。
|
|
while条件循环
当条件为真,执行循环。当条件不再为真,则停止循环。这个循环类型可以通过组合 loop
、if
、else
和 break
来实现。或者直接使用while
。
|
|
for遍历集合
可以使用 for
循环来对一个集合的每个元素执行一些代码。
|
|
Rust的循环不像“C风格”的循环
|
|
相反,Rust是这样的
|
|
当你需要知道当前已经循环多少次时,可以使用.enumerate()
函数
|
|
输出:
1 2 3 4 5
i = 0 and j = 5 i = 1 and j = 6 i = 2 and j = 7 i = 3 and j = 8 i = 4 and j = 9
所有权
首先,让我们看一下所有权的规则。当我们通过举例说明时,请谨记这些规则:
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
变量作用域
变量 s
绑定到了一个字符串字面值,这个字符串值是硬编码进程序代码中的。这个变量从声明的点开始直到当前 作用域 结束时都是有效的。示例 4-1 的注释标明了变量 s
在何处是有效的。
|
|
内存与分配
首先我们要知道变量的两种内存分配位置:堆
和栈
。这个概念再此不做赘述。
当我们声明一个变量的类型,它的内存分配在堆上,我这习惯将其成为引用类型,如果它的内存分配在栈上,则称为值类型。
值类型,如i32
、f64
。引用类型,如string
:
|
|
值类型的传递方式都是copy
|
|
此时 x
、y
都在栈上拥有属于自己的内存空间。
而引用类型的传递方式则不一样。
|
|
那如果是在函数里呢?
|
|
函数的返回值也可以转移所有权。
引用与借用
规则
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
如果我们只是想使用一个变量,而不是获取这个变量的所有权。我们则需要使用引用这个操作。
|
|
我们在这里获得的是s1
的引用,并没有获取s1
的所有权。所以上述代码可以正常的运行。
使用的&
符号就是引用,它允许你使用值但不获取其所有权。就像其他语言类似,获取一个变量的地址,也就是指针。
因为没有获取到所有权,所以当引用离开作用域时,不会将内存释放。但是这里的引用只能使用被引用的变量,当我们想修改这个变量时,则会发现会抛出一个错误。正如变量默认不可变一样,引用默认也是不可改变的。
|
|
如果我们想修改一个引用的值,我们只需做一个小调整:
|
|
这里使用&mut
获取到的是 s
的可变引用,这就清除的表明,我需要修改这个引用的值。不过可变引用有一个很大的限制。
在同一时间只能有一个对某一特定数据的可变引用
这个限制的好处是 Rust 可以在编译时就避免数据竞争。
不可变引用可以存在多个,但不能同时与可变引用存在。原因也很明显,谁也不想自己引用的变量在某一时间被修改。
这种概念和读写锁类似。可以拥有多个读锁(不可变引用),只能存在一个写锁并与读锁互斥(可变引用)。当了解这个概念之后就很好理解了。
注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如以下代码是可以编译的:
|
|
不可变引用 r1
和 r2
的作用域在 println!
最后一次使用之后结束,这也是创建可变引用 r3
的地方。它们的作用域没有重叠,所以代码是可以编译的。编译器在作用域结束之前判断不再使用的引用的能力被称为非词法作用域生命周期(Non-Lexical Lifetimes,简称NLL)。你可以在 The Edition Guide 中阅读更多关于它的信息。
结构体
定义结构体,需要使用 struct
关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为 字段(field)。例如,示例 5-1 展示了一个存储用户账号信息的结构体:
|
|
当变量名与字段名相同时,可以简写
|
|
我们还可以使用结构体更新语法
|
|
在这里..user1
必须放在最后,其他字段实际是使用=
的赋值,所以要注意所有权的移动。
我们还可以创建没有字段的元组结构体,使用.
接上索引来访问单独的值。
|
|
或者创建一个没有任何字段的结构体(类单元结构体)
单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。
|
|
todo 字段的生命周期
打印结构体
|
|
分别输出:
|
|
给struct
绑定方法
定义方法
|
|
为了使函数定义在Rectangle
的上下文中,我们定义了一个impl
块。在这个块中的所有内容都与Rectangle
相关联。
在area
的函数前面上,使用&self
来代替rectangle: &Rectangle
,&self
其实是self: &Self
的缩写。在一个impl
块中,方法的第一个参数必须有一个名为self
的Self
类型的参数,所以Rust在这里提供了self
这个名字来缩写,在这里我们使用&
来表示只是借用这个示例,并没有获取所有权。同时,我们也一样可以获取所有权,或者可变借用。
那我们的
&
运算符去哪里了呢?
area()
函数的参数是&self
,但是我们调用的地方并没有借用rect1
;那是Rust有一个叫做自动引用和解引用的功能。方法调用是Rust中少数几个拥有这种行为的地方。这样我们的代码实际是这样工作的:
1 2
rect1.area(); (&rect1).area();
这样看起来第一种方式简洁的多。
关联函数
所有在impl
块中定义的函数被称为关联函数。我们可以定义一个不以self
为第一参数的关联函数(它不是方法),我们已经使用过这样的函数了,String::from()
。通常这样的函数被我们用作为返回一个结构体新实例的构造函数。
|
|
枚举
在 Rust 中我们使用enum
来定义一个枚举类型。
|
|
在上述代码中定义了一个IpAddrKind
枚举来列出可能的 IP 地址类型,V4
和V6
。这两种类型被称为枚举的成员。
我们可以像这样创建IpAddrKind
两个不同成员的实例。
|
|
可以看到我们可以使用任意成员来调用route
函数。
枚举的成员也可以关联一个值。
|
|
或者关联上一个元组。
|
|
我们还可以对枚举类型关联上方法。
|
|
我们就可以直接使用home.call()
来调用这个方法。
todo option
相关控制流运算符
match
运算符
如果有其他语言基础的话,看到match
这个关键字,应该能联想到switch
。它的用法和switch
是差不多的,只是有一些细节不同。这里我们用上面的IpAddr
绑定的方法call
举例:
|
|
可以看到,我们使用了match
匹配了self
这个变量,在 case 里我们用上了IpAddr::V4(a, b, c, d)
,使用了a b c d
四个变量来分别匹配绑定上的V4(u8, u8, u8, u8)
4个值。而在IpAddr::V6(str)
使用了str
来接收绑定的String
这样我们就可以根据不同的类型在做出不同的操作了。
但是注意 Rust 默认需要我们处理match
后变量的所有可能情况,也就是说,下述代码是不正确的:
|
|
Rust 会提示我们少了IpAddr::V6
的case
。
上述例子只有2种情况,那如果是10种?20种呢?如果真的全部都要列出来的话,那未免太蠢了。所以 Rust 和其他语言一样,提供了类似default
的功能,使用的关键字是_,
我们来看看例子。
|
|
注意必须要显式调用_ => ()
Rust不会默认给你加上。这样的话就可以忽略掉V6
的匹配了。
if let
简单控制流
想上面的例子,只有2种情况,我们还需要显式的用_
去忽略匹配,实在是过于冗长,所以 Rust给了我们另外一种方式来处理这种情况。
|
|
这样我们就不用再用那么冗长的方式来编写了。
package / crate / module
cargo new
会生成项目的雏形,提供了src/main.rs
hesrc/lib.rs
文件,随着项目复杂度的增长,代码量也会随之增长,如果靠一个文件来维护一大堆代码,肯定是不合适的。一般都会使用模块
来拆分文件。
在这里学习一下rust
中代码的组织方式,主要涉及到一下几个名词:
- package:
Cargo
中的概念,用于管理crate
- crate:模块的集合,编译单位,有
bin
和lib
两种,分别是可执行文件,和供他人调用 - module:用户在
crate
内组织代码 - workspace:项目复杂时,管理多个
package
package
使用cargo new
命令会创建一个新项目,也就是一个package
,里面带有一个Cargo.toml
文件,用于定义package
、所需外部依赖,以及如何编译crate
等
crate
Rust
里有两种crate
,lib
类型和bin
类型,并且默认以文件名为标准处理crate
:
src/main.rs
:表示该crate
时一个bin
类型大crate
src/lib.rs
:表示该crate
时一个lib
类型的crate
并且,一个package
中的crate
还有如下与约束:
- 可以有多个
bin
类型的crate
- 只能有0个或者1个
lib
类型的crate
以上两条约束并不互斥,也就是说一个项目下可以有一个lib
和多个bin
类型的crate
,也就是可以编译出多个可执行文件
只是如果有多个bin
类型的crate
,一个src/main.rs
就不够了,需要放到src/bin
下,每个crate
一个文件
mod
当项目逐渐膨胀后,可以对代码以mod
「文件/文件夹」为单位进行拆分,而不是把所有代码都写在src/main.rs
或者src/lib.rs
里
以lib
类型的crate
为例,该类型的crate
入口在src/lib.rs
,也就是crate
的根。定义一个模块也很简单
|
|
而在实际项目中,我们不会只有一个lib.rs
文件,而是会将代码按功能进行拆分成多个模块
模块拆分
一般来说,一个文件都会被看作为一个mod
,并且mod
可以嵌套定义。嵌套定义的mod
可以卸载一个文件里,也可以通过文件夹的形式来实现。具体的我们来看几个例子。
假设当前项目文件结构如下:
|
|
在这里定义了3个mod
:mod_a、mod_b 和 mod_c,其中mod_a
为文件夹形式,而mod_b
和mod_c
都有对应的文件。其中mod_b
是mod_a
的子模块。
我们来看下各个模块之间如何声明和引用。
首先我们先来看看crate
的根,也就是lib.rs
|
|
在这里声明了两个mod
,如果需要在crate
外部访问,需要在mod
前面加上pub
关键字。注意这里不需要声明mod_a
的子模块mod_b
,这个需要mod_a
来声明。
再来看一下这两个mod。先看mod_a,这是一个文件夹形式存在的mod,按cargo规定,这时候需要在该文件夹下有一个名为mod.rs的文件定义该mod下的内容。该文件内容如下:
|
|
可以看到,这个文件和lib.rs
类似,都可以声明mod
。但该文件声明的mod
可以保存到mod.rs
:
|
|
我们再来看看mod_c.rs
的代码:
|
|
除了如何定义mod
,我们还需要的是如何引用其他mod
的定义。在mod_c
中,要想使用mod_b
,可以使用:
- 绝对路径
use crate::mod_a::mod_b
而在mod_b
中使用mod_c
的时候,使用了use super::super::mod_c
这种相对路径的形式。
添加main.rs
最后在上面代码的基础上添加main.rs
,看看作为外部crate
如何使用上面的mod_a
|
|
在这里需要注意的是,引用自己lib
的方法不能使用上面说的绝对路径
或者相对路径
这两种引用方式,必须使用该crate
的名称「也就是Cargo.toml
里的名称」来应用。因为main
和lib
属于不同的crate
pub 修饰符
要想访问其他mod
中的结构体、方法、枚举等,需要对方声明为pub
。如果是想操作结构体中的字段,可以有一下两种方法
- 提供对应的
pub
方法 - 直接修改字段为
pub
use 语句
讲了这么做如何定义mod
,我们来看下如何使用
在crate
和模块中我们可能定义了函数、结构体等,要想在其他模块或者crate
中使用,需要将其引入到当前文件中,类似php
的use
,或者java
中的import
,在Rust
中我们需要使用use
如何表示要被引用的对象,Rust
里称之为path
,我们可以理解为操作系统中的文件目录
path
有两种形式,也和文件系统一样,有绝对路径和相对路径:
- 绝对路径始于
crate
的根(src/main.rs
orsrc/lib.rs
),可以使用crate
名或者crate
这个字面值表示 - 相对路径可以使用当前模块名,当前模块中可以使用的对象,
super
和self
等
path
中的层级使用两个冒号::
,类似文件系统中的斜线.
假设有一下代码(来自官方文档):
|
|
有一些限制也需要知道:
- 在夫模块中不能使用子模块中的私有项目
- 子模块可以使用父模块中的所有项目
- 同一模块内可以直接互相使用
下面是一个使用了super
的例子:
|
|
fix_incorrect_order
方法和cook_order
同属于一个模块,可以直接调用。server_order
方法和back_of_house
同级,因此需要使用super
访问到同级的server_order
方法
如果use
后面的路径具有共同的父路径,可以使用简化的模式。比如:
|
|
可以简化为:
|
|
如果use
的mod
直接有父子关系,也可以像上面那样简化,使用self
代表父mod
。比如:
|
|
可以简化为:
|
|
如果想将某一路径下的所有pub
的item
都引入到当前文件中,可以使用*
|
|
一般会在单元测试中常用,不推荐在业务代码中使用
|
|
引用层级
对比一下两种引用方式:
|
|
这两种方法的结果都是一样的,但是阅读起来给人的感觉不一样。一般来说推荐第一个 case ,这样能明确的知道使用的方法是外部 hosting 模块的方法,后者的话不知道是 use 进来的,还是本模块定义的
重命名
有时候从不同的 crate 或者 mod 引入了同名的 item,这个时候最简单的方式是使用as
关键字进行重命名。
|
|
re-exporting 再导出
当使用use
关键字将外部 item 导入到当前文件之后,这个 item 在当前代码是 private 的,如果使用pub use
的话,还能让使用当前 mod 的第三者,使用在该 mod 中引入的 item。
该机制称为 re-exporting 。
workspace
workspace 用于管理多个相关的 package,不同的 package 有各自的 Cargo.toml,但是整个 workspace 共享一个Cargo.lock,也只有一个 target 目录(编译输出)。
虽然 workspace 内的项目共享一个 Cargo.lock,但是他们之间默认不互相依赖,需要显示添加它们之间的依赖关系。而且在一个项目中添加的依赖,在其他项目中如果想使用,还需要再次声明依赖才行。
不过据我观察 workspace 功能没有什么特别强大之处,不使用该功能也可以同时管理几个 Cargo 项目,因此这里就不再深入介绍了。
常见集合
一般来说在 Rust 常用的集合
vector
允许我们一个挨着一个地存储一系列数量可变的值- 字符串是字符的集合,
String
- 哈希 map 允许我们将值与一个特定的键相关联
vector
新建vector
|
|
更新vector
|
|
类似于其他任何的struct
,vector 在离开其作用域时会被释放。
访问vector
|
|
使用索引语法访问vector
会导致程序panic
,而使用 get 方法访问时 会返回一个 None
在拿到 vector 中任意一个有效的引用,借用检查器将会窒息所有权和借用规则,来确保 vector 内容的这个引用和任何其他引用保持有效。当我们获取到了 vector 的第一个元素的不可变引用并在 vector 末尾增加一个元素的时候,编译无法通过。
|
|
为什么第一元素的引用会关心到末尾的变化?那是因为 vector 和 golang 的 slice 一样,在内部的空间不足时,会进行拷贝扩容,这样第一个元素的引用就指向了被释放的内存。
遍历 vector
|
|
同时 vector 也可以使用枚举来存储多种类型的值
|
|
字符串
创建一个字符串
|
|
更新字符串
|
|
执行上述代码之后,s
将会包含x
,x
还可以继续使用,因为push_str
方法使用了字符串 slice,因此我们不需要获取参数的所有权。
然而直接使用+
运算符将两个String
值合并到一个新String
值中,此时s1
在相加后失去了所有权。因为+
调用了add
函数,这个函数看起来像这样
|
|
这并不是标准库当中实际的签名,标准库中的add
使用泛型定义。
那为什么add
方法的第二个参数是&str
,我们在调用时却是&String
,并且可以通过编译。
那是因为 Rust 使用了一个被称为 Deref 强制转换 的技术,可以理解为把&s2
变成了&s2[..]
并且在签名中add
方法获取self
的所有权,这意味着s1
的所有权将被移动到add
调用中,之后不在生效,这样的好处是不会生成很多拷贝,这个实现比拷贝更加高效。
如果想获取多个字符相加:
|
|
上述代码会将s
设置为hello world
。format!
与println!
的工作原理相同,并且它使用索引不会获取任何参数的所有权。
索引字符串
在很多语言中,通过索引来一用字符串中的单独字符是很常见的操作,比如说 golang
。然而在 Rust 中,不允许使用索引语法访问String
的一部分,会出现错误。
|
|
原因则是在 Rust 中,String
是一个Vec<u8>
的封装,相当于在底层存储的是字节,和大多数编程语言一样。直接使用索引语法获取String
的一部分,相当于获取UTF-8
字符的一个字节,这样可能会返回意外的值,Rust 根本不会编译这些代码。
在Golong
中可以直接索引访问,那是因为Golang
会默认把字符串按照rune
来 遍历。
在 Rust 中也有这样的操作,只不过需要显示使用:
|
|
这样可以获取到每个UTF-8
编码的字符,也就相当于 Golang
中的rune
。
|
|
上述代码可以安全的使用索引来获取单个字符。
Hash Map
新建
可以使用new
来创建一个空的HashMap
,并使用insert
增加元素。
|
|
注意,使用HashMap
需要引入
访问
可以 使用get
方法传入对应的键,从HashMap
中获取值
|
|
或者使用与 vector 类似的方式来遍历
|
|
更新
直接使用相同的键重新调用insert
方法,这样会直接替换成新值。
当我们需要检查对应键是否存在值时,可以使用entry
方法
|
|
并且or_insert
方法返回这个值的可变引用&mut V
,我们可以直接改变它。
所有权
对于类似i32
这样实现了Copy
trait 的类型,值可以直接拷贝进HashMap
。对于拥有所有权的值,其值将被移动而HashMap
会成为这些值的所有者。或者将值的引用传入HashMap
,但是需要保证生命周期,后续会了解。