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由一下关键部分组成:

  • 基本块(Basic block,bb),他是控制流图的基本单位,

Ø 语句(statement)

Ø 终止句(Terminator)

  • 本地变量,占中内存的位置,比如函数参数、局部变量等。
  • 位置(Place),在内存中标识未知的额表达式。
  • 右值(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文件:

  • Cargo.lock:只记录依赖包的详细信息,不需要开发者维护,而是由Cargo自动维护
  • 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 doc
  • lcargo doc --open
  • lcargo test
  • lcargo test -- --test-threads=1
  • lcargo build
  • lcargo build --release
  • lcargo run
  • lcargo install --path
  • lcargo uninstallfirst_pro_create
  • lcargo new –bin use_regex

1.2.2 使用第三方包

Rust可以在Cargo.toml中的[dependencies]下添加想依赖的包来使用第三方包。

然后在src/main.rssrc/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类型只有两个值:truefalse

基本数字类型

主要关注取值范围,具体的见《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 集合中的元素应该是唯一的。
  • HashSet中的元素都是可哈希的类型,BTreeSet中的元素必须是可排序的。
  • 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字符串分为以下几种类型:

  • str:表示固定长度的字符串
  • String:表示可增长的字符串
  • CStr:表示由C分配而被Rust借用的字符串。这是为了兼容windows系统。
  • CString:表示由Rust分配且可以传递给C函数使用的C字符串,同样用于和C语言交互。
  • OsStr:表示和操作系统相关的字符串。这是为了兼容windows系统。
  • OsString:表示OsStr的可变版本。与Rust字符串可以相互交换。
  • Path:表示路径,定义于std::path模块中。Path包装了OsStr。
  • 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中的字符串不能使用索引访问其中的字符,可以通过byteschars两个方法来分别返回按字节按字符迭代的迭代器。

Rust提供了另外两种方法:getget_mut来通过指定索引范围来获取字符串切片。

具体的见《Rust编程之道》的第251页。

2.2.4.2 字符串修改

Ø 追加字符串:pushpush_str,以及extend迭代器

Ø 插入字符串:insertinsert_str

Ø 连接字符串:String实现了Add<&str>AddAssign<&str>两个trait,所以可以使用“+”和“+=”来连接字符串

Ø 更新字符串:通过迭代器或者某些unsafe的方法

Ø 删除字符串:removepoptruncatecleardrain

具体的见《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
  • {:x} :转换成16进制输出
  • {:b} :转换成二进制输出
  • l{:.5}:指定小数点后有效位是5
  • {:e}:科学计数法表示

具体的见《Rust编程之道》的第265页。

2.2.6 原生字符串声明语法:r”…”

原生字符串声明语法(r”…”)可以保留原来字符串中的特殊符号。

具体的见《Rust编程之道》的第270页。

2.2.7 全局类型

Rust支持两种全局类型:

  • 普通常量(Constant)
  • 静态变量(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)。

posted @ 2020-12-27 15:55  九思不出  阅读(990)  评论(0)    收藏  举报