Rust 从 0 到 1 学习记录(一)

0x01 Rust 简介

Rust 最早是 Mozilla 雇员 Graydon Hoare 的个人项目。从 2009 年开始,得到了 Mozilla 研究院的资助,2010 年项目对外公布,2010 ~ 2011 年间实现自举。自此以后,Rust 在部分重构 -> 崩溃的边缘反复横跳(历程极其艰辛),终于,在 2015 年 5 月 15 日发布 1.0 版。

Rust 是一种快速、高并发、安全且具有授权性的编程语言。

Rust 的优势:

  • 与 Go 相比,Rust 表达能力更强,性能更高,线程更安全,包管理更好
  • 与 C++ 相比,性能旗鼓相当,Rust 安全性更好
  • 与 Java 相比,Rust 性能全面领先,占用内存更小
  • 与 Python 相比,性能肯定没得说,对环境要求低,(Python 在安装依赖时总会出现各种各样的坑
  • 没有 GC(垃圾回收机制),所以是安全度极高的语言

0x02 环境搭建

基本环境:

  • macOs
  • Golang IDE
  • Golang IDE Plugins Rust(目前已经停止维护,可以使用 RustRover 或者 VS Code)
  • rustc 1.76.0 (07dca489a 2024-02-04)

在这里使用的 macOs 因此 Rust 的安装可以通过两种方式,方式一:

1
2
// 官方推荐
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

方式二:

1
2
brew install rustup-init
rustup-init

再执行完 rustup-init 后会出现安装方式

1
2
3
1) Proceed with installation (default) //默认安装
2) Customize installation //自定义安装
3) Cancel installation //取消安装

在这里选择 1 即可。
上述两种方式安装成功后,都会出现 Rust is installed now. Great! 的提示。
使用方式二安装还需要一步才算完成,需要执行

1
source $HOME/.cargo/env

使用环境变量生效。
打开终端,执行 rustc -V 会显示版本号

1
2
➜  ~ rustc -V
rustc 1.76.0 (07dca489a 2024-02-04)

出现版本号,才证明你的环境安装成功!

0x02 Rust 基础入门

Cargo

Cargo 是 Rust 的包管理器,包管理器的意义在于任何用户拿到代码都能运行起来。Cargo 在安装 Rust 时已经安装完成了,无需再次安装。

1
cargo new hello_world

cargo new 创建一个项目,项目名为 hello_world,项目的结构和配置文件都由 Cargo 生成。生成的目录结构如下:

1
2
3
4
5
tree hello_world
hello_world
├── Cargo.toml
└── src
└── main.rs

还可以使用 --lib 参数创建依赖库。创建完成后,会在 src/main.rs 看到如下代码:

1
2
3
fn main() {
println!("Hello, world!");
}

运行 Rust 代码的两种方式,方式一:

1
2
3
4
5
6
cd hello_world
cargo run
$ Compiling hello_world v0.1.0 (/Users/th4ank1s/Study/study_rust/hello_world)
$ Finished dev [unoptimized + debuginfo] target(s) in 0.19s
$ Running `target/debug/hello_world`
$ Hello, world!

方式二:
cargo build 命令编译项目,编译之后会在 ./target/debug/ 目录下生成 bin 文件。
cargo build

cargo check

cargo check 快速检查代码是否能编译通过,在项目后期代码量庞大以后可以快速检查,但是目前的大多数的编译器都可以提示语法错误

Cargo.toml 和 Cargo.lock
Cargo.tomlCargo.lock是 cargo 的核心文件

  • Cargo.toml 是 cargo 特有的项目数据描述文件,它存储了项目的所有元配置信息
  • Cargo.lock 是 cargo 工具根据统一项目的 toml 文件生成的项目依赖详细清单,一般不修改,只修改 Cargo.toml 文件。

cargo.toml 文件如下:

1
2
3
4
5
6
7
8
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

  • package 记录了项目的描述信息,name 字段定义项目名称, version 定义当前版本,edition 字段是目前使用的 Rust 大版本。
  • dependencies 用来描述各种依赖
    依赖的三种写法
    1
    2
    3
    4
    5
     [dependencies]
    rand = "0.3"
    hammer = { version = "0.5.0"}
    color = { git = "https://github.com/bjz/color-rs" }
    geometry = { path = "crates/geometry" }
    分别对应:
  • 基于 Rust 官方仓库 crates.io,通过版本说明来描述
  • 基于项目源代码的 git 仓库地址,通过 URL 来描述
  • 基于本地项目的绝对路径或者相对路径,通过类 Unix 模式的路径来描述

