学习 Rust 的读书笔记

目前我唯一认真自学过并且日常中正经使用的编程语言是 Python, 我把下一个认真学的目标定为 Rust。 为什么呢?首先,我的目标是要学一门比较底层的系统级语言的,首选肯定是 C++,但很惭愧,虽然是科班生,但我对于 C++ 的现代化特性了解太少了,日常中也几乎没怎么使用过,补起来其实也很麻烦,跟学一门新的也差不多了; 此外除了语言标准,从编程体验上讲,作为比较历史悠久的语言,感觉 C/C++ 各个大项目都有一套自己的构建方案和自己实现的基础工具库, 对于一些开发上的问题比如引入第三方库的方式、代码格式化、写单元测试、项目构建,缺少一个语言层面上的统一的约束,这点新兴语言都做的更好一些,学起来更舒服; 此外根据我过去的浅薄经验,C/C++ 编译器对程序员的约束太自由了。反过来想,如果在一个更严格的编译器下面学习,学到的一些约束即使后面要写 C++ 应该也是有帮助的。

Rust 之前也看过官方教程,并且刷过一些题了,但还是有些一知半解。据说 Programming Rust 这本写得很好,所以这次是作为一个已经简单入门的新手再学习一遍这本书。本文内容是看书的过程按自己的理解整理的只言片语,并非书的原文,不能保证正确性,出现错误也很正常。除了 Rust 以外,我也希望能通过这次看书加深一些对各种编程语言都通用的概念的理解。

目前的打算是像兴趣爱好一样隔段时间就抽时间学学, 目标是至少能像我写 Python 一样熟练吧(其实根据我去年和今年做 Advent of Code 的结果看,我用这两门语言做题好像用时其实已经差不多了…)

第 3 章 基本类型

相关文档

  • 整型:i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, isize, usize 有无符号+多少位
    • 整数字面量: 116i8, 0xcafe, 0b0000_0010, 0o106,前缀进制+后缀类型,下划线只为帮助人眼 parse
    • 字节字面量: b'X', b'\t', b'\xff', 都表示一个 u8
    • 类型转换: 10_i8 as u16as 转换,强类型,没有隐式转换
    • 一些函数:标准库自带一些常用函数 pow(), abs(), count_ones()等等, 用法 (-4i32).abs() or i32::abs(-4)
    • 溢出处理:debug 下就自动 panic,release 下就跟c/c++一样。带了一些特殊的运算函数可以控制溢出的行为,checked (结果Option包起来), wrapping (结果模一下), saturating (结果取最值), overflowing (返回两个值:结果模一下+是否溢出)
  • 浮点:f32, f64 单精度+双精度
    • 浮点字面量:-1.5625, 2., 1e4, 40f32
    • 特殊值:INFINITY, NEG_INFINITY, NAN
    • 一些函数:同样带一些常用函数,sqrt(), floor(), ceil(), log(), sin()
  • 布尔:bool 大小是 1 字节
    • 强类型,不能 if x {} 如果 x 不是布尔型
  • 字符:char 大小是 4 字节,没想到吧,因为是表示一个合法的 Unicode 字符
    • 字符字面量:'a', '\t', '中', '\xFF', '\u{CA0}'
  • 元组:不同类型用圆括号括起来,如 (&str, i32)
    • ()unit 类型,跟一些函数式语言一样,类似 void
    • t.0, t.1 按下标访问
  • 指针:表示内存地址的类型
    • 引用,&,可以认为是 Rust 的基本指针类型
    • Box,堆上分配的
    • 裸指针,*,就是 C/C++ 里的那种
  • 数组:内存中连续的一系列值
    • 定长数组 [T; N]
    • 可变长数组 Vec<T>
    • 数组切片 [T] 长度不定,总是以引用 &[T] and &mut [T] 的形式出现
  • 字符串:挺复杂的,17章细说
    • 字节字符串 b"GET", 实质上为 &[u8; 3]
    • 可变长字符串,String, 以 UTF-8 编码存储,可以看成 Vec<u8>
    • 字符串切片,&str,可以看成 &[u8]
    • 字符串字面量,例如"Hello",是一个字符串切片,类似C里面的 const char*。自动支持多行,行尾加反斜杠可以代码写多行实际一行。特殊地,r"C:\Users"(普通raw string), r#"xxx"#(加若干井号后是真正的 raw string, 支持双引号)
    • 其他类型,PathBuf(路径),Vec<u8>(非UTF-8编码时),CString(与C库交互,有尾0),OsString(操作系统相关)
  • 复杂类型
    • 结构体 - 9章
    • 枚举类 - 10章
    • Trait - 11章

