Rust太难?那是你没看到这套Rust语言学习万字指南!
一、Rust开发环境指南
1.1 Rust代码执行
根据编译原理知识,编译器不是直接将源语言翻译为目标语言,而是翻译为一种“中间语言”,编译器从业人员称之为“IR”--指令集,之后再由中间语言,利用后端程序和设备翻译为目标平台的汇编语言。
Rust代码执行:
1) Rust代码经过分词和解析,生成AST(抽象语法树)。
2) 然后把AST进一步简化处理为HIR(High-level IR),目的是让编译器更方便的做类型检查。
3) HIR会进一步被编译为MIR(Middle IR),这是一种中间表示,主要目的是:
a) 缩短编译时间;
b) 缩短执行时间;
c) 更精确的类型检查。
4) 最终MIR会被翻译为LLVM IR,然后被LLVM的处理编译为能在各个平台上运行的目标机器码。
Ø IR:中间语言
Ø HIR:高级中间语言
Ø MIR:中级中间语言
Ø LLVM :Low Level Virtual Machine,底层虚拟机。
LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time)
无疑,不同编译器的中间语言IR是不一样的,而IR可以说是集中体现了这款编译器的特征:他的算法,优化方式,汇编流程等等,想要完全掌握某种编译器的工作和运行原理,分析和学习这款编译器的中间语言无疑是重要手段。
由于中间语言相当于一款编译器前端和后端的“桥梁”,如果我们想进行基于LLVM的后端移植,无疑需要开发出对应目标平台的编译器后端,想要顺利完成这一工作,透彻了解LLVM的中间语言无疑是非常必要的工作。
LLVM相对于gcc的一大改进就是大大提高了中间语言的生成效率和可读性, LLVM的中间语言是一种介于c语言和汇编语言的格式,他既有高级语言的可读性,又能比较全面地反映计算机底层数据的运算和传输的情况,精炼而又高效。
1.1.1 MIR
MIR是基于控制流图(Control Flow Graph,CFG)的抽象数据结构,它用有向图(DAG)形式包含了程序执行过程中所有可能的流程。所以将基于MIR的借用检查称为非词法作用域的生命周期。
MIR由一下关键部分组成:
- l
基本块(Basic block,bb),他是控制流图的基本单位,
Ø 语句(statement)
Ø 终止句(Terminator)
- l
本地变量,占中内存的位置,比如函数参数、局部变量等。 - l
位置(Place),在内存中标识未知的额表达式。 - l
右值(RValue),产生值的表达式。
具体的工作原理见《Rust编程之道》的第158和159页。
可以在http://play.runst-lang.org中生成MIR代码。
1.1 Rust安装
Ø 方法一:见Rust官方的installation章节介绍。
实际上就是调用该命令来安装即可:curl https://sh.rustup.rs -sSf | sh
Ø 方法二:下载离线的安装包来安装,具体的可见Rust官方的Other Rust Installation Methods章节。
1.2 Rust编译&运行
1.2.1 Cargo包管理
Cargo是Rust中的包管理工具,第三方包叫做crate。
Cargo一共做了四件事:
- l 使用两个元数据(
metadata)文件来记录各种项目信息 - l 获取并构建项目的依赖关系
- l 使用正确的参数调用
rustc或其他构建工具来构建项目 - l 为Rust生态系统开发建议了统一标准的工作流
Cargo文件:
- l
Cargo.lock:只记录依赖包的详细信息,不需要开发者维护,而是由Cargo自动维护 - l
Cargo.toml:描述项目所需要的各种信息,包括第三方包的依赖
cargo编译默认为Debug模式,在该模式下编译器不会对代码进行任何优化。也可以使用--release参数来使用发布模式。release模式,编译器会对代码进行优化,使得编译时间变慢,但是代码运行速度会变快。
官方编译器rustc,负责将rust源码编译为可执行的文件或其他文件(.a、.so、.lib等)。例如:rustc box.rs
Rust还提供了包管理器Cargo来管理整个工作流程。例如:
lcargo newfirst_pro_create:创建名为first_pro_create的项目lcargo new --libfirst_lib_create:创建命令first_lib_create的库项目lcargo doclcargo doc --openlcargo testlcargo test -- --test-threads=1lcargo buildlcargo build --releaselcargo runlcargo install --pathlcargo uninstallfirst_pro_createlcargo new –bin use_regex

