1. 1. 前言
  2. 2. 第一部分
    1. 2.1. 第1章 初识Rust
      1. 2.1.1. 1.1 版本和发布策略
      2. 2.1.2. 1.2 安装开发环境
      3. 2.1.3. 1.3 Hello World
      4. 2.1.4. 1.4 前奏
    2. 2.2. 第2章 变量和类型
      1. 2.2.1. 2.1 变量声明
        1. 2.2.1.1. 2.1.1 变量遮蔽
        2. 2.2.1.2. 2.1.2 类型推导
        3. 2.2.1.3. 2.1.3 静态变量
        4. 2.2.1.4. 2.1.4 常量
      2. 2.2.2. 2.2 基本数据类型
      3. 2.2.3. 2.3 复合数据类型
    3. 2.3. 第3章 语句和表达式
      1. 2.3.1. 3.1 语句
      2. 2.3.2. 3.2 表达式
        1. 2.3.2.1. 3.2.1 运算表达式
        2. 2.3.2.2. 3.2.2 赋值表达式
        3. 2.3.2.3. 3.2.3 语句块表达式
      3. 2.3.3. 3.3 if-else
        1. 2.3.3.1. 3.3.1 loop
        2. 2.3.3.2. 3.3.2 while
        3. 2.3.3.3. 3.3.3 for
    4. 2.4. 第4章 函数
      1. 2.4.1. 4.1 简介
      2. 2.4.2. 4.2 发散函数
      3. 2.4.3. 4.3 main函数
      4. 2.4.4. 4.4 const fn
    5. 2.5. 第5章 trait
      1. 2.5.1. 5.1 成员方法
      2. 2.5.2. 5.2 静态方法
      3. 2.5.3. 5.3 扩展方法
      4. 2.5.4. 5.4 完整函数调用语法
      5. 2.5.5. 5.5 trait的约束和集成
      6. 2.5.6. 5.6 derive
      7. 2.5.7. 5.7 trait 别名
      8. 2.5.8. 5.8 常见的标准库trait
        1. 2.5.8.1. 5.8.1 Display 和 Debug
        2. 2.5.8.2. 5.8.2 PartialOrd和Ord
        3. 2.5.8.3. 5.8.3 Sized
        4. 2.5.8.4. 5.8.4 Default
    6. 2.6. 第6章 数组和字符串
      1. 2.6.1. 6.1 数组
        1. 2.6.1.1. DST和胖指针
        2. 2.6.1.2. Range
        3. 2.6.1.3. 边界检查
      2. 2.6.2. 6.2 字符串
        1. 2.6.2.1. 6.2.1 &str
        2. 2.6.2.2. 6.2.2 String
    7. 2.7. 第7章 模式结构
      1. 2.7.1. 7.2 match
        1. 2.7.1.1. 7.2.1 exhaustive
        2. 2.7.1.2. 7.2.2 下划线
        3. 2.7.1.3. 7.2.3 match也是表达式
        4. 2.7.1.4. 7.2.4 guards
        5. 2.7.1.5. 7.2.5 变量绑定
        6. 2.7.1.6. 7.2.6 ref和mut
      2. 2.7.2. 7.3 if-let和while-let
    8. 2.8. 第8章 类型系统
      1. 2.8.1. 8.1 代数类型系统
      2. 2.8.2. 8.2 Nerver Type
      3. 2.8.3. 8.3 Option

深入浅出Rust

前言

Rust is a system’s programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.

  • 内存安全:Rust语言是可以保证内存安全的系统级编程语言。这是它的独特的优势。
  • 并发: 在强大的内存安全特性的支持下,Rust一举解决了并发条件下的数据竞争(Data Race)问题。
  • 实用:Rust摈弃了手动内存管理带来的各种不安全的弊端,同时也避免了自动垃圾回收带来的效率损失和不可控性。在绝大部分情况下,保持了“无额外性能损失”的抽象能力。

第一部分

第1章 初识Rust

Rust语言是一门系统编程语言,它有三大特点:运行快、防止段错误、保证线程安全。 系统级编程语言一般具有以下特点:

  • 可以在资源非常受限的环境下执行;
  • 运行时开销很小,非常高效;
  • 很小的运行库,甚至于没有;
  • 可以允许直接的内存操作。

1.1 版本和发布策略

编译器的源码位于https://github.com/rust-lang/rust 项目中;

语言设计和相关讨论位于https://github.com/rust-lang/rfcs 项目中。

在nightly版本中使用试验性质的功能,必须手动开启feature gate。也就是说要在当前项目的入口文件中加入一条#![feature(…name…)]语句。 否则是编译不过的。等到这个功能最终被稳定了,再用新版编译器编译的时候,它会警告你这个feature gate现在是多余的了,可以去掉了。

Rust的标准库文档位于https://doc.rust-lang.org/std/ 。学会查阅标准库文档,是每个Rust使用者的必备技能之一。