第 4 章 所有权与移动

  • 基础的所有权模型:
    • 每个值有一个唯一的所有者,决定着值的生命周期(主人的任务罢了)
    • 变量们拥有它们的值,结构体们拥有它们的字段,元组、数组和Vec们拥有它们的元素
  • 在这之上的扩展:
    • 所有权转移:赋值和传参的默认行为是移动;移动拥有多个值的复杂类型中的一个值可以考虑replacetake等函数
    • Copy 类型:可以 memcpy 的类型就浅拷贝,不移动
    • 引用计数:RcArc, 通过 .clone() 增加引用计数,一个值可以有多个所有者,想想 Python
    • 借用:把值临时借出去,不转移所有权,下一章

第 5 章 引用

  • 引用
    • 一种没有所有权的指针,不影响值的生命周期
    • 共享引用:一个值同时可以有多个,只读,可 Copy
    • 可变引用:一个值同时只能有一个,可写,把值完全独占其他人都不能碰,不 Copy (对于这里我有点疑惑,当传参是可变引用时实际上并没有 move 进去,听人说是引入了一个叫 reborrow 的概念)
    • 这里的引用概念更像 C++ 的指针而不是引用。第一个区别是 C++的引用底层实现虽然也是地址,但用着就像是一个别名,不需要显式解引用(在用.调用方法的时候,Rust也有自动解引用的语法糖,但简单类型就必须显式解引用);第二个区别是 C++ 的引用初始化之后就不能再指向别人了。
    • 引用比较大小时比较的是他们指向的值
    • 不存在空引用 (NULL or nullptr)
    • 可以对任何一个表达式取引用,语言底层会创建一个匿名变量存住值
  • 引用的安全性
    • 对变量x的引用不允许比x活得长,避免产生悬空指针
    • 结构体内的引用类型成员必须带显式生命周期标记,有助于暴露出变量之间的引用关系
    • TODO 对生命周期还有些理解不到位的地方

第 6 章 表达式

  • C/C++ 中表达式和语句有明确区别,前者有值后者没有,Rust 一切都是表达式,都有值
  • 花括号围起来的是一个 Block,如果其中最后一个表达式没有分号表示是这个 Block 的值,带分号的表达式值为()
  • 与 C 有显著区别或独有的表达式:
    • 模式匹配 match, 如只想匹配一个 pattern 可以用 let <pattern> = <expr> 并作为 if 和 while 的条件
    • range 表达式 0..len(vec), (0..=10).step_by(2).rev()
    • for 只是迭代器的语法糖,只有 for in
    • 死循环有专用的关键字 loop
    • break 和 continue 可以返回值, 并且可以加一个 label 跳出多重循环
    • 按位非是!, 没有 ++/--

第 7 章 错误处理

  • Panic
    • 默认过程:打印错误信息 -> Stack Unwinding -> 线程退出
    • 类似 Java 的 RuntimeException
    • 也可以修改默认行为,让进程直接 abort
  • Result
    • Result 是一个用于错误处理的枚举类型,要么 Ok(T) 要么 Err(E)
    • 通常做法是 match 一个 Result 代替 try/catch
    • ? 表达式用在返回 Result 类型的函数,提前将 Err 返回,传递到上一层 (也可用于 Option)
    • 处理多种类型的错误,可以转为 std::error::Error
    • 自定义错误处理类型,实现 DisplayError trait

第 8 章 Crate 和模块

  • Crate
    • 每个 crate 是一个编译单元,有 lib 和 bin 两种类型,包括代码和其他需要的文件
  • Module
    • 模块用于一个项目内代码的组织,有点类似命名空间?
    • 可以放在单独的文件里,做法是把之前定义 mod xxx 的位置改为声明 mod xxx;, 然后定义在同名文件里
    • 也可以是一个目录,目录下需要一个 mod.rs,同样在原来位置声明一下
    • use 引入模块,分隔符::,类比文件系统,特殊关键字:相对路径用 selfsuper, 还可以 crate 表示当前 crate
    • 单元测试可以写在代码里的一个有 #[cfg(test)] 的模块里,每个测试 #[test],集成测试在 tests 目录下