当然 cargo 还有其他的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Commands:
build, b Compile the current package
check, c Analyze the current package and report errors, but don't build object files
clean Remove the target directory
doc, d Build this package's and its dependencies' documentation
new Create a new cargo package
init Create a new cargo package in an existing directory
add Add dependencies to a manifest file
remove Remove dependencies from a manifest file
run, r Run a binary or example of the local package
test, t Run the tests
bench Run the benchmarks
update Update dependencies listed in Cargo.lock
search Search registry for crates
publish Package and upload this package to the registry
install Install a Rust binary. Default location is $HOME/.cargo/bin
uninstall Uninstall a Rust binary
... See all commands with --list

变量绑定与解构

首先是变量命名,基本的 Rust 命名规范在 RFC 430 中有描述,可以参考变量命名。同时命名时应该避开关键字,这些关键字分别为:

  • as - 强制类型转换,或use 和 extern crate包和模块引入语句中的重命名
  • break - 立刻退出循环
  • const - 定义常量或原生常量指针(constant raw pointer)
  • continue - 继续进入下一次循环迭代
  • crate - 链接外部包
  • dyn - 动态分发特征对象
  • else - 作为 if 和 if let 控制流结构的 fallback
  • enum - 定义一个枚举类型
  • extern - 链接一个外部包,或者一个宏变量(该变量定义在另外一个包中)
  • false - 布尔值 false
  • fn - 定义一个函数或 函数指针类型 (function pointer type)
  • for - 遍历一个迭代器或实现一个 trait 或者指定一个更高级的生命周期
  • if - 基于条件表达式的结果来执行相应的分支
  • impl - 为结构体或者特征实现具体功能
  • in - for 循环语法的一部分
  • let - 绑定一个变量
  • loop - 无条件循环
  • match - 模式匹配
  • mod - 定义一个模块
  • move - 使闭包获取其所捕获项的所有权
  • mut - 在引用、裸指针或模式绑定中使用,表明变量是可变的
  • pub - 表示结构体字段、impl 块或模块的公共可见性
  • ref - 通过引用绑定
  • return - 从函数中返回
  • Self - 实现特征类型的类型别名
  • self - 表示方法本身或当前模块
  • static - 表示全局变量或在整个程序执行期间保持其生命周期
  • struct - 定义一个结构体
  • super - 表示当前模块的父模块
  • trait - 定义一个特征
  • true - 布尔值 true
  • type - 定义一个类型别名或关联类型
  • unsafe - 表示不安全的代码、函数、特征或实现
  • use - 在当前代码范围内(模块或者花括号对)引入外部的包、模块等
  • where - 表示一个约束类型的从句
  • while - 基于一个表达式的结果判断是否继续循环

来看一段代码:

1
2
3
4
fn main() {
let a = "hello,world!";
println!("{}", study)
}

在 Rust 中定义变量的方式:

  • let 变量名 = 值
  • let 变量名:数据类型 = 值

let a = "hello,world!" 这个等式的意义把等式右边的 “hello world” 字符串赋值给变量 a,这个过程我们称之为:变量绑定。值得注意的是 Rust 的变量在默认情况下是 不可变 的。这是 Rust 的特性之一,让我们编写的代码更安全,性能更好。如果想要变量可变,需要使用 mut 关键。

由于在默认情况下变量为不可变的,因此在 Rust 中下面的代码在编译时会报错

这种错误是为了避免无法预期的错误发生在我们的变量上:一个变量往往被多处代码所使用,其中一部分代码假定该变量的值永远不会改变,而另外一部分代码却无情的改变了这个值,在实际开发过程中,这个错误是很难被发现的,特别是在多线程编程中。

正确的代码如下:

1
2
3
4
5
let mut a = "hello,world!";
println!("{}", a);

a = "world.hello!";
println!("{}", a);

添加 mut 关键,将变量 a 设置可变的。

忽略未使用的变量

在 Rust 中定义了变量但是没有使用,Rust 编译器会给你一个警告。代码如下:

1
2
3
4
5
fn main() {
let x = 3;
let y = 5;
println!("x = {}", x);
}

警告如下:
unused
如果你希望 Rust 不要警告未使用的变量,可以使用下划线为变量名开头。

1
2
3
4
5
fn main() {
let x = 3;
let _y = 5;
println!("x = {}", x);
}

将变量 y 修改成 _y在编译时 Rust 就不会警告。

解构