1.2 安装开发环境

  • rustc.exe是编译器
  • cargo.exe是包管理器
  • cargo-fmt.exe和rustfmt.exe是源代码格式化工具
  • rust-gdb.exe和rust-lldb.exe是调试器
  • rustdoc.exe是文档生成器
  • rls.exe和racer.exe是为编辑器准备的代码提示工具
  • rustup.exe是管理这套工具链下载更新的工具。

RLS(Rust Language Server)是官方提供的一个标准化的编辑器增强工具。它也是开源的,项目地址在https://github.com/rust-lang-nursery/rls 。它是一个单独的进程,通过进程间通信给编辑器或者集成开发环境提供一些信息,实现比较复杂的功能,比如代码自动提示、跳转到定义、显示函数签名等。

1.3 Hello World

一般Rust源代码的后缀名使用.rs表示。源码一定要注意使用utf-8编码。

1.4 前奏

Rust的代码从逻辑上是分crate和mod管理的。所谓crate大家可以理解为“项目”。每个crate是一个完整的编译单元,它可以生成为一个lib或者exe可执行文件。

在crate内部,则是由mod这个概念管理的,所谓mod大家可以理解为namespace。我们可以使用use语句把其他模块中的内容引入到当前模块中来。

第2章 变量和类型

2.1 变量声明

Rust的变量必须先声明后使用。对于局部变量,最常见的声明语法为:

1
let variable : i32 = 100;

从语法分析的角度来说,Rust的变量声明语法比C/C++语言的简单,局部变量声明一定是以关键字let开头,类型一定是跟在冒号:的后面。语法歧义更少,语法分析器更容易编写。

Rust的变量声明的一个重要特点是:要声明的变量前置,对它的类型描述后置。因为在变量声明语句中,最重要的是变量本身,而类型其实是个附属的额外描述,并非必不可少的部分。

let语句不光是局部变量声明语句,而且具有pattern destructure(模式解构)的功能。

Rust中声明变量缺省是“只读”的。如果我们需要让变量是可写的,那么需要使用mut关键字。let语句在此处引入了一个模式解构,我们不能把let mut视为一个组合,而应该将mut x视为一个组合。

在Rust中,一般把声明的局部变量并初始化的语句称为“变量绑定”,强调的是“绑定”的含义

每个变量必须被合理初始化之后才能被使用。使用未初始化变量这样的错误,在Rust中是不可能出现的(利用unsafe做hack除外)

类型没有“默认构造函数”,变量没有“默认值”。对于let x:i32;如果没有显式赋值,它就没有被初始化,不要想当然地以为它的值是0。

Rust里面的下划线是一个特殊的标识符,在编译器内部它是被特殊处理的。

2.1.1 变量遮蔽

Rust允许在同一个代码块中声明同样名字的变量。如果这样做,后面声明的变量会将前面声明的变量“遮蔽”(Shadowing)起来。

这两个x代表的内存空间完全不同,类型也完全不同,它们实际上是两个不同的变量。

变量遮蔽在某些情况下非常有用,比如,我们需要在同一个函数内部把一个变量转换为另一个类型的变量,但又不想给它们起不同的名字。

在同一个函数内部,需要修改一个变量绑定的可变性。例如,我们对一个可变数组执行初始化,希望此时它是可读写的,但是初始化完成后,我们希望它是只读的。

如果一个变量是不可变的,我们也可以通过变量遮蔽创建一个新的、可变的同名变量。这个过程是符合“内存安全”的。一个“不可变绑定”依然是一个“变量”。虽然我们没办法通过这个“变量绑定”修改变量的值,但是我们重新使用“可变绑定”之后,还是有机会修改的。这样做并不会产生内存安全问题,因为我们对这块内存拥有完整的所有权,且此时没有任何其他引用指向这个变量,对这个变量的修改是完全合法的。

2.1.2 类型推导

Rust只允许“局部变量/全局变量”实现类型推导,而函数签名等场景下是不允许的,这是故意这样设计的。这是因为局部变量只有局部的影响,全局变量必须当场初始化而函数签名具有全局性影响。

2.1.3 静态变量

Rust中可以用static关键字声明静态变量。

用static声明的变量的生命周期是整个程序,从启动到退出。static变量的生命周期永远是’static,它占用的内存空间也不会在执行过程中回收。这也是Rust中唯一的声明全局变量的方法。

全局变量必须在声明的时候马上初始化;

全局变量的初始化必须是编译期可确定的常量,不能包括执行期才能确定的表达式、语句和函数调用;

带有mut修饰的全局变量,在使用的时候必须使用unsafe关键字

Rust不允许用户在main函数之前或者之后执行自己的代码。所以,比较复杂的static变量的初始化一般需要使用lazy方式,在第一次使用的时候初始化。在Rust中,如果用户需要使用比较复杂的全局变量初始化,推荐使用lazy_static库。

2.1.4 常量

使用const声明的是常量,而不是变量。因此一定不允许使用mut关键字修饰这个变量绑定,这是语法错误。