第 9 章 结构体

  • Struct
    • 三种结构体:named-field(有成员,成员有名字), tuple-like(有成员,成员没名字), unit-like(没有成员)
    • 初始化 named-field struct 可以用 .. EXPR 表达式将没有提及的成员从 EXPR 中复制
    • 在 impl block 里定义方法;self 表示当前对象,参数没有 self 就是类方法, 用::访问类中定义的常量和类方法;
    • 没有构造函数,一般定义一个名为 new 的类方法
    • 可以有泛型参数和生命周期参数

第 10 章 枚举类和模式

  • Enum
    • 支持C里面那种枚举类,还可以为枚举类实现方法
    • 除了C里面那种枚举类,还支持带数据的 variant, 每个 variant 类似上面的 tuple struct (典型例子: IP 地址, JSON对象)
  • 模式匹配
    • 可以匹配 字面量、范围、下划线忽略、移动到变量、引用到变量、variable @ subpattern
    • 可以匹配 元组、数组、切片
    • 可以匹配 枚举类 variant 、结构体各个字段
    • Guard 表达式: x if x*x <= r2
    • 多个 pattern 用 | 连接
    • 可以用在 match 表达式、let 表达式,函数传参,迭代器,闭包…

第 11 章 Trait 和泛型

通过 Trait 和 泛型 实现 多态 (polymorphism)

  • trait 的意义在于表示实现了这个 trait 的类型是具有某种能力的(如某类型实现了 Iterator trait 表示这个类型能够通过调用next()产生一个序列)
  • 定义上类似 Java 的接口,但还是有细节上的不同(Java 不能为别人写好的类实现自己写的接口,不能同时实现两个有相同方法名的接口,等等)
  • trait 可以为任意类型增加新方法,为了避免冲突,必须引入到当前 scope 内才能用
  • trait object
    • 对某个 Trait 类型变量的引用称为 trait object
    • Rust 可以自动将普通的引用转换为 trait object, 这是创建 trait object 的唯一方式
    • 只能将普通引用转为 trait object 而不能将 trait object 转回具体的类型
  • 泛型
    • 用法:<T: Bound1 + Bound2> 或者太长写到后面 where T: Bound1 + Bound2
    • 调用时如果不能推断要明确写出来,还要加一个 ::collect::<String>()
  • 选择 trait object 还是 泛型
    • 如果需要用到不同类型混合时,例如需要某个 Trait 类型的 Vec,选择 trait object
    • 泛型的优势是:更快;不是每个Trait 都有 trait object; 可以很容易对类型加约束
  • orphan rule
    • 为某类型实现 trait 时,类型和 trait 在当前 crate 必须有一个是新的
  • trait B 可以是另一个 trait A 的 subtrait, 意味着 impl B 的类型必须 impl A
  • trait 也可以有类型关联函数和类型关联常数
  • trait 关联类型 (例 迭代器) 和 泛型 trait (例 运算符重载) :对同一个 trait 有多种实现(如 From<T>)用泛型,否则用关联类型
  • impl Trait 静态分发

第 12 章 运算符重载

std::ops 文档

  • 基本每种运算符有内置的一个 trait,重载就是实现 trait
  • 如加法,给我自己的类型实现 Add<T> 那么我的类型就能跟类型 T 做加法
  • 不能重载的运算符:取引用、赋值、问号、范围、函数调用

第 13 章 工具 Trait

Rust 内置的 Utility Trait 们

  • Drop, 用于析构, 但实际上清理的工作交给 Rust 自动完成,实现这个 trait 可以自定义在丢弃值时要做的额外操作。
  • Sized, 表示这个类型的值占用的大小都是一样的。多数类型都是 Sized,例外如 str(注意没引用)dyn,这种类型只能通过引用来处理。不明确标记的泛型默认都 Sized
  • Clone, 用于创建一个完全独立的副本。可以理解成深拷贝,但也不是必定要花费很大的代价。标准库里的类型大多数都实现了Clone
  • Copy, 表示这个类型赋值时默认是拷贝而不是移动,即只需要浅拷贝就足够了的类型。与 Drop 不能共存,因为 Rust 预设你析构需要额外操作那么就不能简单地浅拷贝。
  • Deref and DerefMut, 用于明确指出如何解引用一个类型,也就是重载.*,常见的例子如那一堆智能指针类型以及字符串引用与切片的自动转换
  • Default, 用于生成一种类型的默认值
  • AsRef and AsMut, 用于从一种类型返回另一种类型的引用
  • Borrow and BorrowMut, 与上面类似功能,但要求哈希值和大小比较的结果一致,即如果 x==y, 那么 x.borrow() == y.borrow()
  • From and Into, 用于类型的转换,会将原类型 move 进来返回新类型。一个用途跟AsRef差不多,只是拿走了所有权;还能用来当构造函数,例如 IPv4 地址可以 From<[u8; 4]>From<u32>。实现了 From, 同时也就自动获得了 Into
  • TryFrom and TryInto, 与上面类似功能,但允许转换失败,因此返回的是个 Result
  • ToOwned, 是 Clone 的延申,放宽了类型要求,可以 clone 为任意实现了 Borrow<Self> 的类型,比如可以传入切片 &str,创建一个副本并返回一个有所有权的 String,因为 String 实现了 Borrow<str>.