在 Rust 中 let 不仅用于变量的绑定,还能够进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分。

1
2
3
4
5
fn main() {
let (a, b): (bool,bool) = (true, false);
// a = true,不可变; b = false,可变
println!("a = {:?}, b = {:?}", a, b);
}

变量与常量
在 Rust 中变量不可变,会让我想起一个编程概念:常量。常量是绑定到一个常量名且不允许更改的值,如:PI=3.14。在 Rust 中变量与常量存在差异:

  • 常量不允许使用 mut 关键字,常量不仅仅默认不可变,而且自始至终不可变。
  • 在 Rust 中常量使用 const 关键字,而不是 let 关键字来声明,并且需要标注值的类型,如:const MAX_PONIT=100_000,在 Rust 中可以在数字中间插入下划线提高数字的可读性。

变量遮蔽
Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
let x = 5;
// 在作用域内 会对之前的一个 x 进行遮蔽
let x = x + 1;
println!("x = {}", x);

{
// 在当前花括号作用域内,对之前的 x 进行遮蔽
let x = 10;
println!("括号类的 x = {}", x);
}

println!("mian x = {}", x);
}

其运行结果为:

1
2
3
4
x = 6
括号类的 x = 10
mian x = 6

基本类型

Rust 中的每个值都有其确切的数据类型,大体可以分为:基本类型和复合类型。基本类型意味着它们是一个最小化原子类型,无法解构为其他类型(一般意义来说)。其组成:

  • 数值类型: 有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数
  • 字符串:字符串字面量和字符串切片 &str
  • 布尔类型: true和false
  • 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
  • 单元类型: 即 () ,其唯一的值也是 ()

Rust 是静态类型语言,也就是编译器在编译期知道所有的变量的类型,但不必为所有的变量制定类型,因此 Rust 可以根据上下文自动推导出变量的类型。但是某些情况下,它无法推导出变量类型,需要手动标注一个类型。

错误为:

1
2
let num = "33".parse().unwrap();
| ^^^ ----- type must be known at this point

由于 Rust 编辑器无法确定 "33" 是整数,浮点数还是字符串,因此我们需要为标注显式的数据类型,如:let num:i32 = ... 或者 "33".parse::<i32>()

整数类型
整数是没有小数部分的数字,在 Rsut 中 i32 类型表示有符号的 32 为整数,i 是英文档次 interger 的首字母,而 u32,代表无符号 unsigned 类型。在 Rust 中内置的整数类型:

长度 有符号 无符号
8位 i8 u8
16位 i16 u16
32位 i32 u32
64位 i64 u64
128位 i128 u128
根据架构定 isize usize

类型定义的形式统一为:有无符号 + 类型大小(位数)。无符号数表示数字只能取正数和0,而有符号则表示数字可以取正数、负数还有0。每个有符号类型规定的数字范围是 -(2n - 1) ~ 2n - 1 - 1,其中 n 是该定义形式的位长度。因此 i8 可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 u8 能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。isize 和 usize 类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。

整形字面两

数字字面两 示例
十进制 98_222
十六进制 0xff
八进制 0o77
二进制 0b1111_0000
字节(仅u8) b’A’

在 Rust 中整数类型默认使用 i32,如:let i = 100, 那么 i 就是 i32 类型。

浮点类型

浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: f32 和 f64,分别为 32 位和 64 位大小。默认浮点类型是 f64,在现代的 CPU 中它的速度与 f32 几乎相同,但精度更高。

在 Rust 中定义浮点数:

1
2
3
4
5
fn main() {
let x = 2.0; // f64

let y: f32 = 3.0; // f32
}

浮点数根据 IEEE-754 标准实现。f32 类型是单精度浮点型,f64 为双精度。

NaN
在数学上未定义的结果,如对负数取平方根,会产生一个特殊的结果:Rust 的浮点数类型使用 NaN 来处理这些结果。需要注意的是:所有跟 NaN 交互的操作,都会返回一个 NaN,而且 NaN 不能用来比较。可以是 is_nan() 方法来判断一个数值是否是 NaN。

数学运算
Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取模运算。Rust 中提供的所有运算符列表可以查看 完整运算符列表

位运算

运算符 说明
& 位与 相同位置均为1时则为1,否则为0
| 位或 相同位置只要有1时则为1,否则为0
^ 异或 相同位置不相同则为1,相同则为0
! 位非 把位中的0和1相互取反,即0置为1,1置为0
<< 左移 所有位向左移动指定位数,右位补0
>> 右移 所有位向右移动指定位数,带符号移动(正数补0,负数补1)

