摘要:从开发环境、语法、属性、内存管理和Unicode等五部分,为你带来一份详细的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的处理编译为能在各个平台上运行的目标机器码。 Ø Ø Ø Ø LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time) 无疑,不同编译器的中间语言IR是不一样的,而IR可以说是集中体现了这款编译器的特征:他的算法,优化方式,汇编流程等等,想要完全掌握某种编译器的工作和运行原理,分析和学习这款编译器的中间语言无疑是重要手段。 由于中间语言相当于一款编译器前端和后端的“桥梁”,如果我们想进行基于LLVM的后端移植,无疑需要开发出对应目标平台的编译器后端,想要顺利完成这一工作,透彻了解LLVM的中间语言无疑是非常必要的工作。 LLVM相对于gcc的一大改进就是大大提高了中间语言的生成效率和可读性, LLVM的中间语言是一种介于c语言和汇编语言的格式,他既有高级语言的可读性,又能比较全面地反映计算机底层数据的运算和传输的情况,精炼而又高效。 1.1.1 MIRMIR是基于控制流图(Control Flow Graph,CFG)的抽象数据结构,它用有向图(DAG)形式包含了程序执行过程中所有可能的流程。所以将基于MIR的借用检查称为非词法作用域的生命周期。 MIR由一下关键部分组成:
Ø 语句(statement) Ø 终止句(Terminator)
具体的工作原理见《Rust编程之道》的第158和159页。 可以在http://play.runst-lang.org中生成MIR代码。 1.1 Rust安装Ø 方法一:见Rust官方的installation章节介绍。 实际上就是调用该命令来安装即可: Ø 方法二:下载离线的安装包来安装,具体的可见Rust官方的Other Rust Installation Methods章节。 1.2 Rust编译&运行1.2.1 Cargo包管理
cargo编译默认为 官方编译器 Rust还提供了包管理器
1.2.2 使用第三方包Rust可以在 然后在 例如: 值得注意的是,使用 Rust也不建议以“ 具体的见《Rust编程之道》的第323页。 1.4 Rust常用命令1.5 Rust命令规范Ø Ø Ø Ø Ø Ø Ø Ø Cargo默认会把 Ø Rust也不建议以“ 二、Rust语法2.1 疑问&总结2.1.1 Copy语义 && Move语义(Move语义必须转移所有权)类型越来越丰富,值类型和引用类型难以描述全部情况,所以引入了: Ø 复制以后,两个数据对象拥有的存储空间是独立的,互不影响。 基本的 具有值语义的原生类型,在其作为右值进行赋值操作时,编译器会对其进行按位复制。 Ø 复制以后,两个数据对象互为别名。操作其中任意一个数据对象,则会影响另外一个。 智能指针 引用语义类型不能实现Copy,但可以实现Clone的clone方法,以实现深复制。 在Rust中,可以通过是否实现Copy trait来区分数据类型的 Ø Ø 2.1.2 哪些实现了CopyØ Ø
1) 所有成员都是复制语义类型时,需要添加属性 2) 如果有移动语义类型的成员,则无法实现Copy。 Ø Ø 2.1.3 哪些未实现CopyØ 2.1.4 哪些实现了Copy traitØ 对于实现Copy的类型,其clone方法只需要简单的实现按位复制即可。 2.1.5 哪些未实现Copy traitØ 实现了Copy trait,有什么作用? 实现Copy trait的类型同时拥有复制语义,在进行赋值或者传入函数等操作时,默认会进行按位复制。 Ø 对于默认可以安全的在栈上进行按位复制的类型,就只需要按位复制,也方便管理内存。 Ø 对于默认只可在堆上存储的数据,必须进行深度复制。深度复制需要在堆内存中重新开辟空间,这会带来更多的性能开销。 2.1.6 哪些是在栈上的?哪些是在堆上的?2.1.7 let绑定Ø Rust声明的绑定默认为不可变。 Ø 如果需要修改,可以用 2.2 数据类型很多编程语言中的数据类型是分为两类: Ø 值类型 一般是指可以将数据都保存在同一位置的类型。例如数值、布尔值、结构体等都是值类型。 值类型有:
Ø 引用类型 会存在一个指向实际存储区的指针。比如通常一些引用类型会将数据存储在堆中,而栈中只存放指向堆中数据的地址(指针)。 引用类型有:
2.2.1 基本数据类型布尔类型bool类型只有两个值: 基本数字类型主要关注取值范围,具体的见《Rust编程之道》的第26页。 字符类型用 数组类型数组的类型签名为 数组特点:
切片类型切片(Slice)类型是对一个数组的引用片段。在底层,切片代表一个指向数组起始位置的指针和数组长度。用 具体的见《Rust编程之道》的第30页。 str字符串类型字符串类型 Rust将字符串分为两种: 1) 2)
1) 指向字符串序列的指针; 2) 记录长度的值。
never类型
其他(此部分不属于基本数据类型)此部分不属于基本数据类型,由于编排问题,暂时先放在此处。 胖指针
具体的见《Rust编程之道》的第54页。 零大小类型零大小类型(Zero sized Type,ZST)的特点是:它们的值就是其本身,运行时并不占用内存空间。
ZST类型代表的意义是“ 底类型
如果说ZST类型表示“
具体的见《Rust编程之道》的第57页。 2.2.2 复合数据类型元组Rust提供了4中复合数据类型:
先来介绍元组。
结构体Rust提供了3中结构体:
例如: Ø 具名结构体: struct People {name: &’static str,} Ø 元组结构体:字段没有名称,只有类型: struct Color(i32, i32, i32); 当一个元组结构体只有一个字段的时候,称为 struct Integer(u32); Ø 单元结构体:没有任何字段的结构体。单元结构体实例就是其本身。 struct Empty; 结构体更新语法使用Struct更新语法( #[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}; 注:
枚举体该类型包含了全部可能的情况,可以有效的防止用户提供无效值。例如: enum Number {Zero,One,} Rust还支持携带类型参数的枚举体。这样的枚举值本质上属于函数类型,他可以通过显式的指定类型来转换为函数指针类型。例如: enum IpAddr { V4(u8, u8, u8, u8),V6(String),} 枚举体在Rust中属于非常重要的类型之一。例如: 联合体2.2.3 常用集合类型线性序列:向量在Rust标准库std::collections模块下有4中通用集合类型,分别如下:
具体的见《Rust编程之道》的第38页和271页。 向量也是一种数组,和基本数据类型中的数组的区别在于:向量可动态增长。 示例: let mut v1 = vec![];let mut v2 = vec![0; 10];let mut v3 = Vec::new();
线性序列:双端队列双端队列(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要求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
示例: 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”); 优先队列:BinaryHeapRust提供的优先队列是基于 示例: use std::collections::BinaryHeap;let mut heap = BinaryHeap::new();heap.peek(); => peek是取出堆中最大的元素heap.push(98); 容量(Capacity)和大小(Size/Len)无论是Vec还是HashMap,使用这些集合容器类型,最重要的是理解
2.2.4 Rust字符串
具体的见《Rust编程之道》的第249页。
Ø 执行堆中字节序列的指针(as_ptr方法) Ø 记录堆中字节序列的字节长度(len方法) Ø 堆分配的容量(capacity方法) 2.2.4.1 字符串处理方式Rust中的字符串不能使用索引访问其中的字符,可以通过 Rust提供了另外两种方法: 具体的见《Rust编程之道》的第251页。 2.2.4.2 字符串修改Ø 追加字符串: Ø 插入字符串: Ø 连接字符串:String实现了 Ø 更新字符串:通过迭代器或者某些unsafe的方法 Ø 删除字符串: 具体的见《Rust编程之道》的第255页。 2.2.4.3 字符串的查找Rust总共提供了20个方法涵盖了以下几种字符串匹配操作: Ø 存在性判断 Ø 位置匹配 Ø 分割字符串 Ø 捕获匹配 Ø 删除匹配 Ø 替代匹配 具体的见《Rust编程之道》的第256页。 2.2.4.4 类型转换Ø Ø 2.2.5 格式化规则
具体的见《Rust编程之道》的第265页。 2.2.6 原生字符串声明语法: | |
2.3 traittrait是对类型行为的抽象。trait是Rust实现零成本抽象的基石,它有如下机制:
示例: 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对象标签traitCopy traitDeref解引用as操作符From和Into2.4 指针2.3.1 引用Reference用 引用是Rust提供的一种指针语义。引用是基于指针的实现,他与指针的区别是:指针保存的是其指向内存的地址,而引用可以看做某块内存的别名(Alias)。 在所有权系统中,引用 2.3.2 原生指针(裸指针)
2.3.3 智能指针实际上是一种结构体,只是行为类似指针。智能指针是对指针的一层封装,提供了一些额外的功能,比如自动释放堆内存。 智能指针区别于常规结构体的特性在于:它实现了 Ø Ø 2.3.3.1 智能指针有哪些智能指针拥有资源的所有权,而普通引用只是对所有权的借用。 Rust中的值默认被分配到栈内存。可以通过Box<T>将值装箱(在堆内存中分配)。 Ø
Box<T>是指向类型为T的堆内存分配值的智能指针。当Box<T>超出作用域范围时,将调用其析构函数,销毁内部对象,并自动释放堆中的内存。
单线程引用计数指针,不是线程安全的类型。 可以将多个所有权共享给多个变量,每当共享一个所有权时,计数就会增加一次。具体的见《Rust编程之道》的第149页。
Ø Copy on write:一种枚举体的智能指针。Cow<T>表示的是所有权的“借用”和“拥有”。Cow<T>的功能是:以不可变的方式访问借用内容,以及在需要可变借用或所有权的时候再克隆一份数据。 Cow<T>旨在减少复制操作,提高性能,一般用于读多写少的场景。 Cow<T>的另一个用处是统一实现规范。 2.3.4 解引用deref解引用会获得所有权。 解引用操作符: * 哪些实现了deref方法Ø Ø Ø Box<T >支持解引用移动, Rc<T>和Arc<T>智能指针不支持解引用移动。 2.4 所有权机制(ownership):Rust中分配的每块内存都有其所有者,所有者负责该内存的释放和读写权限,并且每次每个值只能有唯一的所有者。 在进行赋值操作时,对于可以实现Copy的复制语义类型,所有权并未改变。对于复合类型来说,是复制还是移动,取决于其成员的类型。 例如:如果数组的元素都是基本的数字类型,则该数组是复制语义,则会按位复制。 2.4.1 词法作用域(生命周期)
函数体本身是独立的词法作用域: Ø 当复制语义类型作为函数参数时,会按位复制。 Ø 如果是移动语义作为函数参数,则会转移所有权。 2.4.2 非词法作用域声明周期借用规则: 因为以上的规则,经常导致实际开发不便,所以引入了 MIR是基于控制流图(Control Flow Graph,CFG)的抽象数据结构,它用有向图(DAG)形式包含了程序执行过程中所有可能的流程。所以将基于MIR的借用检查称为非词法作用域的生命周期。 2.4.2 所有权借用使用可变借用的前提是:出借所有权的绑定变量必须是一个可变绑定。 在所有权系统中,引用 引用在离开作用域之时,就是其归还所有权之时。 Ø 不可变借用(引用)不能再次出借为可变借用。 Ø 不可变借用可以被出借多次。 Ø 可变借用只能出借一次。 Ø 不可变借用和可变借用不能同时存在,针对同一个绑定而言。 Ø 借用的生命周期不能长于出借方的生命周期。具体的举例见《Rust编程之道》的第136页。 核心原则:共享不可变,可变不共享。 因为解引用操作会获得所有权,所以在需要对移动语义类型(如&String)进行解引用时需要特别注意。 2.4.3 生命周期参数编译器的借用检查机制无法对跨函数的借用进行检查,因为当前借用的有效性依赖于词法作用域。所以,需要开发者显式的对借用的生命周期参数进行标注。 2.4.3.1 显式生命周期参数Ø 生命周期参数必须是以 Ø 参数名通常都是 Ø 生命周期参数 标注生命周期参数是由于borrowed pointers导致的。因为有borrowed pointers,当函数返回borrowed pointers时,为了保证内存安全,需要关注被借用的内存的生命周期(lifetime)。 标注生命周期参数并不能改变任何引用的生命周期长短,它只用于编译器的借用检查,来防止悬垂指针。即:生命周期参数的目的是帮助借用检查器验证合法的引用,消除悬垂指针。 例如: &i32; ==> 引用&'a i32; ==> 标注生命周期参数的引用&'a mut i32; ==> 标注生命周期参数的可变引用允许使用&'a str;的地方,使用&'static str;也是合法的。对于'static:当borrowed pointers指向static对象时需要声明'static lifetime。如:static STRING: &'static str = "bitstring"; 2.4.3.2 函数签名中的生命周期参数fn foo 函数名后的 规则: Ø 禁止在没有任何输入参数的情况下返回引用,因为会造成悬垂指针。 Ø 从函数中返回(输出)一个引用,其生命周期参数必须与函数的参数(输入)相匹配,否则,标注生命周期参数也毫无意义。 对于多个输入参数的情况,也可以标注不同的生命周期参数。具体的举例见《Rust编程之道》的第139页。 2.4.3.3 结构体定义中的生命周期参数结构体在含有引用类型成员的时候也需要标注生命周期参数,否则编译失败。 例如: struct Foo<'a> {part: &'a str,} 这里生命周期参数标记,实际上是和编译器约定了一个规则: 结构体实例的生命周期应短于或等于任意一个成员的生命周期。 2.4.3.4 方法定义中的生命周期参数结构体中包含引用类型成员时,需要标注生命周期参数,则在impl关键字之后也需要声明生命周期参数,并在结构体名称之后使用。 例如: impl<'a> Foo<'a> {fn split_first(s: &'a str) -> &'a str {…}} 在添加生命周期参数 注:枚举体和结构体对生命周期参数的处理方式是一样的。 2.4.3.5 静态生命周期参数
字符串字面量是全局静态类型,他的数据和程序代码一起存储在可执行文件的数据段中,其地址在编译期是已知的,并且是只读的,无法更改。 2.4.3.6 省略生命周期参数满足以下三条规则时,可以省略生命周期参数。该场景下,是将其硬编码到Rust编译器重,以便编译期可以自动补齐函数签名中的生命周期参数。 生命周期省略规则:
以上这部分规则还没理解透彻,需要继续熟读《Rust编程之道》的第143页。 2.4.3.7 生命周期限定生命周期参数可以向trait那样作为泛型的限定,有以下两种形式:
具体的举例见《Rust编程之道》的第145页。 2.4.3.8 trait对象的生命周期具体的举例见《Rust编程之道》的第146页。 2.4.3.9 高阶生命周期Rust还提供了
具体的可见《Rust编程之道》的第192页。 2.5 并发安全与所有权2.5.1 标签trait:Send和SyncØ 如果类型T实现了 Ø 如果类型T实现了 2.5.2 哪些类型实现了Send2.5.3 哪些类型实现了Sync2.6 原生类型Rust内置的原生类型 (primitive types) 有以下几类:
|
2.7 函数2.7.1 函数参数
2.7.2 函数参数模式匹配
具体可见《Rust编程之道》的第165页。 2.7.3 泛型函数函数参数并未指定具体的类型,而是用了泛型 泛型函数并未指定具体类型,而是靠编译器来进行自动推断的。如果使用的都是基本原生类型,编译器推断起来比较简单。如果编译器无法自动推断,就需要显式的指定函数调用的类型。 2.7.4 方法和函数方法代表某个实例对象的行为,函数只是一段简单的代码,它可以通过名字来进行调用。方法也是通过名字来进行调用,但它必须关联一个方法接受者。 2.7.5 高阶函数高阶函数是指以函数作为参数或返回值的函数,它是函数式编程语言最基础的特性。 具体可见《Rust编程之道》的第168页。 2.8 闭包Closure
将自由变量和自身绑定的函数就是 闭包的大小在编译期是未知的。 2.8.1 闭包的基本语法
Ø Ø 例如: Ø 当闭包函数没有参数只有捕获的自由变量时,管道符里的参数也可以省略。 例如: 2.8.2 闭包的实现
闭包和普通函数的 闭包可以作为函数参数,这一点直接提升了Rust语言的抽象表达能力。当它作为函数参数传递时,可以被用作泛型的trait限定,也可以直接作为trait对象来使用。 闭包无法直接作为函数的返回值,如果要把闭包作为返回值,必须使用trait对象。 2.8.3 闭包与所有权闭包表达式会由编译器自动翻译为结构体实例,并为其实现Fn、FnMut、FnOnce三个trait中的一个。
Ø 如果要实现 Ø 如果要实现 Ø 如果要实现 2.8.3.1 捕获环境变量的方式
具体可见《Rust编程之道》的第178页。 Rust使用 2.8.3.2 规则总结
Ø 如果不需要修改环境变量,无论是否使用 Ø 如果需要修改环境变量,则自动实现
Ø 如果不需要修改环境变量,而且没有使用 Ø 如果不需要修改环境变量,而且使用 Ø 如果需要修改环境变量,则自动实现
2.9 迭代器Rust使用的是外部迭代器,也就是for循环。外部迭代器:外部可以控制整个遍历进程。 Rust中使用了trait来抽象迭代器模式。 迭代器主要包含:
示例: let iterator = iter.into_iter();let size_lin = iterator.size_hint();let mut counter = Counter { count: 0};counter.next();
示例:
为了确保size_hint方法可以获得迭代器长度的准确信息,Rust引入了两个trait,他们是Iterator的子trait,均被定义在std::iter模块中。
2.9.1 IntoIterator trait如果想要迭代某个集合容器中的元素,必须将其转换为迭代器才可以使用。 Rust提供了FromIterator和IntoIterator两个trait,他们互为反操作。
2.9.2 哪些实现了Iterator的类型?只有实现了 实现了 实现了
2.9.3 迭代器适配器通过适配器模式可以将一个接口转换成所需要的另一个接口。适配器模式能够使得接口不兼容的类型在一起工作。 适配器也叫包装器(Wrapper)。 迭代器适配器,都定义在
具体可见《Rust编程之道》的第202页。 Rust可以自定义迭代器适配器,具体的见《Rust编程之道》的第211页。 2.10 消费器迭代器不会自动发生遍历行为,需要调用next方法去消费其中的数据。最直接消费迭代器数据的方法就是使用for循环。 Rust提供了for循环之外的用于消费迭代器内数据的方法,叫做消费器(Consumer)。 Rust标准库
2.11 锁
三、 Rust属性Ø Ø Ø Ø Ø Ø
l l Ø Ø 四、内存管理4.1 内存回收drop-flag:在函数调用栈中为离开作用域的变量自动插入布尔标记,标注是否调用析构函数,这样,在运行时就可以根据编译期做的标记来调用析构函数。 实现了Copy的类型,是没有析构函数的。因为实现了Copy的类型会复制,其生命周期不受析构函数的影响。 需要继续深入理解第4章并总结,待后续补充。 五、unicodeUnicode字符集相当于一张表,每个字符对应一个非负整数,该数字称为 这些码点也分为不同的类型:
Unicode字符集的每个字符占
Unicode字符编码表:
Rust的源码文件.rs的默认文本编码格式是UTF-8。 六、Rust附录字符串对象常用的方法
|