Rust101
Rust 101
Rust 入门,笔记,合集:vol 1 (明示:还有 vol 2,大概过几个月就有时间学了)
久违的学习笔记上新!我满怀热情地学习《The Rust Programming Language》by Steve Klabnik and Carol Nichols, with contributions from the Rust Community。以及伟大的开源中文译本:https://rustwiki.org/zh-CN/book/
Hello World
1 | // hello.rs |
1 | $ rustc hello.rs |
cargo
:包管理、项目组织构建
1 | $ cargo new hello_cargo # 新建项目 |
Rust 量
- 常量:
const
始终不可变 - 变量:
let
:默认不可变let mut
:只可变,类型不可变
遮蔽(shadow):作用域内重新声明同名变量。
- 名不变
- 值/类型可变
1 | { |
Rust 静态类型
静态类型:编译时确定所有变量的类型。
Rust 标量
类型 | 类型 | 说明 |
---|---|---|
整型 | i/u + size :i16 ,u32 ,… |
* isize : 由机器架构决定(64 位机:isize=i64 )* usize : 用作索引值 |
浮点型 | f32 ,f64 |
默认 f64 ,IEEE 754 格式 |
布尔型 | bool :`true |
false` |
字符 | char |
4 byte:unicode |
算术
+
,-
,*
,/
,%
,…
整数
/
:向下取整
整型:字面量
进制 | 字面量 |
---|---|
二 | 0b1011 |
八 | 0o76 |
十 | 114_514 |
十六 | 0xfe |
字节(u8) | b'A' |
整型:溢出
- debug:panic
- release:二进制补(正常溢出)or 其他可选行为
Rust 复合类型
元组 ()
1 | let tup: (i32, f64, u8) = (500, 6.4, 1);` |
- 多种类型 多个值 放一起,长度固定
- 解构:
let (x, y, z) = tup;
- 索引:
let x = tup.0;
- 单元类型:
()
,单元值:()
- 表达式不返回 =>
return ();
- 表达式不返回 =>
数组 []
1 | let a: [ i32 ; 5 ] = [1, 2, 3, 4, 5]; |
- 相同类型,长度固定
- Repeat:
let a = [3; 5];
=> 5 个 3 - 访问:
let first = a[0];
- 越界:runtime panic
Tips:用 dbg!
宏答应表达式的值到 stderr
1 | let foo = dbg!(表达式); |
Rust 函数
1 | fn 名(参, 数) 〔-> 返回类型〕 { |
注:六角括号 〔〕
表示内容可选
语句 vs 表达式
- 语句:操作,不返回值
- 表达式:计算,产生返回值
- 函数、宏调用,代码块
{ ... }
都是表达式
- 函数、宏调用,代码块
语句 = 表达式 +
;
1 | let y = { |
Rust 控制流
if 条件
1 | if 表达式 { // 表达式必须返回 bool 值,不会隐式转换 |
if
是表达式,有返回结果:
1 | let num = if cond { 5 } else { 6 }; |
循环
loop
:无限循环(可以 break 出来)while
:while 条件 { ... }
for
:for element in array { ... }
break
:退出并从循环返回值
1 | let result = loop { |
Range:
1 | for n in (1..4).rev() { // .rev() 把 Range 反转 |
Rust 所有权
管理权(ownership)主要是管理堆数据
Rust 值的存放:
- stack:编译时已知固定大小的数据
- heap:编译时大小未知或不定的数据
所有权规则:
- 每个值在任一时刻都有且只有一个所有者(owner)变量
- owner 离开作用域,值被丢弃(调用 drop 函数进行销毁)
移动 vs 克隆
栈值克隆:
1 | let x = 5; |
堆值移动:
1 | let s1 = String::from("hello"); |
自动复制一定是运行时对性能影响小的深拷贝。
要克隆堆值,必须使用显式的深拷贝:
1 | let s1 = String::from("hi"); |
trait: Copy & Drop
实际上,起决定作用的并不是“栈值”、“堆值”,而是看类型实现了那种 trait(二选一):
Copy
:可克隆1
2let new = old;
// 之后 old 仍可用Drop
:要移动离开作用域时,值被 drop
标量(整浮布字)都 impl 了 Copy;
元组所有元素 Copy,则元组 Copy;
所有权 & 函数
值传给函数:同赋值:
- Copy 的复制
- Drop 的移动:值移动到了 fn 里:外面(调用后面)不能用了
1 | let i = 42; |
引用、借用
引用:类似于 C 的指针
- 引用:
&
- 解引用:
*
1 | let s = String::from("hi"); |
借用:传 &s
不让函数拥有 s 的所有权
- 函数 end,离开作用域 => 不 drop 值
可变引用
&s
只读引用&mut s
可用于修改 s 的值
1 | let mut s = String::from("hello "); |
同一时间,一个数据只能有多个只读引用或一个可变引用。
变 + 变 | 不变 + 变 | 不变 + 不变 |
---|---|---|
❌ | ❌ | ✅ |
=> (在编译时)避免(并发时) data race
1 | let mut s = String::from("hi"); |
非词法作用域生命周期(Non-Lexical Lifetime,NLL)
引用的作用域是「声明 -> 最后一次使用」。
∴ 可以:
1 | let ir = &s; |
悬垂引用
悬垂(dangling)引用:指向已释放的内存的引用。
=> Rust 直接编译时 Error
1 | fn d() -> &String { |
slice
切片(slice):引用集合中的一段连续的元素。
1 | let s = String::from("hello world"); |
切片是一个不可变引用,结合“不能变+不变”的引用规则
=> 保证 slice 始终可用、未变。
字符串常量:
- 类型:
&str
- 指向二进制程序中的某位置
一般处理字符串的函数都用
fn f(s: &str)
这样传参
String
对象和&str
(包括字面量)都可。
Rust struct
一般结构体
定义:
1 | struct User { |
实例化:
1 | let mut foo = User { |
语法糖:变量名与字段同名 => 简写
{foo: foo, } => {foo, }
1 | fn build_user(email: String, age: u8) -> User { |
结构体更新语法
1 | let user2 = User { |
- 若
user1
中的 Drop 值被user2
使用,则等于移动到了user2
,user1
不再可用; - 若
..user1
只用了user1
中的 Copy 值,则之后user1
仍然可用:- User 中全是 Copy
- 或 Drop 值全被显式给出
元组结构体
元组结构体:无字段名,匿名结构体。
1 | struct Color(i32, i32, i32); |
访字段(同元组):
- 解构:
let (r, g, b) = red;
- 索引:
let r = red.0;
类单元结构体(unit-like struct)
1 | struct Foo; |
方法
1 | struct Foo { |
方法可以和字段重名:
区别 -> 调用时:
foo.bar
是字段foo.bar()
是方法
用于做 getter。
自动(解)引用
1 | foo.bar() |
会自动加上:
1 | (&foo).bar() |
使之匹配方法的 self 参数签名。
关联函数
impl
中定义- 首参非
self
- 调用通过
::
常用来做构造函数:
1 | imple Foo { |
Rust 枚举
成员可带有值:
1 | enum IpAddr { |
枚举可作为参数:
1 | fn route(ip: IpAddr) { |
枚举可实现方法:
1 | impl IpAddr { |
Option
- Rust 没有空值
- 用
Option<T>
表示可能为空的东西
1 | enum Option<T> { |
该类型会 prelude,不用导入:可以忽略前缀(Option::
)直接用 Some
、None
。
1 | let some_number = Some(5); |
match
match: switch-case in Rust.
1 | match ip { |
- match 是表达式:有返回值
- match 需要穷举:用
_ => ...
加默认分支
(没有返回值默认分支可以返回单元值:_ => ()
)
用 match 处理 Option:
1 | fn plus_one(x: Option<i32>) -> Option<i32> { |
if let
if let
只匹配一个模式,忽略其他。
1 | let some_val = Some(10); |
改为:
1 | if let Some(3) = some_val { |
后面还可以加 else { ... }
Rust 项目:模块系统
一个 package 包含一或多个 crate:
1 | ╭ |
*
?
+
一个或以上 零或一个 一个以上
crate
bin crate:
src/main.rs
:一个与 package 同名的二进制可执行src/bin/*
:多个 bin crate
lib crate:
src/lib.rs
:一个与 package 同名的库- 可用
cargo new --lib name
新建库项目
mod
模块层次:
1 | crate // 可以看作一个 root mod |
- mod、fn:无 pub 则私有
- struct:任一成员都需显式 pub 才公开
- enum pub 则所有成员都公开
实现:单文件
e.g. src/lib.rs
1 | pub mod front_of_house { |
实现:多文件
- 每个文件一个模块
- 文件路径(
src/foo/bar.rs
)即模块路径(crate::foo::bar
) mod NAME;
导入其他文件模块
就是把前面单文件中嵌套的 mod,改成同名文件、目录(注意,这样是通过文件名导入,文件中不写 mod NAME { ... }
)
e.g. 多文件多模块
1 | foobar |
src/main.rs
:
1 | mod foo; |
src/foo.rs
:
1 | pub mod bar; |
src/foo/bar.rs
:
1 | pub fn buzz() { |
use
相当于把东西软连接到当前模块:
1 | use crate::front_of_house::hosting 〔as front_hosting〕; |
一般 use 到:
fn 所在的 mod(
use xxx::hosting
而不是use xxx::hosting::add()
)struct、enum 导到类型:
1
2
3
4
5use std::collections:HashMap;
fn main() {
let mut map = HashMap::new()
}
简写 use:合并公共路径
1 | use std::{ cmp::Ordering, io }; |
1 | use std::io::{self, Write}; |
1 | use std::collections::*; |
外部包
- 标准库:
1 | use std::----; |
第三方包:
1
use rand::Rng;
需要在
Cargo.toml
中定义:1
2[dependencies]
rand = "0.8.3"
Rust 集合
- vector
- string
- HashMap
vector
Vec<T>
,变长数组,固定类型 T,但是 T 可以用 enum 来放多种值。
新建:
1 | let mut v1: Vec<i32> = Vec::new(); |
栈式访问(需要 mut):
1 | v.push(5); |
随机访问:
1 | let third: &i32 = &v[2]; |
遍历:
1 | for i in &v { |
string
&str
:字符串切片 -> 语言核心中的String
:标准库提供的一种 utf-8 字符串实现
1 | let mut s1 = String::new(); |
⚠️ 注意 String 不支持索引:s[0]
❌
- String 是 Vec
的封装 - Rust 保证索引运算的时间复杂度是
O(1)
- utf-8 按字节切不合理,按字符切慢,所以不能索引
遍历:
1 | for c in "...".chars() { |
1 | for b in "...".bytes() { |
尾加:
1 | s.push('l'); // 字符 |
1 | let s1 = String::from("foo"); |
1 | let s = format!("{} {} {}", s1, s2, s3); // fmt.Sprintf |
HashMap
HashMap<K, V>
:哈希表
1 | use std::collections::HashMap; // 需要导入 |
From zip:
1 | let keys = vec![String::from("Blue"), String::from("Red")]; |
取值:
1 | let team = String::from("Blue"); |
遍历:
1 | for (key, value) in &scores { |
Insert if not exists:
1 | scores.entry(String::from("Yellow")).or_insert(50); |
改旧值:
1 | let text = "hello world wonderful world"; |
Rust 错误处理
panic:不可恢复
Result: 可恢复
panic
1 | fn main () { |
panic!
宏会导致:
- 展开:回溯栈,逐步清理
- 终止:直接退,留给 OS 清理
默认是展开,终止要在 Cargo.toml
中设:
1 | [profile.release] |
Result
1 | enum Result<T, E> { |
用 match 处理 enum:
1 | use std::fs::File; |
Ok 取值,Err panic 这个模式很常用,所以有化简:
1 | let f = File::open("hi.txt").unwrap(); // Err => panic |
还可以用闭包自定义处理:
1 | let f = File::open("hi.txt").unwrap_or_else(|error| { |
错误传播
错误传播:把 Result 继续向上返回给调用者。
1 | fn open_file () -> Result<File, io::Error> { |
简写:
1 | let f = File::open("hi.txt")?; |
在 main 中用 Result
和 ?
:
1 | fn main() -> Result<(), Box<dyn Error>> { |
Rust 泛型
1 | fn largest<T>(list: &[T]) -> T {...} |
1 | struct Point<T, U> { |
1 | enum Option<T> { |
泛型 x 方法:
1 | impl<T> Point<T> { |
只对某种特定类型实现:
1 | impl Point<f32> { |
Rust trait
trait:定义共享的行为(类似于 interface)
定义 trait:
1 | pub trait Summary { |
实现 trait:
1 | pub struct Foo { ... } |
注意:impl 后面的 trait 或 for 后面的类型,其中之一必须定义在本地作用域:
不能为外部类型实现外部 trait
默认实现:定义 trait 时写一种通用的实现
1 | pub trait Summary { |
trait 作为参数:
1 | pub fn notify(item: impl Summary) { ... } |
1 | pub fn notify<T: Summary+Display>(item: T) { ... } |
1 | fn f<T, U> (t: T, u: U) -> i32 |
Rust 生命周期
引用的 Lifetime
1 | let r; ─┐'a |
❌ r
在 'a
,但引用了更小的 'b
1 | let x = 5; ─┐'b |
✅ r
在 'a
,引用更大的 'b
中的 x
生命周期标注
生命周期标注:告诉编译器一个引用是从哪里借过来的。
函数生命周期标注
函数生命周期标注:告诉返回的引用时从哪个参数借来的。
1 | fn main () { |
TODO:实现 longest
函数。
1 | fn longest(x: &str, y: &str) -> &str { |
❌ 不能编译!
- 返回的引用不知道是从
x
还是y
借来的 - 生命周期不能确定
使用泛型标注:
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
- 用泛型语法声明一个 lifetime 标注
- 标注
x
、y
都是生命周期'a
(Rust 会取二者中较小的) - 标注返回值的 lifetime 也是
'a
调用时:
1 | ✅ { |
1 | ❌ { |
使用标注:必须让返回值与参数关联(必须在参数、返回值中同时出现)
1 | fn f<'a>(x: &str, y:&str) -> &'a str {...} |
若返回值生命周期与参数无关(说明不是借来的),则应该返回具有所有权的东西,而非引用。
结构体生命周期标注
包含引用的 struct 应在泛型列表中加类型标注:
1 | struct E<'a> { |
生命周期省略
省略规则:
每个引用参数都拥有自己的 lifetime;
1
2fn f(x: &str) => fn f<'a>(x: &'a str)
fn f(x: &str, y: &str) => f<'a, 'b>(x: &'a str, y: &'b str)若只有一个输入 lifetime,则输出 lifetime 与之同;
1
2
3fn f(x: &str) -> &str
⬇️
fn f<'a>(x: &'a str) -> &'a str若有一个参数是
&self
或&mut self
,即函数是一个方法,则输入 lifetime 与 self 同;1
规则 3 就是说:方法产出的 lifetime 同 self。
使用规则,一步步走,若推断出了输出的生命周期则结束:
1 | fn f(s: &str) -> &str |
1 | fn f(x: &str, y: &str) -> &str |
静态生命周期
1 | let s: &'static str = "hello"; |
让引用在整个程序中始终可用。
(实际上:&str
字面量自带 'static
)
Rust 测试
1 | // test 属性标注 |
1 | $ cargo test |
测试手段:断言
1 | assert!(BOOL, "fmt", args...) |
测试手段:panic
1 | panic!("...") // => test FAILED |
期望 panic (不 panic 则 failed):
1 |
|
测试手段:Result
Ok
则测试通过Err
则 FAILED
1 |
|
控制测试运行:
1 | $ cargo test TARGET -- --option |
TARGET
:测试目标:- 函数全名
- 部分名称:run 所有包含该词的
--option
:--test-threads=1
单线程--show-output
显示 stdout
忽略测试:
1 |
|
- 用
cargo test
会忽略该测试 - 用
cargo test --ignored
专门跑标记了 ignore 的测试
测试的组织
- 单元测试:测私:在隔离环境,一次一个功能
- 集成测试:测公:以外部视角,一次测多个
单元测试
与待测代码在同一个文件中,搞一个 tests 子模块:
1 | fn foo(a: i32) -> i32 { // 业务代码:待测 |
集成测试
建个单独的 tests 目录,与 src
同级:
1 | . |
integration_test.rs
:
1 | use adder; // 待测 lib |
cargo test
测试所有单元、集成测试cargo test --test FILE_NAME_WITHOUT_RS
测试某集成 test 文件
集成测试模块中的通用代码:
1 | // tests/common/mod.rs |
在其他 test 文件中调用:
1 | use adder; |
二进制 crate 的集成测试:
tests
目录下不能拿src/main.rs
中的代码- 所有功能写在
lib.rs
,成为一个可测试的库(库与 crate 同名) tests
中测试lib.rs
main.rs
只简单调用lib.rs
,无需再测
1 | . |
实例:minigrep
1 | minigrep |
main.rs
:
1 | use std::env; |
具体功能 lib.rs
:
1 | use std::error::Error; |
单元测试:lib.rs
1 |
|
1
2 println!("by CDFMLR");
// 2022.10.03 (实际上是两个月前写的了。。)