序列
在 Rust 中生成连续的数值,例如: 1..5, 生成从 1 到 4 的连续数字,它的区间是左闭右开的。如果想生成从 1 到 5包含5,则使用 1..=5。如:

1
2
3
for item in 1..=5 {
prinln!("{}", item);
}

其输出为:

1
2
3
4
5
1
2
3
4
5

有理数和复数
在 Rust 中有理数和无理数并未包含在标准库中,

  • 有理数和复数
  • 任意大小的整数和任意精度的浮点数
  • 固定精度的十进制小数,常用于货币相关的场景

由社区开发出了高质量的 Rust 数值库: num

整数溢出和浮点数陷阱

假设有一个 u8 ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生整型溢出。

在处理整数溢出时,Rust 有一个有趣的规则既:在 debug 模式下编译时,Rust 会检查整数溢出,同时会 panic;在使用 --release 参数进行 release 模式编译时 Rust 不会检查整数溢出,在检测到溢出时,Rust 会根据补码循环溢出规则来处理,如:在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。

而浮点数陷阱,是由于浮点数底层格式的特殊性,导致如果在使用浮点数不够谨慎,就可能造成危险,主要由两个原因:

  • 浮点数往往是你想要表达数字的近似值。
  • 浮点数在某些特性上是反直觉的。

如:

1
2
3
4
fn main() {
// 断言0.1 + 0.2与0.3相等
assert!(0.1 + 0.2 == 0.3);
}

上面这段代码在运行时会 panic,

因为二进制精度问题,导致了 0.1 + 0.2 并不严格等于 0.3,它们可能在小数点 N 位后存在误差。

字符

Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。Unicode 值的范围从 U+0000 ~ U+D7FF 和 U+E000 ~ U+10FFFF。
由于 Unicode 都是 4 个字节编码,因此字符类型也是占用 4 个字节:

1
2
3
4
fn main() {
let x = '中';
println!("字符'中'占用了{}字节的内存大小",std::mem::size_of_val(&x));
}

运行结果:

布尔
Rust 中的布尔类型有两个可能的值:true 和 false,布尔值占用内存的大小为 1 个字节。

单元类型
单元类型就是 (),唯一的值也是 ()

语句

1
2
3
let a = 8;
let b: Vec<f64> = Vec::new();
let (a, c) = ("hi", false);

以上都是语句,它们完成了一个具体的操作,但是并没有返回值,因此是语句。注意:

1
let b = (let a = 8);

上面的语句在 Rust 中错误的,因为 let 是语句,因此不能将 let 语句赋值给其他值。

表达式
表达式会进行求值,然后返回一个值。例如 5 + 6,在求值后,返回值 11,因此它就是一条表达式。

调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式。值的注意的是:
表达式不能包含分号。表达式加上了分号,它就变成了语句,不会返回一个值。

函数

  • 函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}
  • 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
  • 每个函数参数都需要标注类型

声明一个函数,如:

1
2
3
fn add(i: i32, j: i32) -> i32 {
i + j
}

在 Rust 中声明函数的关键字为 fn, add() 为函数名,参数为 i 和 j, 它们的类型都是 i32,且返回值也是 i32 类型。Rust 是强类型语言,因此在声明函数的时候,函数的参数必须表明参数的类型,参数都需要标明类型,否则会报错。如:

1
2
3
fn add(i: i32, j) -> i32 {
x + y
}

上述函数中 y 没有标明类型,因此在编译的时候会报错:

函数的返回值,由于在 Rust 中函数就是表达式,因此我们可以把函数的返回值直接赋给调用者。函数的返回值就是函数体最后一条表达式的返回值,我们也可以使用 return 关键字返回。如:

1
2
3
4
5
6
7
fn add(x:i32, y:i32) -> i32{
x + y
}

fn add1(x:i32, y:i32) -> i32{
return x + y;
}

函数 add 和 add1 都会将执行的结果返回给调用者。

特殊返回类型

  1. 无返回值()

在 Rust 中函数没有返回值时,会返回一个 ()() 是之前提到的单元类型。单元类型是一个零长度的元组,没啥作用。但是它可以用来表达一个函数没有返回值:

  • 函数没有返回值,那么返回一个 ()
  • 通过 ; 结尾的语句返回一个 ()

需要注意的是:只有表达式能返回值,而 ; 结尾的是语句。

  1. 用不返回的发散函数

当用 ! 作函数返回类型的时候,表示该函数永不返回( diverge function )。

Ref

Rust Course