第 14 章 闭包

  • 匿名函数,语法是用 | | 圈住参数, 后面是函数体
  • 闭包内可以使用当前作用域内的变量,称作捕获。捕获有两种方式:默认是借用的方式, 但闭包和该变量的生命周期要满足借用规则;第二种||前面加一个move将被捕获变量所有权转移进闭包,满足移动规则。
  • 函数类型,写法fn(xxx) -> xxx, 就像C++的函数指针, 指向函数的机器码
  • 闭包和函数的区别在于除了机器码还可能存数据,有为闭包设立的三个 trait FnOnce, FnMut, Fn,这些 trait 会被函数和闭包类型自动实现
  • 以上三种分别表示能被调用一次的闭包,可调用多次但必须可变的闭包,没什么限制的闭包。排在后面的是前面的 subtrait,就是说实现了后面的也一定实现了前面的。定义闭包时 rust 会自己决定这个闭包属于哪一种.

第 15 章 迭代器

迭代器用于产生一系列的的值, 本章关于两个相关的 trait, 从创建到适配到消费一个迭代器的过程和为自己类型实现迭代器。

  • Iterator trait, 有一个关联类型Item和一个方法next, 传入对象的可变引用,生成Option<Item>, 如果返回None表示迭代结束.
  • IntoIterator trait, 实现它表示可以将这个类型转为一个 Iterator, 有一个关联类型Item和一个方法into_iter, 传入对象自己返回创建的迭代器
  • rust 的 for 循环其实是关于迭代器的语法糖: 用into_iter创建迭代器,然后while let Some(ele) = iter.next()循环
  • 创建一个迭代器
    • iteriter_mut 方法,标准库里的可迭代类型大多提供了这两个方法,返回一个迭代器迭代每个元素的引用
    • into_iter,标准库里的可迭代类型大多为&T,&mut T,T这三种类型实现了这个 trait,返回不同的迭代器配合 for 使用
    • from_fnsuccessors,前者给一个函数创建迭代器,后者给一个初始值和一个函数生成相应后继创建迭代器
    • drain,一个可迭代类型传入一个范围,返回这个范围的迭代器,迭代后原来数据中的这部分内容被消耗掉
    • 其他,标准库里很多方法都可以创建迭代器,如Vec<T>windows(), chunks()String.bytes(), .chars()
  • 适配一个迭代器: 消耗迭代器并以某种规则产生一个新的,从而改变迭代器的行为
    • map, filter, filter_map, flat_map, flatten, take, take_while, skip, skip_while, enumerate, rev, zip, cycle 这是比较常见的, 带点函数式思想的语言都会有类似的东西,叫法可能有差别。rust 在这里的使用风格是链式调用xxx.map(xxx).filter(xxx).xxx()
    • 其他可以看迭代器的官方文档
    • 迭代器是惰性的,适配不会立刻就生效,必要时才会计算
  • 消耗一个迭代器:除了直接 for 循环还提供了一些常用手段
    • 累计:count, sum, product, fold, rfold
    • 最值:max, min, max_by, min_by, max_by_key, min_by_key
    • 比较:eq, lt, gt, le, ge, cmp, partial_cmp
    • 寻找:position, nth, last, find
    • 收集:collect, 消耗迭代器,生成一个实现了FromIterator trait 的类型的变量
    • 其他可以看迭代器的官方文档

第 16 章 集合类型

标准库实现的几种常用数据结构:变长数组,双向队列,双向链表,二叉堆,Map(哈希,B树),Set(哈希,B树)

第 17 章 字符串

第 18 章 输入输出

第 19 章 并发

第 20 章 异步编程

第 21 章 宏

第 22 章 Unsafe

第 23 章 外部函数