编译器并不一定会给const常量分配内存空间,在编译过程中,它很可能会被内联优化。因此,用户千万不要用hack的方式,通过unsafe代码去修改常量的值,这么做是没有意义的。以const声明一个常量,也不具备类似let语句的模式匹配功能。

2.2 基本数据类型

  • bool
  • char: 描述任意一个unicode字符,因此它占据的内存空间不是1个字节,而是4个字节。对于ASCII字符其实只需占用一个字节的空间,因此Rust提供了单字节字符字面量来表示ASCII字符。我们可以使用一个字母b在字符或者字符串前面,代表这个字面量存储在u8类型数组中,这样占用空间比char型数组要小一些。
  • 整形类型:
    • isize和usize类型。它们占据的空间是不定的,与指针占据的空间一致,与所在的平台相关。如果是32位系统上,则是32位大小;如果是64位系统上,则是64位大小。
    • 在C++中与它们相对应的类似类型是int_ptr和uint_ptr。在语言标准中规定好各个类型的大小,让编译器针对不同平台做适配,生成不同的代码,是更合理的选择。
    • 在所有的数字字面量中,可以在任意地方添加任意的下划线,以方便阅读let var1 = 0x_ff_u8。字面量后面可以跟后缀,可代表该数字的具体类型,从而省略掉显示类型标记。
    • image-20231127082025918
    • 整数溢出:默认情况下,在debug模式下编译器会自动插入整数溢出检查,一旦发生溢出,则会引发panic;在release模式下,不检查整数溢出,而是采用自动舍弃高位的方式
  • 浮点类型:
    • 在标准库中,有一个std::num::FpCategory枚举,表示了浮点数可能的状态。Nan Infinite Zero Subnormal Normal
    • 在IEEE 754标准中,规定了浮点数的二进制表达方式:x =(-1)^s * (1 + M) *2^e。其中s是符号位,M是尾数,e是指数。尾数M是一个[0, 1)范围内的二进制表示的小数。
    • 数值就小到了无法在32bit范围内合理表达的程度,最终收敛到了0,在后面表示非常小的数值的时候,浮点数就已经进入了Subnormal状态。
    • Nan != Nan 因为NaN的存在,浮点数是不具备“全序关系”(total order)的。
  • 指针类型
    • image-20231127083644698
    • image-20231127083701492
  • 类型转换:在Rust中使用As作为类型转换

2.3 复合数据类型

  • Tuple: tuple指的是“元组”类型,它通过圆括号包含一组表达式构成。tuple内的元素没有名字。tuple是把几个类型组合到一起的最简单的方式。

    1
    2
    3
    4
    5
    6
    let p = (1i32, 2i32);

    let x = p.0;
    let y = p.1;

    let empty:() = ();

    元组内部也可以一个元素都没有。这个类型单独有一个名字,叫unit(单元类型).unit类型是Rust中最简单的类型之一,也是占用空间最小的类型之一。空元组和空结构体struct Foo;一样,都是占用0内存空间。

  • Struct:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct Point {
    x:i32,
    y:i32,
    }

    fn main() {
    let x= 10;
    let y =20;
    let p = Point{x, y};
    let p2 = Point{x:0, y:0};
    println!("Point is", p.x, p.y);
    }

    Rust设计了一个语法糖,允许用一种简化的语法赋值使用另外一个struct的部分成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct Point3d {
    x:i32,
    y:i32,
    z:i32,
    }
    fn default() -> Point3d {
    Point3d {x:0, y:0, z:0}
    }

    let orign = Point3d {x :5, ..default()};
    let point = Point3d {y :1, ..orign};
  • Rust有一种数据类型叫作tuple struct,它就像是tuple和struct的混合。区别在于,tuple struct有名字,而它们的成员没有名字

第3章 语句和表达式

3.1 语句

一个表达式总是会产生一个值,因此它必然有类型;语句不产生值,它的类型永远是()。

3.2 表达式

Rust表达式又可以分为“左值”(lvalue)和“右值”(rvalue)两类。所谓左值,意思是这个表达式可以表达一个内存地址。因此,它们可以放到赋值运算符左边使用。其他的都是右值。

3.2.1 运算表达式

比较运算符的两边必须是同类型的,并满足PartialEq约束。比较表达式的类型是bool。另外,Rust禁止连续比较

所谓逻辑运算符短路的意思是:

  • 对于表达式A&&B,如果A的值是false,那么B就不会执行求值,直接返回false.
  • 对于表达式A||B,如果A的值是true,那么B就不会执行求值,直接返回true。
  • 而“按位与”、“按位或”在任何时候都会先执行左边的表达式,再执行右边的表达式,不会省略。

3.2.2 赋值表达式

赋值表达式具有“副作用”:当它执行的时候,会把右边表达式的值“复制或者移动”(copy or move)到左边的表达式中。

赋值号左右两边表达式的类型必须一致,否则是编译错误。

