Rust 编程语言入门教程
1. 第一章: Rust 简介
1.1. 为什么要用 Rust
Rust 是一种令人兴奋的新编程语言, 它可以让每一个人编写可靠且高效的软件.
它可以用来替换 C/C++, Rust 和他们具有同样的性能, 但是很多常见的 bug 在编译时就可以被消灭.
Rust 是一种通用的编程语言, 但是它更善于以下场景:
需要运行时的速度
需要内存安全
更好的利用多处理器
1.2. 与其他语言比较
C/C++ 性能非常好, 但类型系统和内存都不太安全.
JAVA/C#, 拥有 GC, 能保证内存安全, 也有很多优秀特性, 但是性能不行
RUST:
安全
无需 GC
易于维护, 调试, 代码安全高效
1.3. Rust 特别擅长的领域
高性能 webservice
webassembly
命令行工具
网络编程
嵌入式设备
系统编程
1.4. Rust 与 Firefox
Rust 最初是 Mozilla 公司的一个研究性项目, firefox 是 Rust 产品应用的一个重要例子.Mozilla 一直以来都在用 rust 创建一个名为 servo 的实验性浏览器引擎, 其中的所有内容都是并行执行的.
目前 servo 的部分功能已经被集成到 firefox 里面了
firefox 原来的量子版就包含了 servo 的 css 渲染引擎rust 使得 firefox 在这方便得到了巨大的性能改进
1.5. Rust 的用户和案例
- google: 新操作系统 Fuschia, 其中 Rust 代码量大约 30%
- Amazon: 基于 Linux 开发的直接可以在裸机, 虚拟机上运行容器的操作系统.
- System76: 纯 Rust 开发了下一代安全操作系统 Redox
- 蚂蚁金服: 库操作系统 Occlum
- 斯坦福和密歇根大学: 嵌入式实时操作系统, 应用于 google 的加密产品.
- 微软: 正在使用 Rust 重写 windows 系统中的一些低级组件.
- 微软: winRT/Rust 项目
- Dropbox, yelp, Coursera, LINE, Cloudflare, Atlassian, npm, Ceph, 百度, 华为, Sentry, Deno
1.6. Rust 的优点
- 性能
- 安全性
- 无所畏惧的并发
1.7. Rust 的缺点
- 难学
1.8. 注意
Rust 有很多独有的概念, 它们和现在大多主流语言都不同.
- 所以学习 Rust 必须从基础概念一步一步学, 否则会懵.
1.9. 参考教材
The Rust programming language
Rust 权威指南
2. 安装 Rust
2.1. 安装 rust
- Rust 官网: https://www.rust-lang.org
- Linux or Mac:
1 | curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf| sh |
windows: 按官网安装指引操作
下载安装程序,下载 RUSTUP-INIT.exe 32 位安装程序 或 下载 RUSTUP-INIT.exe 64 位安装程序
然后运行程序并按照屏幕上的说明进行操作。
当提示您安装Visual Studio C++ build tools时,您可能需要安装这些工具。
完成安装后需要将 <home_dir>/bin/.cargo 添加到 Path 环境变量, 这样在任何位置都可以执行 rustc, cargo 和 rustup 命令了
windows subsystem for Linux:
搭建 WSL 可参考我的博客
Windows 下搭建 WSL Linux 开发环境安装 rust 可使用如下命令命令
1
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
2.2. 配置 Rust
2.2.1. 替换镜像
安装完成后需要将 crates.io 替换成国内镜像.
访问https://crates.io非常缓慢,github 的仓库也经常不能访问,建议大家切换到国内镜像站。镜像站实时缓存,托管在码云的 gitee 仓库每隔 30 分钟与 github 同步。
配置方式
- 在 .cargo 目录新建文件 config
注意:新安装的电脑中没有这个文件,需要手动创建 config 文件,并且该文件没有后缀名
编辑文件内容:
1 | [source.crates-io] |
如果使用中科大 USTC 的镜像
1 |
|
修改完保存后执行 build 即可
1 | cargo build |
2.3. 更新 Rust
1 | rustup update |
2.4. 卸载 Rust
1 | rustup self uninstall |
2.5. 验证安装是否成功
1 | rustc --version |
- 结果格式: rustc x.y. z (abcabcabc yyyy-mm-dd)
- 会显示最新稳定版的: 版本号, commit hash, commit 日期
- 当前我安装的版本为 rustc 1.56.1 (59eed8a2a 2021-11-01)
3. 第二章:第一个 rust 程序
3.1. hello world
main.rs
1 | fn main() { |
编译:
1 | rustc main.rs |
编译完成后将会输出可执行文件 main,
运行程序使用命令
1 | ./main |
3.2. Rust 程序解析
- 定义函数 fn main() {}
没有参数, 没有返回值 - main 函数很特别 : 他是每一个 Rust 可执行程序 最先运行的代码
- 打印文本: println!(“hello, world!”)
- rust 的缩进是 4 个空格而不是 tab
- println! 是一个 Rust macro(宏), 如果是函数的话就没有感叹号(!)
- “hello world” 是字符串, 它是 println!的参数
- 这行代码以;结尾
3.3. 编译和运行是单独的两步
运行 rust 程序之前需要先编译, 命令为: rustc 源文件名
1
2
3
4# 编译程序
rustc main.rs
# 运行程序
./main编译成功后, 会生成一个二进制文件例如 main
- 在 window 上还会生成一个.pdb 文件, 里面包含调试信息
Rust 是 ahead-of-time(AOT)预编译的语言
- 可以先编译程序, 然后把可执行文件交给别人运行(无需安装 Rust)
rustc 只适合简单的 Rust 程序, 当文件比较多, 项目管理还是需要使用 Cargo
3.4. hello cargo
3.4.1. cargo 介绍
- Cargo 是 Rust 的构建系统和包管理工具
- 构建代码, 下载依赖的库, 构建这些库
- 安装 Rust 的同时会自动安装 Cargo, 所以这里不需要额外的步骤去安装 Cargo
1 | $cargo --version |
3.4.2. 使用 Cargo 创建项目
创建项目:
1
cargo new hello_cargo
创建完成后项目的目录结构
1
2
3
4
5
6
7
8
9
10
11
12$tree -a .
hello_cargo
├── Cargo.toml
├── .git
│ ├── config
│ ├── description
│ ├── HEAD
...................
├── .gitignore
└── src
└── main.rs目录结构说明:
Cargo.toml:- TOML(Tom’s obvious, Minimal Language)格式,是 Cargo 的配置格式
1
2
3
4
5
6
7
8[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies][package], 是一个项目信息区域,表示下方内容是用来配置包(package)的.
- name: 项目名
- version: 项目版本
- authors: 项目作者
- edition: The Rust edition
- 更多参数说明请参考The Manifest Format
[dependencies], 另一个区域的开始,它会列出项目的依赖项.
在 Rust 里面,代码的包称作crate(板条箱,装货箱;如果cargo是装货物的集装箱,Crate 板条箱就是装载整批货物的小箱子)
src/main.rs
- cargo 生成的 main.rs 在 src 目录下
- 而 Cargo.toml 在项目顶层下
- 源代码都应该在 src 目录下
- 顶层目录可以放置:README, 许可信息, 配置文件和其它与程序源代码无关的文件
- 如果创建项目时没有使用 cargo, 也可以把项目转化为使用 cargo:
- 把源代码文件移动到 src 下
- 创建 Cargo.toml 并填写相应的配置
3.4.3. 构建 Cargo 项目
- cargo build
创建可执行文件:target/debug/hello_cargo (linux/macos)或者 target\debug\hello_cargo.exe (windows) - 第一次运行 cargo build 会在顶层目录生成 cargo.lock 文件
- 该文件负责追踪项目依赖的精确版本
- 不需要手动修改该文件
- 构建和运行 cargo 项目
- cargo run
- cargo run, 编译代码+执行结果
- 如果之前编译成功过,并且源代码没有改变,那么就会直接运行二进制文件
- cargo run
- cargo check
- cargo check, 检查代码,确保能通过编译,但是不产生任何可执行文件
- cargo buid -release
- 编译时会进行优化
- 代码会运行的更快, 但是编译时间更长
- 会在 target/release 而不是 target/debug 生成可执行文件
- 两种配置,一种是开发用的,一种是发布用的
3.5. 猜数字游戏
3.5.1. 声明变量
main.rs
1 | println!("猜数!"); |
3.5.2. 引入依赖包
- rust 中的依赖包被称为 crate
crate 分为两种, 一种是二进制格式的可执行文件; 一种是源文件,也被称为 library crate - rust 的 crate 仓库为 crates.io, 可以访问该网站获得相应的 crate
- cargo 引入依赖的方式
- 在 dependencies 区域添加依赖的 crate 名称和版本如下所示.
1 | #Cargo.toml |
- 示例代码
1 | use std::io; |
4. 第三章 通用的编程概念
4.1. 变量和可变性
声明变量使用 let 关键字
默认情况下,变量是不可变的(immutable)
例如:1
2
3
4
5let x =5; // x 为不可变变量
println!("the value of x is {}", x);
x = 6; // 注意: 这里会有编译错误
println!("the value of x is {}", x);声明变量时, 在变量前面加上 mut, 就可以使变量可变.
1
2
3
4let mut x =5;
println!("the value of x is {}", x);
x = 6;
println!("the value of x is {}", x);
4.2. 变量和常量
常量(constant), 常量在绑定值以后也是不可变的, 但是它与不可变的变量有很多区别:
- 不可以使用 mut, 常量永远都是不可变的
- 声明常量使用 const 关键字, 它的类型必须被标注
- 常量可以在任何作用域内进行声明, 包括全局作用域
- 常量只可以绑定到常量表达式, 无法绑定到函数的调用结果或只能在运行时才能计算出的值
在程序运行期间, 常量在其声明的作用域内一直有效
命名规范: Rust 里常量使用全大写字母, 每个单词之间用下划线分开, 例如:
1
const MAX_POINTS: u32 = 100_000;
4.3. shadowing (隐藏)
可以使用相同的名字声明新的变量, 新的变量就会 shadow(隐藏)之前声明的同名变量
在后续代码中这个变量名代表的就是新的变量
shadow 和把变量标记为 mut 是不一样的
例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17fn main() {
//定义不可变变量x
let x = 5;
println!("the value of x is {}", x);
// x = 6 // 如果这里给x赋值会报错
// 但是如果我们声明一个同名的新的变量,就可以编译通过
// 我们甚至可以改变x的数据类型, 甚至可以定义新的同名但是不同的可变性的变量
//新的同名变量
let x = 6;
println!("the value of x is {}", x);
//不同可变性的同名变量
let mut x = "my love";
println!("the value of x is {}", x);
x = "hello kitty";
println!("the value of x is {}", x);
}
4.4. Rust 数据类型
Rust 是静态编译语言, 在编译时必须知道变量的类型
基于使用的值, 编译器通常能够推断出它的具体类型
但如果可能的类型比较多(例如把 String 转为整数的 parse 方法),就必须添加类型的标注,否则编会报错.
例如:1
2let guess: u32 = "42".parse().expect("not a number")
println!("{}", guess)
4.4.1. 标量类型
- 一个标量类型代表一个单个的值
- Rust 有四个主要的标量类型:
- 整数类型
- 浮点类型
- 布尔类型
- 字符类型
4.4.1.1. 整数类型
整数类型分为无符合整数类型,
- 无符号整数类型以u开头
- 有符号整数类型以 i 开头
Rust 的整数类型列表如图:
- 每种长度都有对应的有符号型和无符号型.
有符号范围
- (负的 2 的 n-1 次方-1) 到(2 的 n-1 次方-1)
无符号范围
- 0 到 2 的 n 次方 -1
isize 和 usize 类型
isize 和 usize 类型的位数由程序运行的计算机的架构所决定
如果是 64 位的计算机,那就是 64 位的
如果是 32 位的计算机,那就是 32 位的
使用 isize 或者 usize 的场景是对某种集合进行索引操作
length signed unsigned 8-bit i8 u8 16-bit i16 u16 32-bit i32 u32 64-bit i64 i64 128-bit i128 u128 arch isize usize 整数的字面值
除了 byte 类型外, 所有的数值字面值都允许使用类型后缀
例如: 57u8: 值为 57 类型为 u8整数的默认类型就是 i32:
- 总体来说速度很快, 即使在 64 位系统中
NumberLiteral Example Decimal Oxff Hex Oo77 Binary Ob1111_0000 Byte (u8 only) b’A’
整数溢出
例如:u8 的范围是 0-255, 如果你把一个 u8 变量的值设为 256, 那么:- 调试模式下编译:rust 会检查整数溢出, 如果发生溢出, 程序编译时就会 panic
- 在发布模式下(–release)编译:rust 不会检查可能导致 panic 的整数溢出
- 在这种模式下如果发生溢出:rust 会执行环绕操作
256 变成 0, 257 变成 1…. 但是不会导致 panic
- 在这种模式下如果发生溢出:rust 会执行环绕操作
4.5. 浮点类型
rust 有两种基础的浮点类型,也就是含有小数部分的类型
- f32, 32 位, 单精度
- f64, 64 位,双精度
rust 的浮点类型使用了 IEEE-754 标准来表述
f64 是默认类型,因为在现代 cpu 上 f64 和 32 的运算速度差不多,而且精度更高
例子:1
2let x = 2.0 //默认为f64
let y: f32 = 3.0; //f32
4.6. 数值操作
1 | let sum = 5+10; //i32 |
4.7. 布尔类型
Rust 的布尔类型有两个值: true 和 false
占用一个字节(Byte)的大小
符号是 bool
例子
1
2
3
4
5
6
7fn main() {
//编译器推断类型
let t = true;
//显示指定类型
let s: bool = false;
}
4.8. 字符类型
rust 语言中 char 类型被用来描述语言中最基础的单个字符.
字符类型的字面值使用单引号
占用4字节大小
是 Unicode 标量值, 可以表示比 ASCII 多得多的字符内容:拼音, 中日文, 零长度空白字符,emoji 表情等.
- 其范围为
- U+0000 到 U+D7FF
- U+E000 到 U+10FFFF
- 但是 unicode 中并没有字符的概念, 所以直觉上认为的字符也许与 Rust 中的概念并不相符
4.9. 复合类型
- 复合类型可以将多个值放在一个类型里
- Rust 提供了两种基础的复合类型: 元组(Tuple), 数组
4.9.1. Tuple
- Tuple 可以将多个类型的多个值放在一个类型里
- Tuple 的长度是固定的: 一旦声明就无法改变
4.9.1.1. 创建 tuple
- 在小括号里, 将值用逗号分开
- Tuple 中的每个位置都对应一个类型,tuple 中各元素的类型不必相同
- 实例
1 | let tup: (i32, f64, u8) = (500, 6.4, 1) |
4.9.1.2. 获取 tuple 的元素值
- 可以使用模式匹配来解构(destructure)一个 Tuple 来获取元素的值
- 例子
1 | let tup: (i32, f64, u8) = (500, 6.4, 1); |
4.9.2. 访问 tuple 的元素
- 在 tuple 变量使用点标记法,后接元素的索引号
- 实例
1 | let tup: (i32, f64, u8) = (500, 6.4, 1) |
4.10. 数组
- 数组也可以将多个值放在一个类型里
- 数组中每个元素的类型必须相同
- 数组的长度也是固定的
4.10.1. 声明一个数组
在中括号里, 各值用逗号分开
例子
1
2
3fn main() {
let a = [1, 2, 3, 4, 5]
}
4.10.2. 数组的用处
如果想让你的数据存放在栈(stack)上, 而不是堆(heap)上,或者想保证有固定数量的元素, 这时使用数组更有好处.
数组没有 Vector 灵活(以后再讲)
- Vector 和数组类似,它是由标准库提供的
- Vector 的长度是可以改变的
- 如果你不确定应该使用数组还是 vector, 那么估计你应该用 vector.
例子
1
2
3
4
5
6
7fn main() {
let months = ["January",
"Fabruary",
......
"December"
]
}
4.10.3. 数组的类型
- 数组的类型以这种形式来表示: [类型; 长度]
- 例如: let a: [i32; 5] = [1, 2, 3, 4, 5];
- 另外一种声明数组的方法
如果数组的没一个元素值都相同, 那么可以在:
在中括号里指定初始值
然后是一个;
最后是数组的长度
例如: let a = [3; 5]; 它相当于:let a = [3, 3, 3, 3, 3]
4.10.4. 访问数组的元素
数组是 stack 上分配的单个块的内存
可以使用索引来访问数组的元素
例子
1
2let first = months[0];
let second = months[1];如果访问的索引超出了数组的范围, 那么
- 编译会通过
- 运行会报错(runtime 时会 panic) Rust 不会允许其继续访问相应地址的内存
说明:
在简单的情况下,编译会报错,但是在复杂情况下,编译不会报错
例如
1
2let index = 15;
let month = months[index]; //此时编译会报错1
2let index = [15, 1, 2 , 3];
let month = months[index[0]]; //此时编译不会报错, 运行时会发生panic
4.11. 3.4 函数
声明函数使用 fn 关键字
依照惯例,针对函数和变量名,rust 使用 snake case 命名规范:
所有的字母都是小写的, 单词之间使用下划线分开
例子
1
2
3
4
5
6
7
8fn main() {
println!("hello world");
another_function();
}
fn another_function() {
println!("another function")
}
4.11.1. 函数的参数
parameters, arguments
例子
1
2
3
4
5
6
7
8fn main() {
println!("hello world");
another_function(5);
}
fn another_function(x: i32) {
println!("the value of x is {}", x)
}
4.11.2. 函数中的语句(statement)和表达式(expression)
- 函数体由一系列语句组成, 可选的由一个表达式结束
- Rust 是一个基于表达式的语言
- 语句是执行一些动作的指令
- 表达式会计算产生一个值
- 函数的定义也是语句
- 语句不返回值,所以不可以使用 let 将一个语句-赋给一个变量
例子
1 | fn main() { |
4.11.3. 函数的返回值
- 在->符号后边声明函数返回值的类型, 但是不可以为返回值命名
- 在 Rust 里面, 返回值就是函数体里面最后一个表达式的值
- 若想提前返回, 需使用 return 关键字, 并指定一个值
- 大多数函数都是默认使用最后一个表达式为返回值
例子:
- 大多数函数都是默认使用最后一个表达式为返回值
1 | fn five() -> i32 { |
4.12. 注释
单行注释
多行注释
文档注释
4.13. 控制流
4.13.1. i f 表达式
if 表达式允许你根据条件来执行不同的代码分支
- 这个条件必须是 bool 类型
if 表达式中, 与条件相关联的代码块就叫做分支(arm)
可选的, 在后面可以加上一个 else 表达式
但是如果使用了多余一个 else if, 那么最好使用 match 来重构代码
例子
1
2
3
4
5
6
7
8fn main() {
let number = 3;
if number < 5 {
println!("condition was false");
} else {
println!("condition was false");
}
}例子:else if
1
2
3
4
5
6
7
8
9
10fn main() {
let number = 3;
if number % 4 == 0 {
println!("number is divisaible by 4");
} else if number % 3 == 0 {
println!("number is divisaible by 3");
} else {
println!("number is not divisaible by 3 or 4");
}
}
4.13.2. 在 let 语句中使用 if
因为 if 是一个表达式, 所以可以将它放在 let 语句中等号的右边(例子)
1
2
3
4
5fn main() {
let condition = ture;
let muber = if condition {5} else {6}; //相对于其它语言中的三元表达式
println!("The value of number is {}", number);
}
4.14. Rust 的循环
- Rust 提供了3中循环:loop, while 和 for.
4.14.1. loop 循环
loop 关键字告诉 Rust 反复地执行一块代码,直到你喊停为止
可以在 loop 循环中使用 break 关键字来告诉程序何时停止循环
例子
1 | fn main() { |
4.14.2. while 条件循环
另外一种常见的循环模式是每次执行循环体之前都判断一次条件.
while 条件循环就是为这种模式而生的
例子
1
2
3
4
5
6
7
8
9fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number -1;
}
println!("LIFTOFF!!!");
}
4.15. for 循环遍历集合
可以使用 while 或 loop 来遍历数组, 但是易出错且低效.
使用 for 循环更简洁紧凑, 它可以针对集合中的每一个元素来执行一些代码
例子
1
2
3
4
5
6fn main() {
let a = [10, 20, 30, 40, 50]
for element in a.iter() {
println!("the value is: {}", element);
}
}由于 for 循环的安全,简洁性,所以它在 Rust 里用的最多
4.16. Range
标准库提供
指定一个开始数字和一个结束数字,Range 可以生成它们之间的数字(不包含介绍)
rev 方法可以反转 range
例子
1
2
3
4
5
6fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!");
}
5. 第四章: 所有权
5.1. 4.1 什么是所有权
- Rust 的核心特性就是所有权
- 所有程序在运行时都必须管理它们使用计算机内存的方式
- 有些语言有垃圾收集机制,在程序运行时, 它们不断地寻找不再使用的内存
- 在其他语言中,程序员必须显示地分配和释放内存
- Rust 采用了第三种方式
- 内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则.
- 当程序运行时,所有权特性不会减慢程序运行速度
5.2. Stack vs Heap
- Stack 按值的接收顺序来存储,按相反的顺序将它们移除(后进先出, LIFO)
- 添加数据叫做压入栈
- 移除数据叫做弹出栈
- 所有存储在 stack 上的数据必须拥有已知的固定的大小
- 编译时大小未知的数据或运行时大小可能变化的数据必须存放在 heap 上
- Heap 内存组织性差一些
- 当你把数据放入 heap 时, 你会请求一定数量的空间
- 操作系统在 heap 里找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址
- 这个过程叫做在 heap 上进行分配, 有时仅仅称为分配
- 把值压到 stack 上不叫分配
- 因为指针是已知固定大小的, 可以把指针存放在 stack 上.
但如果想要实际数据,你必须使用指针来定位 - 把数据压倒 stack 上要比在 heap 上分配快得多:
- 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远在 stack 的顶端
- 在 heap 上分配空间需要做更多的工作:
- 操作系统首先需要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配
- 访问 heap 中的数据要比访问 stack 中的数据慢,因为需要通过指针才能找到 heap 中的数据
- 对于现代的处理器来说, 由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快.
- 如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(stack 上)
- 如果数据之间的距离比较远,那么处理速度就会慢一些(heap 上)
- 在 heap 上分配大量的空间也是需要时间的
- 当你的代码调用函数时,值被传入到函数(也包括指向 heap 的指针).函数本地的变量被压到 stack 上,当函数结束后,这些值会从 stack 上弹出.
5.3. 所有权存在的原因
- 所有权解决的问题
- 跟踪代码的哪些部分正在使用 heap 的哪些数据
- 最小化 heap 上的重复数据量
- 清理 heap 上未使用的数据以避免空间不足
- 一旦你懂得了所有权,那么就不需要经常去想 stack 或 heap 了
- 但是知道管理 heap 数据是所有权存在的原因,这有助于解释它为什么会这样工作.
5.4. 所有权,内存与分配
5.4.1. 所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域的时候,该值将被删除
5.5. 变量作用域
scope 就是程序中一个项目的有效范围
例子
1
2
3
4
5fn main() {
//s 不可以
let s = "hello"; // s可用
//可以对s进行相关操作
} //s 作用域到此结束,s不再可用
5.5.1. string 类型
- String 比那些基础标量数据类型更复杂
- 字符串字面值: 程序里手写的那些字符串值.它们是不可变的
- Rust 还有第二种字符串类型: String
- 在 heap 上分配,能够存储在编译时未知数量的文本
5.5.1.1. 创建 String 类型的值
- 可以使用 from 函数从字符串字面值创建出 String 类型
1 | let s = String::from("hello");// :: 表示from是String类型下的函数 |
这类字符串是可以被修改的
1 | fn main() { |
- 字符串字面值,在编译时就知道它的内容, 其文本内容直接被编码到最终的可执行文件里
- 速度快,高效,得益于其不可变性
- String 类型,为了支持可变性, 需要在 heap 上分配内存来保存编译时未知的文本内容:
- 操作系统必须在运行时来请求内存
- 这步通过调用 String::from 来实现
- 当用完 String 之后,需要使用某种方式将内存返回给操作系统
- 这步,在拥有 GC 的语言中,GC 会跟踪并清理不再使用的内存
- 没有 GC 的语言中,就需要我们去识别内存何时不再使用,并调用代码将它释放
- 如果忘了,那就浪费内存.
- 如果提前做了,变量就会非法
- 如果做了两次,也是 bug. 必须一次分配对应一次释放
- rust 采用了不同的方式: 对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的归还给操作系统.
- drop 函数
5.5.2. 变量和数据交互的方式: 移动 move
多个变量可以与同一个数据使用一种独特的方式来交互
1
2let x = 5;
let y = x;整数是已知且固定大小的简单的值, 这两个5被压到了 stack 中
1
2let s1 = String::from("hello");
let s2 = s1;一个 String 由3部分组成:
- 一个指向存放字符串内容的内存的指针
- 一个长度
- 一个容量
上面这些东西被放在 stack 上.
存放字符串内容的部分在 heap 上
长度 len, 就是存放字符串内容所需的字节数
当把 s1 赋值给 S2, String 的数据被复制了一份
在 stack 上复制了一份指针, 长度, 容量
并没有复制指针所指向的 heap 上的数据
当变量离开作用域时,Rust 会自动调用 Drop 函数,并将变量使用的 heap 内存释放.
当 S1, S2 离开作用域时, 它们都会尝试释放相同的内存二次释放(double free) bug
为了保证内存安全:Rust 没有尝试复制被分配的内存
Rust 让 s1 失效
当 s1 离开作用域的时候, rust 不需要释放任何东西
试试看当 s2 创建后,再使用 s1 是什么效果
1
2
3
4
5fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); //这里会有编译错误,这里s1已经失效了
}- 浅拷贝(shallow copy)
- 深拷贝(deep copy)
- 你也许会将复制, 指针, 长度, 容量视为浅拷贝, 但是由于 Rust 让 s1 失效了, 所以我们用一个新的术语:移动 move
- 隐藏了一个设计原则,Rust 不会自动创建数据的深拷贝
- 就运行时性能而言, 任何自动赋值的操作都是廉价的
5.5.3. 变量和数据交互的方式: 克隆(Clone)
- 如果真想对 heap 上面的 String 数据进行深拷贝, 而不仅仅是 stack 上的数据,可以使用 clone 方法
1 | fn main() { |
5.5.4. Stack 上的数据: 复制
- Copy trait, 可以容易想到整数这样完全放在 stack 上面的类型
如果一个类型实现了 Copy 这个 trait,那么旧的变量在赋值后仍然可用 - 如果一个类型或者该类型的一部分实现了 Drop trait,那么 Rust 不允许让它再去实现 Copy trait 了
5.5.5. 一些拥有 copy trait 的类型
- 任何简单标量的组合类型都可以是 copy 的
- 任何需要分配内存或某种资源的都不是 copy 的
- 一些拥有 Copy trait 的类型
- 所有的整数类型, 例如 u32
- bool
- char
- 所有的浮点类型, 例如 f64
- tuple 元组, 如果其所有的字段都是 Copy 的
(i32, i32) 是
(i32, String) 不是
- 所有的整数类型, 例如 u32
5.5.6. 所有权与函数
- 在语义上, 将值传递给函数和把值赋给变量是类似的:
- 将值传递给函数将发生移动或复制
5.5.7. 返回值与作用域
- 函数在返回值的过程中同样也会发生所有权的转移
1 | fn main() { |
- 一个变量的所有权总是遵循同样的模式:
- 把一个值赋给其它变量时就会发生移动
- 当一个包含 heap 数据的变量离开作用域时,它的值就会被 Drop 函数清理,除非数据的所有权移动到另一个变量上了
5.5.8. 如何让函数使用某个值,但是不获得其所有权?
1 | fn main() { |
5.6. 引用和借用
- 以下例子中参数的类型是&String 而不是 String
- &符号就表示引用:允许你引用某些值而不取得所有权
1 | fn main() { |
我们把引用作为函数参数这个行为叫做借用
是否可以修改借用的东西?不行
和变量一样,引用默认也是不可变的
1 | fn main() { |
5.6.1. 可变引用
可变引用有一个重要的限制: 在特定作用域内,对某一块数据, 只能有一个可变引用
1 |
|
- 这样做的好处是可在编译时防止数据竞争
以下三种行为下会发生数据竞争:
- 两个或多个指针同时访问同一个数据
- 至少有一个指针用于写入数据
- 没有使用任何机制来同步对数据的访问
- 可以通过创建新的作用域, 来允许非同时的创建多个可变引用(例子)
- 不可以同时拥有一个可变引用和一个不变的引用
1 | fn main() { |
5.6.2. 悬空引用 Dangling References
悬空指针(Dangling Pointer): 一个指针引用了内存中的某个地址, 而这块内存可能已经释放并分配给其它人使用了.
在 Rust 里, 编译器可保证引用永远都不是悬空引用:
- 如果你引用了某些数据, 编译器将保证在引用离开作用域之前数据不会离开作用域
1
2
3
4
5
6
7
8fn main() {
let r = dangle();
}
fn dangle() -> &String { // 这里编译器会报错,因为s出了此作用域将会被释放,而返回值是一个指向已经被释放区域的指针,这会导致问题,而rust在编译期就杜绝了这种错误.
let s = String::from("hello");
&s
}
5.6.3. 引用的规则
- 在任何给定的时刻, 只能满足下列条件之一:
- 一个可变的引用
- 任何数量不可变的引用
5.7. 切片
Rust 的另一种不持有所有权的数据类型: 切片(slice)
一道题, 编写一个函数
- 它接收字符串作为参数
- 返回它在这个字符串里找到的第一个单词
- 如果函数没有找到任何空格, 那么整个字符串就被返回
1 |
|
字符串切片
- 字符串切片是指向字符串中一部分内容的引用
- 例子
- 形式:[开始索引..结束索引]
- 开始索引就是切片起始位置的索引值
- 结束索引是终止位置的下一个索引值
1 |
|
注意:
- 字符串切片的范围索引必须发生在有效的 UTF-8 字符边界内。
- 如果尝试从一个多字节的字符中创建字符串切片,程序会报错并退出
1 |
|
将字符串切片作为参数传达
- fn first_word(s: &String) -> &str {
- 有经验的 Rust 开发者会采用&str 作为参数类型,因为这样就可以同时接收字符串和&str 类型的参数了
- fn first_word(s: &str) -> &str {
- 使用字符串切片,直接调用该函数
- 使用 String, 可以创建一个完整的 String 切片来调用该函数
- 定义函数时使用字符串切片来代替字符串引用会使得我们的 API 更加通用,且不丧失任何功能。
1 |
|
其他类型的切片
- 例子
1 |
|
5. Struct
定义并实例化 struct
什么是 struct
- struct 结构体
- 自定义的数据类型
- 为相关联的值命名, 打包=>有意义的组合
定义 struct
- 使用 struct 关键字, 并为整个 struct 命名
- 在花括号内, 为所有字段(Field)定义名称和类型
例如:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
实例化 struct
- 想要使用 struct, 需要创建 struct 的实例:
- 为每个字段指定具体值
- 无需按声明的顺序进行指定
例子:
1 | fn main() { |
获取 struct 里面的某个值
- 使用点标记法:
1 |
|
注意:
一旦 struct 的实例是可变的,那么实例中所有的字段都是可变的。rust 不允许我们声明 struct 中一部分字段可变,而另一部分字段不可变。
struct 作为函数的返回值
1 |
|
字段初始化简写
- 当字段名与字段值对应的变量名相同时,就可以使用字段初始化简写的方式:
1 |
|
struct 更新语法
- 当你想基于某个 struct 实例来创建一个新实例的时候,可以使用 struct 的更新语法
不使用更新语法的例子:
1 |
|
使用更新语法的例子:
1 |
|
Rust基础部分未完待续
6. Rust web 框架
2022 年可选择的三个 Rust Web 框架:actix-web、warp 和 axum。
- actix-web:4.0.0-rc.35,134,720Actix Web 是一个功能强大、实用且速度极快的 Rust Web 框架
- Warp: 0.3.24,114,095 以翘曲的速度服务于网络
- axum: 专注于人体工程学和模块化的 Web 框架(由 tokio 团队提供)
比较:
- axum 有最干净的 API,它建立在 hyper 之上,它(当然)是 Rust 中经过测试最可靠的 HTTP 堆栈,并且因为它是由 tokio 团队开发的。但它的年轻可能会让一些人感到不舒服。
- 对于较大的项目,我认为这 actix-web 是无可争议的赢家。这就是为什么它是我选择 Bloom 的原因。
- 对于较小的项目(最多 50 条路由)warp,尽管它有原始的 API,但它非常好,因为它也是建立在其之上的 hyper,因此受益于它的可靠性和性能。
详细比较:
- JSON 反序列化:所有框架都使用泛型来提供简单的 JSON 反序列化。话虽如此,我发现两者都 axum 可以 actix-web 更直接地与他们的助手一起使用来自动提取类型化的正文有效负载。
- 路由:axum 是明显的赢家,紧随其后的是 actix-web,然后是 warp 有利于组合的功能性 API,这与我们通常对 Web 框架的期望相去甚远。
- 中间件:warp, 毫无疑问…
- 状态:在构建 Web 服务时,您需要共享一些变量,例如数据库连接池或一些用于外部服务的客户端。所有框架的人机工程学都非常相似。
Which web framework do you use in Rust?
axum was voted mostly
7. 后记
本文原文位于鹏叔的技术博客 - Rust 编程语言入门教程, 若需要获取最近更新, 请访问原文.
7.1. web 框架选择参考文档
Rust 编程语言入门教程