以笔记形式记录所学习要点,仅供个人回顾巩固知识。
所阅读的文档为Brown University所推出的带交互问答的版本https://rust-book.cs.brown.edu,在章节中含有问答题,可以帮助加强理解。
rustup
工具作为其编译链和开发组件管理工具,可安装管理Rust编译器rustc
,依赖安装及项目构建工具cargo
等。
rustc
默认编译出的可执行程序包含debug信息且未优化,当编译产品时需要使用参数--release
。
- 直接运行
cargo run
可一次编译和运行程序,无需先运行cargo build
,运行cargo check
可检查源码是否存在问题,且无需编译。
- 定义声明变量时
- 使用
let
关键字默认定义不可修改值的变量,只能最多被赋值一次; - 使用
let mut
能定义可修改值的变量,可被赋值多次; - 使用
let
多次定义同名变量可shadow之前定义的变量,之前的定义作废,且可改变该变量的类型。个人感觉这种特性虽然省了重新命名变量的工作,但也容易引起错误,特别是需要在一段逻辑中间部分定义变量的时候,有可能不小心覆盖前面语句已经定义的变量; - 使用
const
可以定义常量,并且需要显示指定其类型,以及在声明语句中进行赋值。
- 基本数据类型scalar包括:
- 整数:
- 浮点数:
- 布尔值:
- 字符:
- Tuple:
- Array:
8bit-128bit
无符号及有符号数,isize/usize类型由编译目标系统架构决定size大小。字面量赋值类型推断变量默认为
i32
。字面量赋值可用10进制(三位数字可用下划线分隔开)、16进制0x,8进制0o、2进制0b(四位数字可用下划线分隔开)以及字节b(如b’A’)。
当对整数变量赋值产生溢出时,在默认的debug编译模式下会直接报错终止程序,在release模式下不会报错。
f32
及f64
, ieee754字面量赋值类型推断变量默认为
f64
bool
: true
,false
,单字节char
,四字节unicode字符,用单引号包围字符复合类型compound包括:
类型表示:
(field1: Type1, field2: Type2, ……)
,值表示:(1, 'A', "Some String")
。不同字段可为不同类型。
可使用destruction语法解构获取其中每个字段,或者直接使用从0开始的下标访问字段,如t.0访问第一个字段。
空Tuple
()
为一种特殊类型unit
,表示为空,类似C语言中的void。栈空间连续内存区域存储,固定大小不会再改变,适合已知元素数量大小的场景。
显示声明变量为Array类型:
[type; size]
,如let arr: [i32; 5] = {1,2,3,4,5}
。初始化Array变量,如:
let arr = [2; 5]
,定义有5个元素的数组,对数组中每个元素初始化为ii32类型的值2。数组元素的访问使用下标index访问,当index访问产生越界时运行时直接报错终止程序,具有很好的内存安全性。
- 声明方法函数:
使用关键字
fn
,方法函数名称一般用小写字母,单词间用下划线连接。使用
-> type
声明方法的返回值类型。声明时方法参数的类型必须显示声明。
方法返回值可以使用
return
语句或者直接在方法体末尾使用expression
表达式(语句尾不带分号)作为其返回值,若方法体中没有return
语句并且没有用expression
表达式作为返回值,则方法默认返回unit
即()
。- 条件表达式
if
中的条件表达式不用小括号包围。并且表达式的值必须为bool
类型这不同于C, JS等其他语言,否则编译器会报错。if
本身是一种表达式,可用来赋值给左值变量,如if condition {2} else {5}
- 循环表达式
loop:
没有循环判断条件
使用
loop
表达式可以返回值,即在break
后添加欲返回的值,loop
表达式返回的值可用于赋值给变量。loop
在嵌套使用时支持使用lable标记不同的循环体,以便于使用continue
和break
时可以指定所作用的循环层级,label名称需要以单引号开头,未指定label默认作用当前语句所在最内层级循环体。while:
带判断条件
for in:
遍历一个范围
- 安全性
A foundational goal of Rust is to ensure that your programs never have undefined behavior. That is the meaning of "safety."
Rust提供的安全性保证即是确保程序执行过程中不会出现未定义行为,因为可能出现各种意想不到的后果,影响程序逻辑的正确性。
A secondary goal of Rust is to prevent undefined behavior at compile-time instead of run-time.
Rust在编译阶段保证不出现未定义行为,而不是在运行阶段进行检查。以此来保证程序的可靠性和执行性能。
- ownership可去除内存相关的未定义行为以保证内存安全。
Rust自动管理栈空间内存。
Rust使用
Box
创建指针管理堆heap内存,Box::new(..)
,不允许手动释放内存等操作以防止访问无效内存等未定义操作的发生。Box指向的堆内存空间跟随其拥有者即栈空间内变量的自动释放而自动释放,这跟C++智能指针类似。由于如果栈空间内多个变量都拥有同一个Box指向的堆内存空间,那么栈变量的释放会引起多次同一堆内存空间的自动释放,也会引起未定义行为,因此Rust设计了ownership机制。只有拥有者才能管理其所指向的堆内存空间,这其实在C++智能指针中也有对应的机制比如std::unique_ptr结合move语义,但仍然容易写出未定义行为的代码,C++编译器可能不会制止相应行为,比如访问已经被moved的std::unique_ptr。
Rust中的
Vec
,String
,HashMap
都是在堆内存空间中管理可变数量的元素内存。内存相关未定义行为包括:读写已释放内存空间,重复释放同一内存空间。
所有权机制避免了使用垃圾回收器的代价,同时也能保障指针的安全使用以避免未定义行为。
- 引用(
&
,&mut
)实际上依然是指针,其在Rust被使用时被隐式的解引用使用(*),其出发点是为了解决所有权ownwership机制导致的数据复制使用代价。
引用通过借用borrow在不转移数据所有权的情况下复用数据,并且通过借用检查器borrow checker基于一种数据权限机制(
Read
Write
Owner
)确保引用的安全使用。权限与变量访问路径关联。
引用转移或屏蔽了原数据的相应权限,并在其生命结束后返回原数据的相应权限。
原数据必须“存活”得比指向它的引用要久才行。
对于Tuple和Array,对其中任何元素的引用都会导致对Tuple或Array本身以及对其中任何元素访问路径的权限的影响。
&
引用会使原变量本身的Owner
权限失效。&mut
会引起其所借用的变量本身以及其&
引用的读(R)、写(W)权限的失效。- 对于元素值不是copyable的collection类型不能使用索引访问的方式转移其元素的所有权,编译器会报错,可以使用
remove
方法将元素从collection中移除或使用clone
方法复制元素值,或者直接通过引用来访问元素。
struct
类型定义:
struct Name { field_a: type_a, field_b: type_b }
值:
Name { field_a: 1, field_b: 'B'}
struct
类型定义中字段之间的分割符是,
不是;
对struct实例内字段的赋值,有和
ES6
类似的语法:当字段名与值变量同名时可以只写字段名,使用..
语法可将另一个struct实例的字段值都赋值给当前struct实例的对应字段。与Tuple相比,其对字段赋予了名称更具有可读性,但也支持类似Tuple的Struct如
struct Name(i32, i32, i64);
,其值和Tuple类似但需要在小括号前加struct类型名,如Name(1, 2, 3)
。还可以定义
Unit-like
struct,其没有任何字段:struct NoFields;
类型名后需要加分号;
struct的字段类型如果定义为引用则必须指定生命周期。
对struct内字段的引用会影响struct实例本身的RWO权限(不同字段的引用是否有影响?)。
struct内method的定义:
impl Name { fn func_name(&self[,more params]) -> <return_type> }
其中&self
是self: &Self
的缩写,是struct实例的引用,method的第一个参数通常是&self
或&mut self
,也可是self
这种情况下其实例所有权将被该method转移(除非实现Copy
trait)。也可为struct定义非method方法,其参数没有实例的引用,类似其他语言的类静态方法。
method调用:
s.method()
,非method调用:S::method()
method方法和非method方法统称为该struct的
associated function
。可以定义与struct字段重名的associated function,做任何任务。
可以在同一个
impl
代码块内定义多个associated functions也可以在不同impl
代码段内分别定义该struct的associated function。通过struct实例调用method实际是一种function调用的语法糖,如
s.mthod()
等价于S::method(&s)
enum
枚举类型:
enum TypeName { Variant1, Variant2}
一个枚举值可以包含有若干不同类型的具体值:
enum TypeName { Variant1(2, 'A', String::from("ok")), Variant2 }
,可以通过match
表达式获取枚举值内部包含的具体值。match
表达式匹配枚举值时按顺序匹配,需要列出所有可能的情况,但可以在最后匹配分支上使用默认匹配。Option
是一种特殊enum
,包含Some
和None
两种枚举值,其中可通过Some
获取其包含的值,使用Option::None
这一特殊类型避免其他语言使用null值等出现的问题,因为严格的类型检查使得无法写出将None
赋值给某个正常类型变量的逻辑语句。编程人员必须意识到某个变量或表达式的类型为Option
,要将其中包含的具体值提取出来使用,必须考虑None
的情况。当只对
enum
取值的某一种情况感兴趣时,可以使用if let
语句而不是match
更简便。- 源码结构
package > crates > modules
一个项目对应为一个package
一个package内可有多个crates,即多个binary crates(src/main.rs默认的binary crate,src/bin/* 可有多个binary crates)和一个library crate(src/lib.rs)
编译器从根文件即
src/main.rs
或src/lib.rs
开始解析编译代码。module模式提供了一种命名空间机制,将相关逻辑功能代码聚合在同一module内,可以防止实体命名冲突,并且module实现了访问可见性,默认在module中的实体包括子moudle,struct,enum,function等都是private的,若要将其暴露给外界,需要在实体声明前使用
pub
关键字。其中struct的每个field字段可单独指定可见性,其默认可见性为private,即使是tuple struct,对于enum,其每个variant的可见性与整体的可见性保持一致。module路径可为绝对路径和相对路径,绝对路径的root为
crate
,crate是隐含的一个module,相对路径的第一个元素表示当前module同级的其他module或实体。路径中super
表示父级module。使用
use
可以将module内的实体引入到当前scope内使用类似其他语言的include或import,注意是当前scope,一个scope由module分隔。使用
use xxx as yyy
可重命名引入的实体名称。使用
pub use
可将引入的实体名重新在本module中暴露给其它module使用,即re-esxporting
。使用路径前缀和路径嵌套方式可在单个
use
语句引入多个实体,如use std::io::{self, Write}
使用符号
*
可引入相应module内的所有public实体。项目中使用
mod
声明某个module的地方只能有一处,其余地方只需要使用其路径访问该module。这与include
不同。使用不同文件来组织module代码时,其组织规则有点类似Next.js的默认路由规则,即基于文件系统的目录名称和文件名称使与module tree路径一致。
- String
类型不能使用索引值获得单个字符,其是对Vec<uint8>的封装。
&str指向的字符串序列是有效的UTF-8编码字符串。
- Error Handling
使用宏
panic!
可产生不可恢复错误停止程序执行。Result<T, E>是一个枚举,其变体Ok(T)含有T类型的返回值,Err(E)含有E类型的错误信息。
使用
match
表达式可处理Result值,针对分支Ok和Err分别处理,还可以在分支中嵌套match
表达式。match表达式嵌套太多显得代码杂乱,可以使用
unwrap_or_else
方法并结合闭包匿名函数处理错误。使用Result的
unwrap()
方法可以在其为Ok时,返回其包含的值,在其为Err时调用panic!
使用Result的
expect
方法其效果和unwrap()
方法类似,但可以通过其参数指定错误信息字符串。使用
?
操作符可使函数内的Result相关语句将错误信息Err提前返回抛出给函数调用方(自动转换Err所含值的类型,需要已实现相应转换接口)并终止后续逻辑的执行,或在成功时返回Ok中含有的值。?
操作符也可用于函数内的Option相关语句,当其值为None时提前返回None给函数调用方,当为Some时获取到其中包含的值。- Trait
类似其他语言的Interface概念,为某种类型实现某种
Trait
时类型和Trait必须至少有一个是本地定义的,即可以为自定义的某种类型实现其他库中的某种Trait,或为其他库中的某种类型实现自定义Trait。不能为外部类型实现外部Trait,这会导致相同类型对相同trait的实现可能不同,引起混乱。定义Trait时可以只声明方法,或者也可以给出方法的默认实现,在该默认实现中可以调用该Trait中的其他方法,即使被调用的其他方法没有给出默认实现。
Trait可以起到面向对象多态的效果。
方法参数及返回值类型可以使用Trait:
&impl Trait
,多Trait(同时实现):&(impl Tarit1 + Trait2)
Trait Bounds:
<T: Trait>(item1: &T, item2: &T)
,item1和item2必须是相同类型,impl Trait
无此限制。使用where子句使方法参数Trait Bounds声明更清晰易读,如:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
使用where子句改写为:
fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug, {
impl Trait
作为返回值时,方法体不同逻辑只能返回同一种类型,即使不同类型实现了相同的Trait,调用处代码只知道返回了实现该Trait的某一具体类型,但具体什么类型不知道。trait blanket: 可以定义为所有已经实现某Trait的类型实现新的Trait,具有传导性:
- reference lifetime generic
- 对所有引用类型输入参数赋予不同的生命周期;
- 若只有单个引用类型输入参数,则所有引用类型返回值的生命周期与该输入参数一致;
- 若是method其参数有
&self
则所有返回值的生命周期与其一致。
没有改变参数及返回值所涉及引用的生命周期,只是描述他们之间的生命周期关系,以让编译器能阻止不满足条件的情形。
未显示说明引用类型输入参数和返回值生命周期的情况下,编译器根据预设规则自行推断,若无法推断出所有涉及输入参数和返回值的生命周期,则编译不通过会报错。推断规则:
`static
生命周期表示程序的整个运行期间。- test
assert_eq! assert_ne!
的参数需要实现PartialEq
和Debug
trait,所有primitive类型和标准库类型都有实现,自定义类型需要实现,如使用#[derive(PartialEq, Debug)]
使用attribute
#[should_panic(expected = "some text expected from the fatal message")]
可以测试希望终止异常的代码。cargo test
编译测试代码成可执行程序后再执行,可以分别指定cargo test命令的参数和编译结果可执行程序的参数来运行测试,使用--
来分隔两种参数。默认是多线程并行执行测试方法,可使用参数指定单线程顺序执行cargo test -- --test-threads=1
;默认测试通过的测试例不会打印输出被测试代码中的标准输出,可使用参数输出cargo test -- --show-output
。可指定欲测试的测试方法名只测试该测试例,
cargo test func_name_to_test
可指定欲测试的测试方法名中的一部分来测试多个匹配的测试例,
cargo test func_part_name_to_test
,测试模块名也在匹配的内容当中,所以指定测试模块名即可测试其中的所有测试方法测试例。可忽略某些测试例方法,在
#[test]
下一行使用#[ignore]
。还可以反向指定只运行被标记为ignore的测试例使用命令参数cargo test -- --ignore
。单元测试处于逻辑代码文件内,
#cfg(test)
标记的子mod中,其可以访问该逻辑代码中的相应private
内容。无法对main.rs内的代码进行测试,需要进行关注点分离Seperate of Concerns,将主要逻辑组织到lib.rs等文件中,然后对其中的逻辑代码进行测试。集成测试位于单独的目录
tests
中。