赋值表达式也有对应的类型和值。这里不是说赋值表达式左操作数或右操作数的类型和值,而是说整个表达式的类型和值。Rust规定,赋值表达式的类型为unit,即空的tuple ()。Rust这么设计是有原因的,比如说可以防止连续赋值。如果你有x: i32、y: i32以及z: i32,那么表达式z = y = x会发生编译错误。因为变量z的类型是i32但是却用()对它初始化了,编译器是不允许通过的。

Rust不支持++、–运算符,请使用+= 1、-= 1替代

3.2.3 语句块表达式

在Rust中,语句块也可以是表达式的一部分。语句和表达式的区分方式是后面带不带分号(;)。

例如:

1
2
3
4
5
fn my_func() -> i32
{
...
100
}

最后一条表达式没有加分号,因此整个语句块的类型就变成了i32,刚好与函数的返回类型匹配。这种写法与return 100;语句的效果是一样的,相较于return语句来说没有什么区别,但是更加简洁。特别是用在后面讲到的闭包closure中,这样写就方便轻量得多。

3.3 if-else

规定if和else后面必须有大括号,可读性会好很多。

条件表达式并未强制要求用小括号包起来;如果加上小括号,编译器反而会认为这是一个多余的小括号,给出警告

1
2
3
4
if ... {
} else if ... {
} else {
}

if-else结构还可以当表达式使用,因此在Rust中,没有必要专门设计像C/C++那样的三元运算符(? :)语法,因为通过现有的设计可以轻松实现同样的功能:

1
let x:i32 = if condition {1} else {10};

如果使用if-else作为表达式,那么一定要注意,if分支和else分支的类型必须一致,否则就不能构成一个合法的表达式,会出现编译错误。如果else分支省略掉了,那么编译器会认为else分支的类型默认为()。

1
2
3
4
5
fn invalid_expr(cond :bool) -> i32 {
if cond {
42
}
}

如果此处编译器不报错,放任程序编译通过,那么在执行到else分支的时候,就只能返回一个未初始化的值,这在Rust中是不允许的。

3.3.1 loop

在Rust中,使用loop表示一个无限死循环。另外,break语句和continue语句还可以在多重循环中选择跳出到哪一层的循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn test5()
{
let mut m = 1;
let n = 1;
'a : loop {
if m <100 {
m += 1;
} else {
'b :loop {
if m + n > 50{
println!("break");
break 'a;
} else {
continue 'a;
}
}
}
}
}

在loop内部break的后面可以跟一个表达式,这个表达式就是最终的loop表达式的值。如果一个loop永远不返回,那么它的类型就是“发散类型”。

1
2
3
4
5
fn main()
{
let v = loop {};
println!("{}" , v);
}

编译器可以判断出v的类型是发散类型,而后面的打印语句是永远不会执行的死代码。

3.3.2 while

为什么Rust专门设计了一个死循环,loop语句难道不是完全多余的吗?

相比于其他的许多语言,Rust语言要做更多的静态分析。loop和while true语句在运行时没有什么区别,它们主要是会影响编译器内部的静态分析结果。

3.3.3 for

Rust中的for循环实际上是许多其他语言中的for-each循环。

1
2
3
4
let array = &[1,2,3,4,5];
for i in array {
println!("number is {}", i);
}

第4章 函数

4.1 简介

函数也可以不写返回类型,在这种情况下,编译器会认为返回类型是unit ()

函数可以当成头等公民(first class value)被复制到一个值中,这个值可以像函数一样被调用。

每一个函数都具有自己单独的类型,但是这个类型可以自动转换到fn类型。

即使两个函数有同样的参数类型和同样的返回值类型,但它们是不同类型,如果直接赋值的话就会报错报错了。修复方案是让func的类型为通用的fn类型即可:

1
2
3
4
// 使用as类型转换
let mut func = add1 as fn((i32,i32))->i32;
// 使用显示类型标记
let mut func : fn((i32, i32))->i32 = add1;

Rust的函数体内也允许定义其他item,包括静态变量、常量、函数、trait、类型、模块等。当你需要一些item仅在此函数内有用的时候,可以把它们直接定义到函数体内,以避免污染外部的命名空间。

4.2 发散函数

Rust支持一种特殊的发散函数(Diverging functions),它的返回类型是感叹号!。如果一个函数根本就不能正常返回

1
2
3
fn diverges() -> ! {
panic!("This func never return!")
}

因为panic!会直接导致栈展开,所以这个函数调用后面的代码都不会继续执行,它的返回类型就是一个特殊的!符号,这种函数也叫作发散函数。发散类型的最大特点就是,它可以被转换为任意一个类型。

在Rust中,有以下这些情况永远不会返回,它们的类型就是!:

  • panic!以及基于它实现的各种函数/宏,比如unimplemented! 、unreachable!;
  • 死循环loop {};
  • 进程退出函数std::process::exit以及类似的libc中的exec一类函数。

4.3 main函数

传递参数和返回状态码都由单独的API来完成,如果要读取环境变量,可以用std::env::var()以及std::env::vars()函数获得。

