Rust 从 0 到 1 (环境搭建,基础类型)
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 | // 官方推荐 |
方式二:
1 | brew install rustup-init |
再执行完 rustup-init
后会出现安装方式
1 | 1) Proceed with installation (default) //默认安装 |
在这里选择 1 即可。
上述两种方式安装成功后,都会出现 Rust is installed now. Great!
的提示。
使用方式二安装还需要一步才算完成,需要执行
1 | source $HOME/.cargo/env |
使用环境变量生效。
打开终端,执行 rustc -V
会显示版本号
1 | ➜ ~ rustc -V |
出现版本号,才证明你的环境安装成功!
0x02 Rust 基础入门
Cargo
Cargo 是 Rust 的包管理器,包管理器的意义在于任何用户拿到代码都能运行起来。Cargo 在安装 Rust 时已经安装完成了,无需再次安装。
1 | cargo new hello_world |
cargo new
创建一个项目,项目名为 hello_world
,项目的结构和配置文件都由 Cargo
生成。生成的目录结构如下:
1 | tree hello_world |
还可以使用 --lib
参数创建依赖库。创建完成后,会在 src/main.rs
看到如下代码:
1 | fn main() { |
运行 Rust 代码的两种方式,方式一:
1 | cd hello_world |
方式二:cargo build
命令编译项目,编译之后会在 ./target/debug/
目录下生成 bin 文件。
cargo check
cargo check 快速检查代码是否能编译通过,在项目后期代码量庞大以后可以快速检查,但是目前的大多数的编译器都可以提示语法错误
Cargo.toml 和 Cargo.lockCargo.toml
和 Cargo.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 | Commands: |
变量绑定与解构
首先是变量命名,基本的 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 | fn main() { |
在 Rust 中定义变量的方式:
- let 变量名 = 值
- let 变量名:数据类型 = 值
let a = "hello,world!"
这个等式的意义把等式右边的 “hello world” 字符串赋值给变量 a,这个过程我们称之为:变量绑定。值得注意的是 Rust 的变量在默认情况下是不可变
的。这是 Rust 的特性之一,让我们编写的代码更安全,性能更好。如果想要变量可变,需要使用mut
关键。
由于在默认情况下变量为不可变的,因此在 Rust 中下面的代码在编译时会报错
这种错误是为了避免无法预期的错误发生在我们的变量上:一个变量往往被多处代码所使用,其中一部分代码假定该变量的值永远不会改变,而另外一部分代码却无情的改变了这个值,在实际开发过程中,这个错误是很难被发现的,特别是在多线程编程中。
正确的代码如下:
1 | let mut a = "hello,world!"; |
添加 mut 关键,将变量 a 设置可变的。
忽略未使用的变量
在 Rust 中定义了变量但是没有使用,Rust 编译器会给你一个警告。代码如下:
1 | fn main() { |
警告如下:
如果你希望 Rust 不要警告未使用的变量,可以使用下划线为变量名开头。
1 | fn main() { |
将变量 y 修改成 _y
在编译时 Rust 就不会警告。
解构
在 Rust 中 let
不仅用于变量的绑定,还能够进行复杂变量的解构:从一个相对复杂的变量中,匹配出该变量的一部分。
1 | fn main() { |
变量与常量
在 Rust 中变量不可变,会让我想起一个编程概念:常量。常量是绑定到一个常量名且不允许更改的值,如:PI=3.14
。在 Rust 中变量与常量存在差异:
- 常量不允许使用 mut 关键字,常量不仅仅默认不可变,而且自始至终不可变。
- 在 Rust 中常量使用
const
关键字,而不是 let 关键字来声明,并且需要标注值的类型,如:const MAX_PONIT=100_000
,在 Rust 中可以在数字中间插入下划线提高数字的可读性。
变量遮蔽
Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的。如:
1 | fn main() { |
其运行结果为:
1 | x = 6 |
基本类型
Rust 中的每个值都有其确切的数据类型,大体可以分为:基本类型和复合类型。基本类型意味着它们是一个最小化原子类型,无法解构为其他类型(一般意义来说)。其组成:
- 数值类型: 有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数
- 字符串:字符串字面量和字符串切片 &str
- 布尔类型: true和false
- 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
- 单元类型: 即 () ,其唯一的值也是 ()
Rust 是静态类型语言,也就是编译器在编译期知道所有的变量的类型,但不必为所有的变量制定类型,因此 Rust 可以根据上下文自动推导出变量的类型。但是某些情况下,它无法推导出变量类型,需要手动标注一个类型。
错误为:
1
2let 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 | fn main() { |
浮点数根据 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 | for item in 1..=5 { |
其输出为:
1 | 1 |
有理数和复数
在 Rust 中有理数和无理数并未包含在标准库中,
- 有理数和复数
- 任意大小的整数和任意精度的浮点数
- 固定精度的十进制小数,常用于货币相关的场景
由社区开发出了高质量的 Rust 数值库: num。
整数溢出和浮点数陷阱
假设有一个 u8 ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生整型溢出。
在处理整数溢出时,Rust 有一个有趣的规则既:在 debug 模式下编译时,Rust 会检查整数溢出,同时会 panic;在使用 --release
参数进行 release
模式编译时 Rust 不会检查整数溢出,在检测到溢出时,Rust 会根据补码循环溢出规则来处理,如:在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。
而浮点数陷阱,是由于浮点数底层格式的特殊性,导致如果在使用浮点数不够谨慎,就可能造成危险,主要由两个原因:
- 浮点数往往是你想要表达数字的近似值。
- 浮点数在某些特性上是反直觉的。
如:
1 | fn main() { |
上面这段代码在运行时会 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 | fn main() { |
运行结果:
布尔
Rust 中的布尔类型有两个可能的值:true 和 false,布尔值占用内存的大小为 1 个字节。
单元类型
单元类型就是 ()
,唯一的值也是 ()
。
语句
1 | let a = 8; |
以上都是语句,它们完成了一个具体的操作,但是并没有返回值,因此是语句。注意:
1 | let b = (let a = 8); |
上面的语句在 Rust 中错误的,因为 let 是语句,因此不能将 let 语句赋值给其他值。
表达式
表达式会进行求值,然后返回一个值。例如 5 + 6,在求值后,返回值 11,因此它就是一条表达式。
调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式。值的注意的是:
表达式不能包含分号。表达式加上了分号,它就变成了语句,不会返回一个值。
函数
- 函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}
- 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
- 每个函数参数都需要标注类型
声明一个函数,如:
1 | fn add(i: i32, j: i32) -> i32 { |
在 Rust 中声明函数的关键字为 fn
, add()
为函数名,参数为 i 和 j, 它们的类型都是 i32,且返回值也是 i32 类型。Rust 是强类型语言,因此在声明函数的时候,函数的参数必须表明参数的类型,参数都需要标明类型,否则会报错。如:
1 | fn add(i: i32, j) -> i32 { |
上述函数中 y 没有标明类型,因此在编译的时候会报错:
函数的返回值,由于在 Rust 中函数就是表达式,因此我们可以把函数的返回值直接赋给调用者。函数的返回值就是函数体最后一条表达式的返回值,我们也可以使用 return 关键字返回。如:
1 | fn add(x:i32, y:i32) -> i32{ |
函数 add 和 add1 都会将执行的结果返回给调用者。
特殊返回类型
- 无返回值()
在 Rust 中函数没有返回值时,会返回一个 ()
, ()
是之前提到的单元类型。单元类型是一个零长度的元组,没啥作用。但是它可以用来表达一个函数没有返回值:
- 函数没有返回值,那么返回一个 ()
- 通过 ; 结尾的语句返回一个 ()
需要注意的是:只有表达式能返回值,而 ; 结尾的是语句。
- 用不返回的发散函数
当用 ! 作函数返回类型的时候,表示该函数永不返回( diverge function )。