1.2.2 使用第三方包
Rust可以在Cargo.toml中的[dependencies]下添加想依赖的包来使用第三方包。
然后在src/main.rs或src/lib.rs文件中,使用extern crate命令声明引入该包即可使用。
例如:

值得注意的是,使用extern crate声明包的名称是linked_list,用的是下划线“_”,而在Cargo.toml中用的是连字符“-”。其实Cargo默认会把连字符转换成下划线。
Rust也不建议以“-rs”或“_rs”为后缀来命名包名,而且会强制性的将此后缀去掉。
具体的见《Rust编程之道》的第323页。
1.4 Rust常用命令
1.5 Rust命令规范
Ø 函数: 蛇形命名法(snake_case),例如:func_name()
Ø 文件名: 蛇形命名法(snake_case),例如file_name.rs、main.rs
Ø 临时变量名:蛇形命名法(snake_case)
Ø 全局变量名:
Ø 结构体: 大驼峰命名法,例如:struct FirstName { name: String}
Ø enum类型: 大驼峰命名法。
Ø 关联常量:常量名必须全部大写。什么是关联常量见《Rust编程之道》的第221页。
Ø Cargo默认会把连字符“-”转换成下划线“_”。
Ø Rust也不建议以“-rs”或“_rs”为后缀来命名包名,而且会强制性的将此后缀去掉。
二、Rust语法
2.1 疑问&总结
2.1.1 Copy语义 && Move语义(Move语义必须转移所有权)
类型越来越丰富,值类型和引用类型难以描述全部情况,所以引入了:
Ø 值语义(Value Semantic)
复制以后,两个数据对象拥有的存储空间是独立的,互不影响。
基本的原生类型都是值语义,这些类型也被称为POD(Plain old data)。POD类型都是值语义,但是值语义类型并不一定都是POD类型。
具有值语义的原生类型,在其作为右值进行赋值操作时,编译器会对其进行按位复制。
Ø 引用语义(Reference Semantic)
复制以后,两个数据对象互为别名。操作其中任意一个数据对象,则会影响另外一个。
智能指针Box<T>封装了原生指针,是典型的引用类型。Box<T>无法实现Copy,意味着它被rust标记为了引用语义,禁止按位复制。
引用语义类型不能实现Copy,但可以实现Clone的clone方法,以实现深复制。
在Rust中,可以通过是否实现Copy trait来区分数据类型的值语义和引用语义。但为了更加精准,Rust也引用了新的语义:复制(Copy)语义和移动(Move)语义。
Ø Copy语义:对应值语义,即实现了Copy的类型在进行按位复制时是安全的。
Ø Move语义:对应引用语义。在Rust中不允许按位复制,只允许移动所有权。
2.1.2 哪些实现了Copy
Ø 结构体 :当成员都是复制语义类型时,不会自动实现Copy。
Ø 枚举体 :当成员都是复制语义类型时,不会自动实现Copy。
结构体 && 枚举体:
1) 所有成员都是复制语义类型时,需要添加属性#[derive(Debug,Copy,Clone)]来实现Copy。
2) 如果有移动语义类型的成员,则无法实现Copy。
Ø 元组类型 :本身实现了Copy。如果元素均为复制语义类型,则默认是按位复制,否则执行移动语义。
Ø 字符串字面量 &str: 支持按位复制。例如:c = “hello”; 则c就是字符串字面量。
2.1.3 哪些未实现Copy
Ø 字符串对象String :to_string() 可以将字符串字面量转换为字符串对象。
2.1.4 哪些实现了Copy trait
Ø 原生整数类型
对于实现Copy的类型,其clone方法只需要简单的实现按位复制即可。
2.1.5 哪些未实现Copy trait
Ø Box<T>
实现了Copy trait,有什么作用?
实现Copy trait的类型同时拥有复制语义,在进行赋值或者传入函数等操作时,默认会进行按位复制。
Ø 对于默认可以安全的在栈上进行按位复制的类型,就只需要按位复制,也方便管理内存。
Ø 对于默认只可在堆上存储的数据,必须进行深度复制。深度复制需要在堆内存中重新开辟空间,这会带来更多的性能开销。
2.1.6 哪些是在栈上的?哪些是在堆上的?
2.1.7 let绑定
Ø Rust声明的绑定默认为不可变。
Ø 如果需要修改,可以用mut来声明绑定是可变的。
2.2 数据类型
很多编程语言中的数据类型是分为两类:
Ø 值类型
一般是指可以将数据都保存在同一位置的类型。例如数值、布尔值、结构体等都是值类型。
值类型有:
l原生类型l结构体l枚举体
Ø 引用类型
会存在一个指向实际存储区的指针。比如通常一些引用类型会将数据存储在堆中,而栈中只存放指向堆中数据的地址(指针)。
引用类型有:
l普通引用类型l原生指针类型
2.2.1 基本数据类型
布尔类型
bool类型只有两个值:true和false。
基本数字类型
主要关注取值范围,具体的见《Rust编程之道》的第26页。
字符类型
用单引号来定义字符(char)类型。字符类型代表一个Unicode标量值,每个字节占4个字节。
数组类型
数组的类型签名为[T; N]。T是一个泛型标记,代表数组中元素的某个具体类型。N代表数组长度,在编译时必须确定其值。
数组特点:
- l 大小固定
- l 元素均为同类型
- l 默认不可变
切片类型
切片(Slice)类型是对一个数组的引用片段。在底层,切片代表一个指向数组起始位置的指针和数组长度。用[T]类型表示连续序列,那么切片类型就是&[T]和&mut[T]。
具体的见《Rust编程之道》的第30页。
str字符串类型
字符串类型str,通常是以不可变借用的形式存在,即&str(字符串切片)。
Rust将字符串分为两种:
1) &str :固定长度字符串
2) String :可以随意改变其长度。
&str字符串类型由两部分组成:
1) 指向字符串序列的指针;
2) 记录长度的值。
&str存储于栈上,str字符串序列存储于程序的静态只读数据段或者堆内存中。
&str是一种胖指针。
never类型
never类型,即!。该类型用于表示永远不可能有返回值的计算类型。
其他(此部分不属于基本数据类型)
此部分不属于基本数据类型,由于编排问题,暂时先放在此处。
胖指针
胖指针:包含了动态大小类型地址信息和携带了长度信息的指针。
具体的见《Rust编程之道》的第54页。
零大小类型
零大小类型(Zero sized Type,ZST)的特点是:它们的值就是其本身,运行时并不占用内存空间。
单元类型和单元结构体大小为零,由单元类型组成的数组大小也是零。
ZST类型代表的意义是“空”。
底类型
底类型其实是介绍过的never类型,用叹号(!)表示。它的特点是:
- l 没有值
- l 是其他任意类型的子类型
如果说ZST类型表示“空”的话,那么底类型就表示“无”。
底类型无值,而且它可以等价于任意类型。
具体的见《Rust编程之道》的第57页。
2.2.2 复合数据类型
元组
Rust提供了4中复合数据类型:
l元组(Tuple)l结构体(Struct)l枚举体(Enum)l联合体(Union)
先来介绍元组。元组是一种异构有限序列,形如(T,U,M,N)。所谓异构,就是指元组内的元素可以是不同类型。所谓有限,是指元组有固定的长度。
- l 空元组:
() - l 只有一个值时,需要加逗号:
(0,)
结构体
Rust提供了3中结构体:
l具名结构体l元组结构体l单元结构体
例如:
Ø 具名结构体:
struct People {
name: &’static str,
}
Ø 元组结构体:字段没有名称,只有类型:
struct Color(i32, i32, i32);
当一个元组结构体只有一个字段的时候,称为New Type模式。例如:
struct Integer(u32);
Ø 单元结构体:没有任何字段的结构体。单元结构体实例就是其本身。
struct Empty;
结构体更新语法
使用Struct更新语法(..)从其他实例创建新实例。当新实例使用旧实例的大部分值时,可以使用struct update语法。 例如:
#[derive(Debug,Copy,Clone)]
struct Book<’a> {
name: &’a str,
isbn: i32,
version: i32,
}
let book = Book {
name: “Rust编程之道”, isbn: 20181212, version: 1
};
let book2 = Book {version: 2, ..book};
注:
- l 如果结构体使用了移动语义的成员字段,则不允许实现Copy。
- l Rust不允许包含了String类型字段的结构体实现Copy。
- l 更新语法会转移字段的所有权。
枚举体
该类型包含了全部可能的情况,可以有效的防止用户提供无效值。例如:
enum Number {
Zero,
One,
}
Rust还支持携带类型参数的枚举体。这样的枚举值本质上属于函数类型,他可以通过显式的指定类型来转换为函数指针类型。例如:
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
枚举体在Rust中属于非常重要的类型之一。例如:Option枚举类型。
联合体
2.2.3 常用集合类型
线性序列:向量
在Rust标准库std::collections模块下有4中通用集合类型,分别如下:
- 线性序列:
向量(Vec)、双端队列(VecDeque)、链表(LinkedList) - Key-Value映射表:
无序哈希表(HashMap)、有序映射表(BTreeMap) - 集合类型:
无序集合(HashSet)、有序集合(BTreeSet) - 优先队列:
二叉堆(BinaryHeap)
具体的见《Rust编程之道》的第38页和271页。
向量也是一种数组,和基本数据类型中的数组的区别在于:向量可动态增长。
示例:
let mut v1 = vec![];
let mut v2 = vec![0; 10];
let mut v3 = Vec::new();
vec!是一个宏,用来创建向量字面量。
线性序列:双端队列
双端队列(Double-ended Queue,缩写Deque)是一种同时具有队列(先进先出)和栈(后进先出)性质的数据结构。
双端队列中的元素可以从两端弹出,插入和删除操作被限定在队列的两端进行。
示例:
use std::collections::VecDeque;
let mut buf = VecDeque::new();
buf.push_front(1);
buf.get(0);
buf.push_back(2);
线性序列:链表
Rust提供的链表是双向链表,允许在任意一端插入或弹出元素。最好使用Vec或VecDeque类型,他们比链表更加快速,内存访问效率更高。
示例:
use std::collections::LinkedList;
let mut list = LinkedList::new();
list.push_front(‘a’);
list.append(&mut list2);
list.push_back(‘b’);
Key-Value映射表:HashMap和BTreeMap
HashMap<K, V>=> 无序BTreeMap<K, V>=> 有序
其中HashMap要求key是必须可哈希的类型,BTreeMap的key必须是可排序的。
Value必须是在编译期已知大小的类型。
示例:
use std::collections::BTreeMap;
use std::collections::HashMap;
let mut hmap = HashMap::new();
let mut bmap = BTreeMap::new();
hmap.insert(1,”a”);
bmap.insert(1,”a”);
集合:HashSet和BTreeSet
HashSet<K>和BTreeSet<K>其实就是HashMap<K, V>和BTreeMap<K, V>把Value设置为空元组的特定类型。
- l 集合中的元素应该是唯一的。
- l
HashSet中的元素都是可哈希的类型,BTreeSet中的元素必须是可排序的。 - l
HashSet应该是无序的,BTreeSet应该是有序的。
示例:
use std::collections::BTreeSet;
use std::collections::HashSet;
let mut hset = HashSet::new();
let mut bset = BTreeSet::new();
hset.insert(”This is a hset.”);
bset.insert(”This is a bset”);
优先队列:BinaryHeap
Rust提供的优先队列是基于二叉最大堆(Binary Heap)实现的。
示例:
use std::collections::BinaryHeap;
let mut heap = BinaryHeap::new();
heap.peek(); => peek是取出堆中最大的元素
heap.push(98);
容量(Capacity)和大小(Size/Len)
无论是Vec还是HashMap,使用这些集合容器类型,最重要的是理解容量(Capacity)和大小(Size/Len)。
容量是指为集合容器分配的内存容量。
大小是指集合中包含的元素数量。
2.2.4 Rust字符串
Rust字符串分为以下几种类型:
- l
str:表示固定长度的字符串 - l
String:表示可增长的字符串 - l
CStr:表示由C分配而被Rust借用的字符串。这是为了兼容windows系统。 - l
CString:表示由Rust分配且可以传递给C函数使用的C字符串,同样用于和C语言交互。 - l
OsStr:表示和操作系统相关的字符串。这是为了兼容windows系统。 - l
OsString:表示OsStr的可变版本。与Rust字符串可以相互交换。 - l
Path:表示路径,定义于std::path模块中。Path包装了OsStr。 - l
PathBuf:跟Path配对,是path的可变版本。PathBuf包装了OsString。
str属于动态大小类型(DST),在编译期并不能确定其大小。所以在程序中最常见的是str的切片(Slice)类型&str。
&str代表的是不可变的UTF-8字节序列,创建后无法再为其追加内容或更改其内容。&str类型的字符串可以存储在任意地方:
Ø 静态存储区
Ø 堆分配
Ø 栈分配
具体的见《Rust编程之道》的第249页。
String类型本质是一个成员变量为Vec<u8>类型的结构体,所以它是直接将字符内容存放于堆中的。
String类型由三部分组成:
http://www.jintianxuesha.com/?id=1191
http://www.jintianxuesha.com/?id=1084
http://www.jintianxuesha.com/?id=1085
Ø 执行堆中字节序列的指针(as_ptr方法)
Ø 记录堆中字节序列的字节长度(len方法)
Ø 堆分配的容量(capacity方法)
2.2.4.1 字符串处理方式
Rust中的字符串不能使用索引访问其中的字符,可以通过bytes和chars两个方法来分别返回按字节和按字符迭代的迭代器。
Rust提供了另外两种方法:get和get_mut来通过指定索引范围来获取字符串切片。
具体的见《Rust编程之道》的第251页。
2.2.4.2 字符串修改
Ø 追加字符串:push和push_str,以及extend迭代器
Ø 插入字符串:insert和insert_str
Ø 连接字符串:String实现了Add<&str>和AddAssign<&str>两个trait,所以可以使用“+”和“+=”来连接字符串
Ø 更新字符串:通过迭代器或者某些unsafe的方法
Ø 删除字符串:remove、pop、truncate、clear和drain
具体的见《Rust编程之道》的第255页。
2.2.4.3 字符串的查找
Rust总共提供了20个方法涵盖了以下几种字符串匹配操作:
Ø 存在性判断
Ø 位置匹配
Ø 分割字符串
Ø 捕获匹配
Ø 删除匹配
Ø 替代匹配
具体的见《Rust编程之道》的第256页。
2.2.4.4 类型转换
Ø parse:将字符串转换为指定的类型
Ø format!宏:将其他类型转成成字符串
2.2.5 格式化规则
- l 填充字符串宽度:
{:5},5是指宽度为5 l截取字符串:{:.5}l对齐字符串:{:>}、{:^}、{:<},分别表示左对齐、位于中间和右对齐l{:*^5}使用*替代默认空格来填充
- l 符号
+:表示强制输出整数的正负符号 - l 符号
#:用于显示进制的前缀。比如:十六进制0x - l 数字
0:用于把默认填充的空格替换成数字0 - l
{:x}:转换成16进制输出 - l
{:b}:转换成二进制输出 l{:.5}:指定小数点后有效位是5- l
{:e}:科学计数法表示
具体的见《Rust编程之道》的第265页。
2.2.6 原生字符串声明语法:r”…”
原生字符串声明语法(r”…”)可以保留原来字符串中的特殊符号。
具体的见《Rust编程之道》的第270页。
2.2.7 全局类型
Rust支持两种全局类型:
- l
普通常量(Constant) - l
静态变量(Static)
区别:
- l 都是在
编译期求值的,所以不能用于存储需要动态分配内存的类型 - l 普通常量
可以被内联的,它没有确定的内存地址,不可变 - l 静态变量
不能被内联,它有精确的内存地址,拥有静态生命周期 - l 静态变量可以通过内部包含UnsafeCell等容器
实现内部可变性 - l 静态变量还有其他限制,具体的见《Rust编程之道》的第326页
- l 普通常量也不能引用静态变量
在存储的数据比较大,需要引用地址或具有可变性的情况下使用静态变量。否则,应该优先使用普通常量。
但也有一些情况是这两种全局类型无法满足的,比如想要使用全局的HashMap,在这种情况下,推荐使用lazy_static包。利用lazy_static包可以把定义全局静态变量延迟到运行时,而非编译时。
2.3 trait
trait是对类型行为的抽象。trait是Rust实现零成本抽象的基石,它有如下机制:
- l trait是Rust唯一的接口抽象方式;
- l 可以静态分发,也可以动态分发;
- l 可以当做标记类型拥有某些特定行为的“标签”来使用。
示例:
struct Duck;
struct Pig;
trait Fly {
fn fly(&self) -> bool;
}
impl Fly for Duck {
fn fly(&self) -> bool {
return true;
}
}
impl Fly for Pig {
fn fly(&self) -> bool {
return false;
}
}
静态分发和动态分发的具体介绍可见《Rust编程之道》的第46页。
trait限定
以下这些需要继续深入理解第三章并总结。待后续继续补充。
trait对象
标签trait
Copy trait
Deref解引用
as操作符
From和Into
2.4 指针
2.3.1 引用Reference
用&和& mut操作符来创建。受Rust的安全检查规则的限制。
引用是Rust提供的一种指针语义。引用是基于指针的实现,他与指针的区别是:指针保存的是其指向内存的地址,而引用可以看做某块内存的别名(Alias)。

浙公网安备 33010602011771号