1
2
3
4
5
6
7
8
fn main() {
for arg in std::env::args() {
println!("Arg: {}", arg);
}

std::process::exit(0);
}

4.4 const fn

函数可以用const关键字修饰,这样的函数可以在编译阶段被编译器执行,返回值也被视为编译期常量。

第5章 trait

  • trait 更多关注于行为的抽象,而不持有数据。
  • 类通常是数据和行为的封装体,并且具有实例化能力。
  • 在 Rust 中,没有类的概念。Rust 使用结构体(struct)来封装数据,而行为则通过实现 trait 来定义。

5.1 成员方法

所有的trait中都有一个隐藏的类型Self(大写S),代表当前这个实现了此trait的具体类型。

trait中定义的函数,也可以称作关联函数(associated function)。

函数的第一个参数如果是Self相关的类型,且命名为self(小写s),这个参数可以被称为“receiver”(接收者)。具有receiver参数的函数,我们称为“方法”(method),可以通过变量实例使用小数点来调用。

没有receiver参数的函数,我们称为“静态函数”(static function),可以通过类型加双冒号::的方式来调用。

Rust中Self(大写S)和self(小写s)都是关键字,大写S的是类型名,小写s的是变量名。

另外,针对一个类型,我们可以直接对它impl来增加成员方法,无须trait名字。我们可以把这段代码看作是为Circle类型impl了一个匿名的trait。用这种方式定义的方法叫作这个类型的“内在方法”(inherent methods)。

5.2 静态方法

没有receiver参数的方法(第一个参数不是self参数的方法)称作“静态方法”。静态方法可以通过Type::FunctionName()的方式调用。

需要注意的是,即便我们的第一个参数是Self相关类型,只要变量名字不是self,就不能使用小数点的语法调用函数。

5.3 扩展方法

我们还可以利用trait给其他的类型添加成员方法,哪怕这个类型不是我们自己写的。

在声明trait和impl trait的时候,Rust规定了一个Coherence Rule(一致性规则)或称为Orphan Rule(孤儿规则):impl块要么与trait的声明在同一个的crate中,要么与类型的声明在同一个crate中。

如果trait来自于外部crate,而且类型也来自于外部crate,编译器不允许你为这个类型impl这个trait。

trait本身既不是具体类型,也不是指针类型,它只是定义了针对类型的、抽象的“约束”。不同的类型可以实现同一个trait,满足同一个trait的类型可能具有不同的大小。因此,trait在编译阶段没有固定大小,目前我们不能直接使用trait作为实例变量、参数、返回值。

5.4 完整函数调用语法

这个语法可以允许使用类似的写法精确调用任何方法,包括成员方法和静态方法。其他一切函数调用语法都是它的某种简略形式。它的具体写法为<T as TraitName>::item。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
trait Cook {
fn start(&self);
}
trait Wash {
fn start(&self);
}

struct Chef;
impl Cook for Chef {
fn start(&self) {
println!("Cooking");
}
}

impl Wash for Chef {
fn start(&self) {
println!("Washing");
}
}

fn main() {
let chef = Chef;
// chef.start();
Cook::start(&chef);
Wash::start(&chef);
<Chef as Wash>::start(&chef);

}

需要注意的是,通过小数点语法调用方法调用,有一个“隐藏着”的“取引用”步骤。虽然我们看起来源代码长的是这个样子me.start(),但是大家心里要清楚,真正传递给start()方法的参数是&me而不是me,这一步是编译器自动帮我们做的。

5.5 trait的约束和集成

Rust可以对泛型有约束,比如只要实现了一个Debug trait的结构体都可以传入到这个泛型结构里面。泛型约束既是对实现部分的约束,也是对调用部分的约束。

1
2
3
4
use std::fmt::Debug;
fn my_print<T:Debug>(x:T) {
println!("The value is {:?}", x);
}

trait允许继承。

1
2
trait Base {...}
trait Derived : Base {...}

这表示Derived trait继承了Base trait。它表达的意思是,满足Derived的类型,必然也满足Base trait。所以,我们在针对一个具体类型impl Derived的时候,编译器也会要求我们同时impl Base。

5.6 derive

Rust里面为类型impl某些trait的时候,逻辑是非常机械化的。为许多类型重复而单调地impl某些trait,是非常枯燥的事情。为此,Rust提供了一个特殊的attribute,它可以帮我们自动impl某些trait。它的语法是,在你希望impl trait的类型前面写#[derive(…)],括号里面是你希望impl的trait的名字。这样写了之后,编译器就帮你自动加上了impl块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#[derive(Copy, Clone,Default,Debug,Hash,PartialEq,Eq,PartialOrd,Ord)]
struct Foo {
data:i32
}
fn main() {
let foo = Foo{data:1};
let mut bar = foo;
bar.data = 2;
println!("foo.data = {}",foo.data);
println!("bar.data = {}",bar.data);
}

|
|


