包和模块
rust有4层:
- 工作空间(workspace):
Set<packages> - 项目(packages): 由cargo管理,这是项目层级,可以看作
Set<crate>,特征是有Cargo.toml。一个package只能有一个libcrate,但是可以有多个binarycrate - 包(crate): 这是最小编译单元。层级比go的package大,类似java的
Sub-module,可以看作独立的项目,但是crate和文件树没有强对应关系,本质是一个入口(lib或binary)+依赖链的模式。同样类似的Set<mod> - 模块(module): 代码构成单元,对象命名空间
Tip
理解语义即可,rust的命名简直瞎搞。
package
二进制package
表示默认的crate是二进制的,也就是src/main.rs是入口文件,此时crate的名称同package名。
cargo new my-project库package
crate是lib模式,入口文件为src/lib.rs,此时crate的名称依然是package名。
cargo new my-lib --libTip
lib package可以和binary package同名,没有影响
典型package
├── Cargo.toml
├── Cargo.lock
├── src
│ ├── main.rs # binary crate,和pacakge同名
│ ├── lib.rs # lib crate, 和pacage同名
│ └── bin
│ └── main1.rs # binary crate,名称就是文件名
│ └── main2.rs # 同上
├── tests
│ └── some_integration_tests.rs
├── benches
│ └── simple_bench.rs
└── examples
└── simple_example.rs
模块
mod类型
- 文件
mod: 只有在其他文件中通过显示mod声明时,这个被声明的文件才变成文件mod,否则它就是一个普通的文件,rust甚至不会看见它。这种mod声明本质是建立链接。mod block: 直接声明的mod块这2个
mod在使用上没有任何区别,只是为了方便理解语义。
要点:
- 模块必须在crate的语境下
- rust的模块本质是编译链,这是和其他语言最大的区别,比如go,ts,python,lua等的”模块”系统,和文件系统深度绑定。rust的模块跟随编译链走,从编译链的入口模块开始,最后形成一棵以入口模块为根的模块树(不是标准多叉树),所以广义的crate指的实际是这棵树。
- 目录必须转换为模块才能被找到,有2种方式:
<dir>/mod.rs。对,就是python/lua的模式<div>.rs,创建一个同级同名的rs文件
- 文件模块使用前,必须显示使用
mod来声明(链接到树上),rust完全不会主动去文件系统中找 - 模块和文件不是一对一,一个文件可以定义多个
mod,这个mod可以是文件mod或者mod block mod可以嵌套,当然,不然目录也没必要转换为mod了。mod block同样支持嵌套。- 只有同一个作用域的对象(
mod也是对象)才是相互可见的,否则必须显示pub修饰。
Tip
这里也能看出来rust不太喜欢主动替用户做决定。
use
"use" identifier{"::"identifier}[ "as" identifier]";";很多语言都是直接从文件树上找,但是rust必须先用mod形成树,然后use才能从这棵树上找对象。
use第三方crate
当第三方依赖添加进来后,本质是它的crate暴露在了项目crate的runtime,默认暴露的还有内置crate,比如std。
所以可以总结use的含义: 从runtime中的crate和当前模块树搜索对象。
方式
use的对象在当前模块的作用域中,属于当前模块了,当然也可以暴露出去,使用pub修饰即可use多个,可以使用{obj,...}表示- 当
self不在use的最前面时,表示导入的这个模块本身,use std::io::{self, Write}; *导入所有,use std::collections::*;
绝对路径和相对路径
在use中,关键字crate特指当前crate的根mod,类似/的意义。
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}最佳实践
并没有规定和建议强制使用哪种形式,但是有个原则,局部的用相对,全局的用绝对
特殊标识
super: 类似../的概念,用于嵌套modself: 表示当前mod
可见性
pub定义可见,注意每一层都要显示暴露。
还可以更加细化:
pub意味着可见性无任何限制pub(crate)表示在当前包可见pub(self)在当前模块可见,和什么都不修饰的private行为一致,当前和子模块中可以访问。这个主要用于宏编程的显示可见性设置。pub(super)在父模块可见pub(in )表示在某个路径代表的模块中可见,其中path必须是父模块或者祖先模块
基础
变量
-
rust的变量,默认是immutable的,必须显示的使用
mut,比如let mut a : i32 = 1来声明。 -
rust也可以多次
let同一个变量名,来赋值对应作用域之前的。这和重新赋值不同,这是重新分配了一次内存(正常情况下),后面对这个变量的使用,都使用的是最新地址,而且原来的那个内存不会因为被遮蔽而释放,它依然遵循常规的释放逻辑。 -
rust支持非常方便的解构赋值
let (a, b, c):(i32, i32, i32) = (1, 2, 3);
基本类型
类型的标识有多种形式, 以下表示的含义完全相同:
let a: i32 = 1; // 1此时就是i32,如果a: i64 = 1,那1就是i64
let a: i32 = 1i32; // 显示指定
let a: i32 = 1_i32; // 显示指定
let a = 1i32; // a被推断为i32
let a = 1_i32; // a被推断为i32数值类型
整数
可以参考chapter4的整数类型.
同样iszie和usize大小,取决于CPU的位数。
Note
rust的整数表示,比如i8, i16, i32, u8, u16, u32, f32, f64等非常直观且容易记忆。
默认使用i32
溢出
这是所有静态类型语言都会遇到的问题。
有符号整数的大小: -2^n-1 ~ (2^n-1)-1
无符号整数的大小: 0 ~ (2^n-1)-1
rust的溢出处理默认使用”补码循环溢出”逻辑,表现上就是高位丢弃,比如255u8 + 1u8 就会变成0u8。
也可以显示处理,给方法加上对应的前缀:
- 使用
wrapping_*方法在所有模式下都按照补码循环溢出规则处理,例如 wrapping_add - 如果使用
checked_*方法时发生溢出,则返回 None 值 - 使用
overflowing_*方法返回该值和一个指示是否存在溢出的布尔值 - 使用
saturating_*方法,可以限定计算后的结果不超过目标类型的最大值或低于最小值
// all is true
assert_eq!(255u8.wrapping_add(1), 0);
assert_eq!(255u8.checked_add(1), None);
assert_eq!(255u8.overflowing_add(1), (0, true));
assert_eq!(u8::MAX.saturating_add(1), u8::MAX);浮点数
默认为f64
基于IEEE754实现,可以参考IEEE754浮点数标准,浮点数存储标准IEEE-754也进行了简单说明。
根据标准也可以知道,二进制是无法精确的表示十进制的浮点数的,所以计算机的浮点数都是近似的表达,书中也明确提到:
-
避免在浮点数上测试相等性。因为2个不同的小数的浮点数位可能相同,毕竟精度会丢失。
assert!(0.1 + 0.2 == 0.3); // assertion failed assert!(0.1f32 + 0.2f32 == 0.3f32); // assertion successed,32位的近似表达又是一致的 assert!(0.1 + 0.1 == 0.2); // assertion true,0.1 + 0.1的近似表达 == 0.2的近似表达 -
当结果在数学上可能存在未定义时,需要格外的小心
Note
基本使用IEEE754实现的,都有这个问题,包括python, java等等
更直观的例子:
在浮点表达上,32位由于精度较差,刚好0.1 + 0.2和0.3的近似表达就是一致的,但是64位则不一样,因为它们的精度更高。
let abc: (f32, f32, f32) = (0.1, 0.2, 0.3);
let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3);
println!("abc (f32)");
println!(" 0.1 + 0.2: {:x}", (abc.0 + abc.1).to_bits()); // 3e99999a
println!(" 0.3: {:x}", (abc.2).to_bits()); // 3e99999a
println!("xyz (f64)");
println!(" 0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits()); // 3fd3333333333334
println!(" 0.3: {:x}", (xyz.2).to_bits()); // 3fd3333333333333
assert!(abc.0 + abc.1 == abc.2); // successed
assert!(xyz.0 + xyz.1 == xyz.2); // failedNaN
数学上未定义的结果,被处理为NaN(not a number),NaN不能被比较。
基本运算
- 只有同类型的才能进行运算,没有所谓的隐式转换。
- 长数字,比如
100,000,可以表示为100_000。
位运算
见chapter15中第4点。
Caution
移位不能超过类型的位数,比如u8,
<<,>>最多只能移7位
Range
模型基本类似python的range
..: 不包含右边界..=: 包含右边界
字符本质是u32(c是u8),所以也可以用于range。
for i in 1..=5 {
println!("{}", i);
}
for c in 'a'..'e' {
println!("{}", c);
}As 类型转换
let a: i32 = 10;
let b: u16 = 100; // 将 i32 转换为 f64
let c = a as f64; // 将 u16 转换为 i32
let d = b as i32; // 甚至可以将浮点数直接截断为整数
let e = 3.14_f32 as i32; // 结果为 3这个是比较粗暴的转换,如果需要更安全的可以使用try_into()
有理数和复数
本质就是非具体的数值表达,而是”结构”表达, 毕竟计算机无法表示所有数(连浮点数都无法准确表示)。结构表达可以规避一些问题比如精度丢失,长度太长等,比如1/3,根本无法准确表示,但是却可以直接用1/3表示。
Note
对于很多精度要求高的场景,比如财务,用
i64,f64有时候是无法表达的,这些类型都有上限。
甚至有些数自然界都没有,比如复数,只能结构表达。
Tip
num库提供了复数类型,可以用num::complex::Complex表示。
字符、布尔、单元类型
-
char. 不同于c的一个字节,rust中的char是一个Unicode码点(严格来说是标量值,筛选过的码点),用4个字节表示,所以可以表示任意Unicode字符,包括汉字。和字符非常相似。
-
bool. true or false, 一个字节表示
-
单元类型. 用
()表示,且拥有唯一的值(),可以用于函数返回值,比如fn main()就是返回(),()内存占用为0,但是依然分配地址,但是这个地址可以看作是”虚拟的”,实际不存在。None,void, 但是为什么用empty tuple?作用像
语句和表达式
- 语句:
let a = 1; - 表达式:
1 + 2, 如果加上;就会变成语句
表达式总是会返回值,语句则没有。
下面的都是表达式:
1, 就返回1{ let x = 1; x + 1 }, 没错,语句块也是,所以可以把函数体也当作一个表达式。最后一个表达式的返回值就是语句块的返回值,如果块的最后不是表达式,则使用隐含的()作为表达式,也就是返回()。if xxx { 1 } else { 2 }, 也是表达式some_fn()
Tip
所以函数可以省略
return,因为默认会返回最后一个表达式
函数
没有什么特殊的,关键字,函数名,形参列表,返回类型,函数体。

fn add(a: i32, b: i32) -> i32 {
a + b
}没有或显示使用()作为返回类型,表示函数返回(),该类型的唯一值。
Typescript和Python都有Never的概念,作为返回类型时,表示永不返回,也就是不会执行到return(比如异常,死循环等等),rust用!表示同样的概念(这是什么奇怪的关键字想法)。
fn dead_end() -> ! {
panic!("你已经到了穷途末路,崩溃吧!");
}所有权和借用
内存管理方式
- 手动GC: c, c++
- 自动GC: go, java, python等等
- 通过所有权管理: rust
所有权管理的本质,依然是用户主导,只是释放内存的逻辑被融合进了语义中,不再是显示的释放,完全依靠编译器去实现,简单的说c是纯手动,rust则提供了开关。
内存分区
一般的程序运行时大致有3个内存区域:
- 栈: 函数的调用都会进行入栈和出栈,栈用于管理地址跳转逻辑,变量的内存分配,返回值等等。具体查看stack;
- 堆: 操作系统视角,就是一块内存区域,用于存储复杂数据。栈一般是比较小的。
- 数据段: 静态数据,全局数据,一般只读,随着程序加载写入.
类型与内存分配
在不同的语言中,都有这种逻辑,根据类型,构造的对象,一般会直接分配1个或2个内存,即所谓的引用类型和值类型。 rust的所有权、借用、共享等对象之间的关系,都和类型强关联,通过类型来实现。
内存对象之间的关系
这里的内存对象指运行时分配的内存地址内容,且和内存位置无关,不管栈、堆还是数据段。
内存对象之间的关系,本质是资源管理的方式具现,比如:
- 所有权: 我拥有你,我管理你,我的周期结束,你的周期也结束
- 借用:所有权没有转移,我们建立临时的关系,但是我的生命周期不允许超过你
- 共享:我们都有你的所有权,通过引用计数的方式管理资源
说明
本文把分配的内存对象,其中的入口对象,称为访问入口对象
所有权
在处理2个内存对象类型的资源释放的问题时,传统的多个引用(多个所有者),释放时必须检查所有引用者的生命周期是否都结束了,否则会造成空指针,而如果有引用没有正确的释放,又会造成内存泄露。
rust引入所有权,明确管理者,且通常只有一个,这样管理就变的非常简单,我结束,你释放。
那内存对象之间怎么建立什么样的关系,谁来决定?
类型,类型构造对象(逻辑上的对象),类型决定了内存分配方式,类型决定了内存对象之间的关系。 总之类型决定语义,类型决定关系。
Tip
逻辑对象,类型构建的内存对象的统一,狭义上指它的访问入口对象。
| 类型 | 构造形式 | 对象关系 | 代码表现 |
|---|---|---|---|
i32 | 标量 | 各自拥有(Copy) | let b = a; // a,b 都有效 |
Box<T> | 拥有指针 | 唯一拥有(Move) | let b = a; // a 失效 |
&T | 不可变引用 | 借用(只读) | let r = &a; // 不拥有 |
&mut T | 可变引用 | 独占借用(可修改) | let r = &mut a; // 独占 |
Rc<T> | 共享指针 | 共享拥有 | Rc::clone(&a) // 计数+1 |
Option<T> | 可选 | 可能有/无 | Some(v) / None |
Vec<T> | 集合 | 拥有多个 | vec![a,b,c] // 拥有所有 |
*mut T | 裸指针 | 无关系(unsafe) | 自己管理 |
根据上表的内容,不同的类型意味着不同的关系和管理模式。
- 通常分配一个内存的,它的所有者就是自己,它的赋值总是COPY,它不管理其他资源,它的释放就是简单的释放自己
- 通常分配两个内存的,它就有所有者了,它的赋值总是MOVE,转移了所有者,释放它,会同时释放它拥有的。
而借用和共享等的形式,也有对应的类型,他们也涉及多个内存对象:
- 借用,给予访问权限,但是编译器不允许借用者的生命周期超过被借用者,所以这是一个局部优化措施
- 共享,引用计数的形式,计数归0,就释放。这是妥协?
关于只分配一个内存的,比如i32,它的所有者就是自己,它出栈,内存释放,这是它在栈中的情况,如果这种类型在堆中呢,怎么释放?
rust不允许这种以裸值的形式出现在堆中,因为这无法安全的暴露它的访问方式,除非使用裸指针,但是这是unsafe的,需要手动释放。实际上所有的访问入口对象都不能以裸值的形式出现在堆中,除非unsafe。
Tip
同一个值,不同的类型,有不同的逻辑表达,裸指针也是指针,但是rust并不做额外的控制,它此时就像c的指针一样,完全由用户管理。
引用和借用
如果只有所有权转移,那程序的表达能力将非常弱,需要大量的控制代码,心智负端极大。有些轻量使用场景,我只是想临时获取引用,这时候使用简单的“借用”就可以了。
Tip
这里也可以看出来,一个普通的逻辑行为,都做了不同的表达,实际上整个rust中有大量类似的行为,在其他语言中普通的操作,到这里都进行了细化,以便更精确的控制。理解上要困难得多,对比c,多了大量的抽象控制表达,对比go,把控制权交给了用户。
取址
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}&取址操作,注意这和c,go的不太一样的是,会根据类型获取到瘦指针或胖指针。
Tip
瘦指针: 纯地址 胖指针: 地址 + 其他元信息
比如go的slice,map, string,在栈中实际都是所谓胖指针,但是go并不把他们当作指针,而在rust中他们都是指针。
&取址后的类型是对应的&<type>,这个和*<type>裸指针的区别是,它是安全的,而*<type>是不安全的,也就是不保证对应的地址有内容。但是他们2者表达的是一个内容,即指针类型(包括胖指针)。
借用
通过取址,获得访问权限,就是“借用”。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len() // s -> s1,操作时自动解引用
}
这看起来像间接指针,实际也是,但是表达的逻辑是借用。
可变引用(借用)
fn main() {
let mut s = String::from("hello"); // 这里也必须是可修改的
change(&mut s); // &mut 可变借用
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}注意: 同一作用域,同一数据只能有一个可变,且可变和不可变不能同时存在。
悬垂引用
不管用什么概念来包装,此时既然是取址,那就可能存在被取址的释放了,但是它的地址还存在某处,形成空指针。 不过rust在编译阶段就阻止了这种行为,帮你避免了panic。
// 编译错误
fn main() {
// dangle执行完,s就释放了,s的地址返回给了reference_to_nothing,它指向不存在的内存
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}