impl Copy for Foo {...}
impl Clone for Foo {...}
impl Default for Foo {...}
...

支持自动化derive的trait有以下:

1
Debug Clone Copy Hash RustcEncodable RustcDecodable PartialEq Eq ParialOrd Ord Default FromPrimitive Send Sync

5.7 trait 别名

1
2
3
4
5
6
7
8
9
10
pub trait Service {
type Request;
type Response;
type Error;
type Future: Future<Item = Self::Response, Error = Self::Error>;
fn call(&self, req: Self::Request) -> Self::Future;

}

trait HttpService: Service<Request = http::Request<Body>, Response = http::Response<Body>> {}

5.8 常见的标准库trait

5.8.1 Display 和 Debug

只有实现了Display trait的类型,才能用{}格式控制打印出来;只有实现了Debug trait的类型,才能用{:? } {:#? }格式控制打印出来。

5.8.2 PartialOrd和Ord

因为NaN的存在,浮点数是不具备“total order(全序关系)”的。在这里,我们详细讨论一下什么是全序、什么是偏序。Rust标准库中有如下解释。对于集合X中的元素a, b, c,

  • 如果a < b则一定有! (a > b);反之,若a > b,则一定有!(a < b),称为反对称性。
  • 如果a < b且b < c则a < c,称为传递性。
  • 对于X中的所有元素,都存在a < b或a > b或者a == b,三者必居其一,称为完全性。

如果集合X中的元素只具备上述前两条特征,则称X是“偏序”。同时具备以上所有特征,则称X是“全序”。因为浮点数中特殊的值NaN不满足完全性。

只有Ord trait里面的cmp函数才能返回一个确定的Ordering。f32和f64类型都只实现了PartialOrd,而没有实现Ord。

因此,对浮点数数组求最大值是会报错的,因为Nan的存在,不能求出最大值。

5.8.3 Sized

不定长类型在使用的时候有一些限制,比如不能用它作为函数的返回类型,而必须将这个类型藏到指针背后才可以。

Rust中对于动态大小类型专门有一个名词Dynamic Sized Type。我们后面将会看到的[T],str以及dyn Trait都是DST。

5.8.4 Default

所以Rust里面推荐使用普通的静态函数作为类型的“构造器”。比如,常见的标准库中提供的字符串类型String

第6章 数组和字符串

6.1 数组

数组是一个容器,它在一块连续空间内存中,存储了一系列的同样类型的数据。只有元素类型和元素个数都完全相同,这两个数组才是同类型的。只有元素类型和元素个数都完全相同,这两个数组才是同类型的。

1
2
3
let xs:[i32;5] = [1,2,3,4,5]
// 所有元素也可以初始化为同样的数据
let ys:[i32;500] = [0;500];

把数组xs作为参数传给一个函数,这个数组并不会退化成一个指针。而是会将这个数组完整复制进这个函数。函数体内对数组的改动不会影响到外面的数组。

对数组内部元素的访问,可以使用中括号索引的方式。Rust支持usize类型的索引的数组,索引从0开始计数。

既然[T; n]是一个合法的类型,那么它的元素T当然也可以是数组类型,因此[[T; m]; n]类型自然也是合法类型。

对数组取借用borrow操作,可以生成一个“数组切片”(Slice)。数组切片对数组没有“所有权”,我们可以把数组切片看作专门用于指向数组的指针,是对数组的另外一个“视图”。

DST和胖指针

Slice与普通的指针是不同的,它有一个非常形象的名字:胖指针(fat pointer)。与这个概念相对应的概念是“动态大小类型”(Dynamic Sized Type, DST)。所谓的DST指的是编译阶段无法确定占用空间大小的类型。为了安全性,指向DST的指针一般是胖指针。胖指针内部的数据既包含了指向源数组的地址,又包含了该切片的长度。

对于DST类型,Rust有如下限制:

  • 只能通过指针来间接创建和操作DST类型,&[T]Box<[T]>可以,[T]不可以;
  • 局部变量和函数参数的类型不能是DST类型,因为局部变量和函数参数必须在编译阶段知道它的大小因为目前unsized rvalue功能还没有实现;
  • enum中不能包含DST类型,struct中只有最后一个元素可以是DST,其他地方不行,如果包含有DST类型,那么这个结构体也就成了DST类型。

这一设计的好处有:

  • 首先,DST类型虽然有一些限制条件,但我们依然可以把它当成合法的类型看待,比如,可以为这样的类型实现trait、添加方法、用在泛型参数中等;
  • 胖指针的设计,避免了数组类型作为参数传递时自动退化为裸指针类型,丢失了长度信息的问题,保证了类型安全;
  • 这一设计依然保持了与“所有权”“生命周期”等概念相容的特点。

Range

Rust中的Range代表一个“区间”,一个“范围”,它有内置的语法支持,就是两个小数点..

在begin..end这个语法中,前面是闭区间,后面是开区间。

在Rust中,还有其他的几种Range,包括

  • std::ops::RangeFrom代表只有起始没有结束的范围,语法为start..,含义是[start, +∞);
  • std::ops::RangeTo代表没有起始只有结束的范围,语法为..end,对有符号数的含义是(-∞, end),对无符号数的含义是[0, end);
  • std::ops::RangeFull代表没有上下限制的范围,语法为..,对有符号数的含义是(-∞, +∞),对无符号数的含义是[0, +∞)。

虽然左闭右开区间是最常用的写法,然而,在有些情况下,这种语法不足以处理边界问题。比如,我们希望产生一个i32类型的从0到i32::MAX的范围,就无法表示。因为按语法,我们应该写0..(i32::MAX + 1),然而(i32::MAX+1)已经溢出了。所以,Rust还提供了一种左闭右闭区间的语法,它使用这种语法来表示..=

边界检查

在Rust中,“索引”操作也是一个通用的运算符,是可以自行扩展的。如果希望某个类型可以执行“索引”读操作,就需要该类型实现std::ops::Index trait,如果希望某个类型可以执行“索引”写操作,就需要该类型实现std::ops::IndexMut trait。

6.2 字符串

Rust的字符串显得有点复杂,主要是跟所有权有关。Rust的字符串涉及两种类型,一种是&str,另外一种是String。

6.2.1 &str

str是Rust的内置类型。&str是对str的借用。Rust的字符串内部默认是使用utf-8编码格式的。

Rust里面的字符串不能视为char类型的数组,而更接近u8类型的数组。

就是不能支持O(1)时间复杂度的索引操作。如果我们要找一个字符串s内部的第n个字符,不能直接通过s[n]得到,这一点跟其他许多语言不一样。在Rust中,这样的需求可以通过下面的语句实现:

1
s.chars().nth(n)

它的时间复杂度是O(n),因为utf-8是变长编码,如果我们不从头开始过一遍,根本不知道第n个字符的地址在什么地方。但是,综合来看,选择utf-8作为内部默认编码格式是缺陷最少的一种方式了。相比其他的编码格式,它有相当多的优点。比如:它是大小端无关的,它跟ASCII码兼容,它是互联网上的首选编码,等等。

6.2.2 String

接下来讲String类型。它跟&str类型的主要区别是,它有管理内存空间的权力。

&str类型是对一块字符串区间的借用,它对所指向的内存空间没有所有权,哪怕&mut str也一样。

String类型在堆上动态申请了一块内存空间,它有权对这块内存空间进行扩容

第7章 模式结构

模式结构的意思是:把原来的结构肢解为单独的、局部的、原始的部分 。

构造和解构遵循类似的语法,我们怎么把一个数据结构组合起来的,我们就怎么把它拆解开来。

1
2
let tuple = (1i32, false, 3f32);
let (head, center, tail) = tuple;

7.2 match

当一个类型有多种取值可能性的时候,特别适合使用match表达式。

如果我们进行匹配的值同时符合好几条分支,那么总会执行第一条匹配成功的分支,忽略其他分支。

7.2.1 exhaustive

因为Rust要求match需要对所有情况做完整的、无遗漏的匹配,如果漏掉了某些情况,是不能编译通过的。exhaustive意思是无遗漏的、穷尽的、彻底的、全面的。exhaustive是Rust模式匹配的重要特点。

有些时候我们不想把每种情况一一列出,可以用一个下划线来表达“除了列出来的那些之外的其他情况”

7.2.2 下划线

下划线还能用在模式匹配的各种地方,用来表示一个占位符,虽然匹配到了但是忽略它的值的情况

下划线在Rust里面用处很多,比如:在match表达式中表示“其他分支”,在模式中作为占位符,还可以在类型中做占位符,在整数和小数字面量中做连接符,等等。

除了下划线可以在模式中作为“占位符”,还有两个点..也可以在模式中作为“占位符”使用。下划线表示省略一个元素,两个点可以表示省略多个元素。

7.2.3 match也是表达式

match表达式的每个分支可以是表达式,它们要么用大括号包起来,要么用逗号分开。每个分支都必须具备同样的类型。

我们还可以使用范围作为匹配条件,使用..表示一个前闭后开区间范围,使用..=表示一个闭区间范围

1
2
3
4
5
6
let x = 'X';
match x {
'a'..='z' => println!("lowercase"),
'A'..='Z' => println!("uppercase"),
_ => println!("something else"),
}

7.2.4 guards

可以使用if作为“匹配看守”(match guards)。当匹配成功且符合if条件,才执行后面的语句。

1
2
3
4
5
6
7
8
9
10
enum OptionalInt {
Value(i32),
Missing,
}
let x = OptionalInt::Value(5);
match x {
OptionalInt::Value(i) if i > 5 => println! ("Got an int bigger than five! "),
OptionalInt::Value(..) => println! ("Got an int! "),
OptionalInt::Missing => println! ("No such luck."),
}

7.2.5 变量绑定

可以使用@符号绑定变量。@符号前面是新声明的变量,后面是需要匹配的模式:

1
2
3
4
5
let x = 1;
match x {
e @ 1 ..= 5 => println! ("got a range element {}", e),
_ => println! ("anything"),
}

7.2.6 ref和mut

如果我们需要绑定的是被匹配对象的引用,则可以使用ref关键字。之所以在某些时候需要使用ref,是因为模式匹配的时候有可能发生变量的所有权转移,使用ref就是为了避免出现所有权转移。ref是“模式”的一部分,它只能出现在赋值号左边,而&符号是借用运算符,是表达式的一部分,它只能出现在赋值号右边。

1
2
3
4
5
6
7
let x = 5_i32;
match x {
ref r => println! ("Got a reference to {}", r), // 此时 r 的类型是 `&i32`
}

let mut x: &mut i32;
// ^1 ^2

以上两处的mut含义是不同的。第1处mut,代表这个变量x本身可变,因此它能够重新绑定到另外一个变量上去,具体到这个示例来说,就是指针的指向可以变化。第2处mut,修饰的是指针,代表这个指针对于所指向的内存具有修改能力,因此我们可以用*x = 1;这样的语句,改变它所指向的内存的值。

7.3 if-let和while-let

Rust不仅能在match表达式中执行“模式解构”,在let语句中,也可以应用同样的模式。Rust还提供了if-let语法糖。它的语法为if let PATTERN =EXPRESSION { BODY }。后面可以跟一个可选的else分支。

第8章 类型系统

8.1 代数类型系统

一个类型所有取值的可能性叫作这个类型的“基数”(cardinality)。

最简单的类型unit ()的基数就是1,它可能的取值范围只能是()。再比如说,bool类型的基数就是2,可能的取值范围有两个,分别是true和false。对于i32类型,它的取值范围是232,我们用Cardinality(i32)来代表i32的基数。

我们把多个类型组合到一起形成新的复合类型,这个新的类型就会有新的基数。如果两个类型的基数是一样的,那么我们可以说它们携带的信息量其实是一样的,我们也可以说它们是“同构”的。

对于数组类型,可以对应为每个成员类型都相同的tuple类型(或者struct是一样的)。用数学公式类比,则比较像乘方运算。

Rust中的enum类型就相当于代数中的“求和”运算。比如,某个类型可以代表“东南西北”四个方向,

空的enum可以类比为数字0; unit类型或者空结构体可以类比为数字1; enum类型可以类比为代数运算中的求和;tuple、struct可以类比为代数运算中的求积;数组可以类比为代数运算中的乘方。

加法具有交换率,同理,enum中的成员交换位置,也不会影响它的表达能力;乘法具有交换率,同理,struct中的成员交换位置,也不影响它的表达能力。

8.2 Nerver Type

像unit类型和没有成员的空struct类型,都可以类比为代数中的数字1。这样的类型在内存中实际需要占用的空间为bits_of(()) = log2(1) = 0。也就是说,这样的类型实际上是0大小的类型。

这样的类型在Rust类型系统中的名字叫作never type,它们有一些属性是其他类型不具备的:

  • 它们在运行时根本不可能存在,因为根本没有什么语法可以构造出这样的变量;
  • Cardinality(Never) = 0;
  • 考虑它需要占用的内存空间bits_of(Never) = log2(0) = -∞,也就是说逻辑上是不可能存在的东西;
  • 处理这种类型的代码,根本不可能执行;
  • 返回这种类型的代码,根本不可能返回;
  • 它们可以被转换为任意类型。

8.3 Option

利用类型系统(ADT)将空指针和非空指针区别开来,分别赋予它们不同的操作权限,禁止针对空指针执行解引用操作。编译器和静态检查工具不可能知道一个变量在运行期的“值”,但是可以检查所有变量所属的“类型”,来判断它是否符合了类型系统的各种约定。如果我们把null从一个“值”上升为一个“类型”,那么静态检查就可以发挥其功能了。实际上早就已经有了这样的设计,叫作Option Type,并在scala、haskell、Ocaml、F#等许多程序设计语言中存在了许多年。

Option类型不仅在表达能力上非常优秀,而且运行开销也非常小。在这里我们还可以再次看到“零性能损失的抽象”能力

  1. 如果从逻辑上说,我们需要一个变量确实是可空的,那么就应该显式标明其类型为Option,否则应该直接声明为T类型。从类型系统的角度来说,这二者有本质区别,切不可混为一谈。
  2. 不要轻易使用unwrap方法。这个方法可能会导致程序发生panic。对于小工具来说无所谓,在正式项目中,最好是使用lint工具强制禁止调用这个方法。
  3. 相对于裸指针,使用Option包装的指针类型的执行效率不会降低,这是“零开销抽象”。
  4. 不必担心这样的设计会导致大量的match语句,使得程序可读性变差。因为Option类型有许多方便的成员函数,再配合上闭包功能,实际上在表达能力和可读性上要更胜一筹。