Rust 编程语言入门教程

1. 第一章: Rust简介

1.1. 为什么要用Rust

Rust是一种令人兴奋的新编程语言, 它可以让每一个人编写可靠且高效的软件.
它可以用来替换C/C++, Rust和他们具有同样的性能, 但是很多常见的bug在编译时就可以被消灭.
Rust是一种通用的编程语言, 但是它更善于以下场景:
需要运行时的速度
需要内存安全
更好的利用多处理器

1.2. 与其他语言比较

C/C++ 性能非常好, 但类型系统和内存都不太安全.
JAVA/C#, 拥有GC, 能保证内存安全, 也有很多优秀特性, 但是性能不行
RUST:
安全
无需GC
易于维护, 调试, 代码安全高效

1.3. Rust特别擅长的领域

高性能webservice
webassembly
命令行工具
网络编程
嵌入式设备
系统编程

1.4. Rust与Firefox

  • Rust最初是Mozilla公司的一个研究性项目, firefox是Rust产品应用的一个重要例子.Mozilla 一直以来都在用rust创建一个名为servo的实验性浏览器引擎, 其中的所有内容都是并行执行的.
    目前servo的部分功能已经被集成到firefox里面了
    firefox原来的量子版就包含了servo的css渲染引擎

  • rust使得firefox在这方便得到了巨大的性能改进

1.5. Rust的用户和案例

  • google: 新操作系统Fuschia, 其中Rust代码量大约30%
  • Amazon: 基于Linux开发的直接可以在裸机, 虚拟机上运行容器的操作系统.
  • System76: 纯Rust开发了下一代安全操作系统Redox
  • 蚂蚁金服: 库操作系统Occlum
  • 斯坦福和密歇根大学: 嵌入式实时操作系统, 应用于google的加密产品.
  • 微软: 正在使用Rust重写windows系统中的一些低级组件.
  • 微软: winRT/Rust项目
  • Dropbox, yelp, Coursera, LINE, Cloudflare, Atlassian, npm, Ceph, 百度, 华为, Sentry, Deno

1.6. Rust的优点

  • 性能
  • 安全性
  • 无所畏惧的并发

1.7. Rust的缺点

  • 难学

1.8. 注意

Rust有很多独有的概念, 它们和现在大多主流语言都不同.

  • 所以学习Rust必须从基础概念一步一步学, 否则会懵.

1.9. 参考教材

The Rust programming language
Rust权威指南

2. 安装Rust

2.1. 安装rust

1
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf| sh

2.2. 配置Rust

2.2.1. 替换镜像

安装完成后需要将crates.io替换成国内镜像.

访问https://crates.io非常缓慢,github 的仓库也经常不能访问,建议大家切换到国内镜像站。镜像站实时缓存,托管在码云的 gitee 仓库每隔30分钟与 github 同步。

配置方式

  • 在 .cargo 目录新建文件config

注意:新安装的电脑中没有这个文件,需要手动创建config文件,并且该文件没有后缀名

编辑文件内容:

1
2
3
4
5
6
[source.crates-io]
registry="https://github.com/rust-lang/crates.io-index" #这行可以不要,只是说明原始地址
replace-with='crates-cn'

[source.crates-cn]
registry="https://gitee.com/crates/crates.io-index.git"

如果使用中科大USTC的镜像

1
2
3
4
5
6
7

[source.crates-io]
replace-with='ustc'

[source.ustc]
registry="https://mirrors.ustc.edu.cn/crates.io-index"

修改完保存后执行build即可

1
cargo build

2.3. 更新Rust

1
rustup update

2.4. 卸载Rust

1
rustup self uninstall

2.5. 验证安装是否成功

1
rustc --version
  • 结果格式: rustc x.y. z (abcabcabc yyyy-mm-dd)
  • 会显示最新稳定版的: 版本号, commit hash, commit日期
  • 当前我安装的版本为 rustc 1.56.1 (59eed8a2a 2021-11-01)

3. 第二章:第一个rust程序

3.1. hello world

main.rs

1
2
3
fn main() {
println!("hello world");
}

编译:

1
rustc main.rs

编译完成后将会输出可执行文件main,
运行程序使用命令

1
./main

3.2. Rust 程序解析

  • 定义函数 fn main() {}
    没有参数, 没有返回值
  • main 函数很特别 : 他是每一个Rust 可执行程序 最先运行的代码
  • 打印文本: println!(“hello, world!”)
    • rust 的缩进是4个空格而不是tab
    • println! 是一个Rust macro(宏), 如果是函数的话就没有感叹号(!)
    • “hello world” 是字符串, 它是println!的参数
    • 这行代码以;结尾

3.3. 编译和运行是单独的两步

  • 运行rust程序之前需要先编译, 命令为: rustc 源文件名

    1
    2
    3
    4
    # 编译程序
    rustc main.rs
    # 运行程序
    ./main
  • 编译成功后, 会生成一个二进制文件例如main

    • 在window上还会生成一个.pdb文件, 里面包含调试信息
  • Rust是ahead-of-time(AOT)预编译的语言

    • 可以先编译程序, 然后把可执行文件交给别人运行(无需安装Rust)
  • rustc只适合简单的Rust程序, 当文件比较多, 项目管理还是需要使用Cargo

3.4. hello cargo

3.4.1. cargo 介绍

  • Cargo 是Rust的构建系统和包管理工具
    • 构建代码, 下载依赖的库, 构建这些库
  • 安装Rust的同时会自动安装Cargo, 所以这里不需要额外的步骤去安装Cargo
1
2
$cargo --version
cargo 1.56.0 (4ed5d137b 2021-10-04)

3.4.2. 使用Cargo创建项目

  • 创建项目:

    1
    cargo new hello_cargo
  • 创建完成后项目的目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    $tree -a .      
    hello_cargo

    ├── Cargo.toml
    ├── .git
    │   ├── config
    │   ├── description
    │   ├── HEAD
    ........省略部分目录...........
    ├── .gitignore
    └── src
    └── main.rs

    目录结构说明:
    Cargo.toml:

    • TOML(Tom’s obvious, Minimal Language)格式,是Cargo的配置格式
    1
    2
    3
    4
    5
    6
    7
    8
    [package]
    name = "hello_cargo"
    version = "0.1.0"
    edition = "2018"

    # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

    [dependencies]
  • [package], 是一个项目信息区域,表示下方内容是用来配置包(package)的.

    • name: 项目名
    • version: 项目版本
    • authors: 项目作者
    • edition: The Rust edition
    • 更多参数说明请参考The Manifest Format
  • [dependencies], 另一个区域的开始,它会列出项目的依赖项.

  • 在Rust里面,代码的包称作crate(板条箱,装货箱;如果cargo是装货物的集装箱,Crate板条箱就是装载整批货物的小箱子)

    src/main.rs

    • cargo生成的main.rs在src目录下
    • 而Cargo.toml在项目顶层下
    • 源代码都应该在src目录下
    • 顶层目录可以放置:README, 许可信息, 配置文件和其它与程序源代码无关的文件
    • 如果创建项目时没有使用cargo, 也可以把项目转化为使用cargo:
      • 把源代码文件移动到src下
      • 创建Cargo.toml并填写相应的配置

3.4.3. 构建Cargo项目

  • cargo build
    创建可执行文件:target/debug/hello_cargo (linux/macos)或者 target\debug\hello_cargo.exe (windows)
  • 第一次运行cargo build会在顶层目录生成cargo.lock文件
    • 该文件负责追踪项目依赖的精确版本
    • 不需要手动修改该文件
  • 构建和运行cargo项目
    • cargo run
      • cargo run, 编译代码+执行结果
      • 如果之前编译成功过,并且源代码没有改变,那么就会直接运行二进制文件
  • cargo check
    • cargo check, 检查代码,确保能通过编译,但是不产生任何可执行文件
  • cargo buid -release
    • 编译时会进行优化
    • 代码会运行的更快, 但是编译时间更长
    • 会在target/release 而不是target/debug生成可执行文件
  • 两种配置,一种是开发用的,一种是发布用的

3.5. 猜数字游戏

3.5.1. 声明变量

main.rs

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
println!("猜数!");
println!("猜测一个数!");
/**
* let声明一个变量
* mut 声明变量为可变变量,
* 默认情况下变量是不可变的, 除非显示使用mut指明变量为可变变量
* = 赋值操作
* 注意申明时没有指定变量类型, 变量类型是根据赋初始值时进行推导的
* String是由rust 的标准库所提供的类型,内部是使用utf-8编码
* :: 符号表明new是String类型的关联函数,关联函数相当与其他语言中的静态方法
**/
let mut guess = String::new();

/**
* io是rust标准库中的一个包名
* stdin()方法会返回一个Stdin对象, 标准输入对象
* read_line 是标准输入对象的一个方法,调用该方法时,需要提供一个可变字符串变量,用于接收用户输入
* & 取地址符号,表示传递引用, 表示这个参数是一个引用reference,通过引用我们就可以在不同地方,访问程序的统一块内存区域
* &mut表示这个引用是可变的, 如果不加mut, 表明这个引用也是不可变的
* read_line函数返回的是一个i0:Result<usize>对象,expect是result对象的一个方法
* result是一个枚举类型, 其有两种类型的返回结果, 一种是err, 一种ok
* 如果返回的result为err, 该expect就会将错误信息输出到终端
* 如果返回结果是ok类型, expect就会提取出result中附加的值并将这个值作为结果返回给用户
* 如果不调用expect方法, 编译时将会收到rusult未被使用的警告
**/
io::stdin().read_line(&mut guess).expect("无法读取行");
//{} 是一个占位符,输出时将会替换成相对应的变量的值
println!("你猜测的数时{}", guess);

3.5.2. 引入依赖包

  • rust中的依赖包被称为crate
    crate分为两种, 一种是二进制格式的可执行文件; 一种是源文件,也被称为library crate
  • rust的crate仓库为crates.io, 可以访问该网站获得相应的crate
  • cargo 引入依赖的方式
  • 在dependencies 区域添加依赖的crate名称和版本如下所示.
1
2
3
4
5
6
7
8
#Cargo.toml
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2018"

[dependencies]
rand = "^0.7.0"
  • 示例代码
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
29
30
31
32
33
34
use std::io;
use rand::Rng; //trait
use std::cmp::Ordering;
fn main() {
println!("猜数!");

let secret_num = rand::thread_rng().gen_range(1, 101);
//rust循环
loop {
println!("猜测一个数!");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("无法读取行");
println!("你猜测的数是: {}", guess);
//shadow
let guess:u32 = match guess.trim().parse() {
Ok(num) => num,
Err(ex) => {
//rust异常处理
println!("解析错误 {} {}", guess, ex);
continue;
}
};
// rust条件运算
match guess.cmp(&secret_num) {
Ordering::Less => println!("Too small!"), //arm
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}

4. 第三章 通用的编程概念

4.1. 变量和可变性

  • 声明变量使用let 关键字

  • 默认情况下,变量是不可变的(immutable)
    例如:

    1
    2
    3
    4
    5
    let x =5;  // x 为不可变变量
    println!("the value of x is {}", x);
    x = 6; // 注意: 这里会有编译错误
    println!("the value of x is {}", x);

  • 声明变量时, 在变量前面加上mut, 就可以使变量可变.

    1
    2
    3
    4
    let mut x =5;
    println!("the value of x is {}", x);
    x = 6;
    println!("the value of x is {}", x);

4.2. 变量和常量

  • 常量(constant), 常量在绑定值以后也是不可变的, 但是它与不可变的变量有很多区别:
    • 不可以使用mut, 常量永远都是不可变的
    • 声明常量使用const关键字, 它的类型必须被标注
    • 常量可以在任何作用域内进行声明, 包括全局作用域
    • 常量只可以绑定到常量表达式, 无法绑定到函数的调用结果或只能在运行时才能计算出的值
  • 在程序运行期间, 常量在其声明的作用域内一直有效
  • 命名规范: Rust里常量使用全大写字母, 每个单词之间用下划线分开, 例如:
    1
    const MAX_POINTS: u32 = 100_000;

4.3. shadowing (隐藏)

  • 可以使用相同的名字声明新的变量, 新的变量就会shadow(隐藏)之前声明的同名变量

    • 在后续代码中这个变量名代表的就是新的变量

    • shadow和把变量标记为mut是不一样的
      例如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      fn main() {
      //定义不可变变量x
      let x = 5;
      println!("the value of x is {}", x);
      // x = 6 // 如果这里给x赋值会报错
      // 但是如果我们声明一个同名的新的变量,就可以编译通过
      // 我们甚至可以改变x的数据类型, 甚至可以定义新的同名但是不同的可变性的变量

      //新的同名变量
      let x = 6;
      println!("the value of x is {}", x);
      //不同可变性的同名变量
      let mut x = "my love";
      println!("the value of x is {}", x);
      x = "hello kitty";
      println!("the value of x is {}", x);
      }

4.4. Rust数据类型

  • Rust 是静态编译语言, 在编译时必须知道变量的类型

    • 基于使用的值, 编译器通常能够推断出它的具体类型

    • 但如果可能的类型比较多(例如把String 转为整数的parse方法),就必须添加类型的标注,否则编会报错.
      例如:

      1
      2
      let guess: u32 = "42".parse().expect("not a number")
      println!("{}", guess)

4.4.1. 标量类型

  • 一个标量类型代表一个单个的值
  • Rust有四个主要的标量类型:
    • 整数类型
    • 浮点类型
    • 布尔类型
    • 字符类型
4.4.1.1. 整数类型
  • 整数类型分为无符合整数类型,

    • 无符号整数类型以u开头
    • 有符号整数类型以i开头
  • Rust 的整数类型列表如图:

    • 每种长度都有对应的有符号型和无符号型.
  • 有符号范围

    • (负的2的n-1次方-1) 到(2的n-1次方-1)
  • 无符号范围

    • 0 到2的n次方 -1
  • isize 和 usize类型

    • isize和usize类型的位数由程序运行的计算机的架构所决定

    • 如果是64位的计算机,那就是64位的

    • 如果是32位的计算机,那就是32位的

    • 使用isize或者usize的场景是对某种集合进行索引操作

      lengthsignedunsigned
      8-biti8u8
      16-biti16u16
      32-biti32u32
      64-biti64i64
      128-biti128u128
      archisizeusize
    • 整数的字面值

      • 除了byte类型外, 所有的数值字面值都允许使用类型后缀
        例如: 57u8: 值为57 类型为u8

        • 整数的默认类型就是i32:

          • 总体来说速度很快, 即使在64位系统中
          NumberLiteralExample
          DecimalOxff
          HexOo77
          BinaryOb1111_0000
          Byte (u8 only)b’A’
    • 整数溢出
      例如:u8的范围是0-255, 如果你把一个u8变量的值设为256, 那么:

      • 调试模式下编译:rust会检查整数溢出, 如果发生溢出, 程序编译时就会panic
      • 在发布模式下(–release)编译:rust不会检查可能导致panic的整数溢出
        • 在这种模式下如果发生溢出:rust会执行环绕操作
          256变成0, 257变成1…. 但是不会导致panic

4.5. 浮点类型

  • rust 有两种基础的浮点类型,也就是含有小数部分的类型
    • f32, 32位, 单精度
    • f64, 64位,双精度
  • rust的浮点类型使用了IEEE-754标准来表述
  • f64是默认类型,因为在现代cpu上f64和32的运算速度差不多,而且精度更高
    例子:
    1
    2
    let x = 2.0 //默认为f64
    let y: f32 = 3.0; //f32

4.6. 数值操作

1
2
3
4
5
let sum = 5+10;   //i32
let difference = 95.5-4.3; //f64
let product = 4*30;
let quotient = 56.7/32.2 //f64
let reminder = 54%5

4.7. 布尔类型

  • Rust的布尔类型有两个值: true和false
  • 占用一个字节(Byte)的大小
  • 符号是bool
  • 例子
    1
    2
    3
    4
    5
    6
    7
    fn main() {
    //编译器推断类型
    let t = true;
    //显示指定类型
    let s: bool = false;
    }

4.8. 字符类型

rust语言中char类型被用来描述语言中最基础的单个字符.
字符类型的字面值使用单引号
占用4字节大小
是Unicode标量值, 可以表示比ASCII多得多的字符内容:拼音, 中日文, 零长度空白字符,emoji表情等.

  • 其范围为
    • U+0000到U+D7FF
    • U+E000到U+10FFFF
  • 但是unicode中并没有字符的概念, 所以直觉上认为的字符也许与Rust中的概念并不相符

4.9. 复合类型

  • 复合类型可以将多个值放在一个类型里
  • Rust提供了两种基础的复合类型: 元组(Tuple), 数组

4.9.1. Tuple

  • Tuple可以将多个类型的多个值放在一个类型里
  • Tuple的长度是固定的: 一旦声明就无法改变
4.9.1.1. 创建tuple
  • 在小括号里, 将值用逗号分开
  • Tuple中的每个位置都对应一个类型,tuple中各元素的类型不必相同
  • 实例
1
2
let tup: (i32, f64, u8) = (500, 6.4, 1)
println!("{},{},{}", tup.0, tup.1, tup.2)
4.9.1.2. 获取tuple的元素值
  • 可以使用模式匹配来解构(destructure)一个Tuple来获取元素的值
  • 例子
1
2
3
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; //这里使用模式匹配解构tup的值
println!("{},{},{}", x, y, z)

4.9.2. 访问tuple的元素

  • 在tuple变量使用点标记法,后接元素的索引号
  • 实例
1
2
let tup: (i32, f64, u8) = (500, 6.4, 1)
println!("{},{},{}", tup.0, tup.1, tup.2)

4.10. 数组

  • 数组也可以将多个值放在一个类型里
  • 数组中每个元素的类型必须相同
  • 数组的长度也是固定的

4.10.1. 声明一个数组

  • 在中括号里, 各值用逗号分开
  • 例子
    1
    2
    3
    fn main() {
    let a = [1, 2, 3, 4, 5]
    }

4.10.2. 数组的用处

  • 如果想让你的数据存放在栈(stack)上, 而不是堆(heap)上,或者想保证有固定数量的元素, 这时使用数组更有好处.
  • 数组没有Vector灵活(以后再讲)
    • Vector和数组类似,它是由标准库提供的
    • Vector的长度是可以改变的
    • 如果你不确定应该使用数组还是vector, 那么估计你应该用vector.
  • 例子
    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let months = ["January",
    "Fabruary",
    ......
    "December"
    ]
    }

4.10.3. 数组的类型

  • 数组的类型以这种形式来表示: [类型; 长度]
    • 例如: let a: [i32; 5] = [1, 2, 3, 4, 5];
  • 另外一种声明数组的方法
    如果数组的没一个元素值都相同, 那么可以在:
    在中括号里指定初始值
    然后是一个;
    最后是数组的长度
    例如: let a = [3; 5]; 它相当于:let a = [3, 3, 3, 3, 3]

4.10.4. 访问数组的元素

  • 数组是stack上分配的单个块的内存

  • 可以使用索引来访问数组的元素

  • 例子

    1
    2
    let first = months[0];
    let second = months[1];
  • 如果访问的索引超出了数组的范围, 那么

    • 编译会通过
    • 运行会报错(runtime时会panic) Rust不会允许其继续访问相应地址的内存
      说明:
      在简单的情况下,编译会报错,但是在复杂情况下,编译不会报错
      例如
    1
    2
    let index = 15;
    let month = months[index]; //此时编译会报错
    1
    2
    let index = [15, 1, 2 , 3];
    let month = months[index[0]]; //此时编译不会报错, 运行时会发生panic

4.11. 3.4 函数

  • 声明函数使用fn关键字
  • 依照惯例,针对函数和变量名,rust使用snake case命名规范:
    • 所有的字母都是小写的, 单词之间使用下划线分开

    • 例子

      1
      2
      3
      4
      5
      6
      7
      8
      fn main() {
      println!("hello world");
      another_function();
      }

      fn another_function() {
      println!("another function")
      }

4.11.1. 函数的参数

  • parameters, arguments
  • 例子
    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    println!("hello world");
    another_function(5);
    }

    fn another_function(x: i32) {
    println!("the value of x is {}", x)
    }

4.11.2. 函数中的语句(statement)和表达式(expression)

  • 函数体由一系列语句组成, 可选的由一个表达式结束
  • Rust是一个基于表达式的语言
  • 语句是执行一些动作的指令
  • 表达式会计算产生一个值
  • 函数的定义也是语句
  • 语句不返回值,所以不可以使用let将一个语句-赋给一个变量

例子

1
2
3
4
5
6
7
8
fn main() {
let x = 5;
let y = {
let x = 1;
//x+3; //注意这里有个分号,它是语句而不是表达式, 但是这个语句有些特殊, 它的值等于一个空的tuple 即()
x+3 //这里没有分号, 它是一个表达式, 表达式的值为5 它是整个block的返回值, 该值将会被赋给变量y
}
}

4.11.3. 函数的返回值

  • 在->符号后边声明函数返回值的类型, 但是不可以为返回值命名
  • 在Rust里面, 返回值就是函数体里面最后一个表达式的值
  • 若想提前返回, 需使用return 关键字, 并指定一个值
    • 大多数函数都是默认使用最后一个表达式为返回值
      例子:
1
2
3
4
5
6
7
fn five() -> i32 {
5
}
fn main() {
let x = five();
println("the value of x is: {}", x);
}

4.12. 注释

单行注释
多行注释
文档注释

4.13. 控制流

4.13.1. if 表达式

  • if 表达式允许你根据条件来执行不同的代码分支

    • 这个条件必须是bool类型
  • if 表达式中, 与条件相关联的代码块就叫做分支(arm)

  • 可选的, 在后面可以加上一个else表达式

  • 但是如果使用了多余一个else if, 那么最好使用match来重构代码

  • 例子

    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let number = 3;
    if number < 5 {
    println!("condition was false");
    } else {
    println!("condition was false");
    }
    }
  • 例子:else if

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    let number = 3;
    if number % 4 == 0 {
    println!("number is divisaible by 4");
    } else if number % 3 == 0 {
    println!("number is divisaible by 3");
    } else {
    println!("number is not divisaible by 3 or 4");
    }
    }

4.13.2. 在let语句中使用if

  • 因为if是一个表达式, 所以可以将它放在let语句中等号的右边(例子)
    1
    2
    3
    4
    5
    fn main() {
    let condition = ture;
    let muber = if condition {5} else {6}; //相对于其它语言中的三元表达式
    println!("The value of number is {}", number);
    }

4.14. Rust的循环

  • Rust提供了3中循环:loop, while 和 for.

4.14.1. loop 循环

loop关键字告诉Rust反复地执行一块代码,直到你喊停为止
可以在loop循环中使用break关键字来告诉程序何时停止循环
例子

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut counter = 0;
let resut = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
}

println!("The result is: {}", result);
}

4.14.2. while条件循环

  • 另外一种常见的循环模式是每次执行循环体之前都判断一次条件.
  • while条件循环就是为这种模式而生的
  • 例子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let mut number = 3;
    while number != 0 {
    println!("{}!", number);
    number = number -1;
    }
    println!("LIFTOFF!!!");
    }

4.15. for 循环遍历集合

  • 可以使用while 或 loop 来遍历数组, 但是易出错且低效.

  • 使用for循环更简洁紧凑, 它可以针对集合中的每一个元素来执行一些代码

  • 例子

    1
    2
    3
    4
    5
    6
    fn main() {
    let a = [10, 20, 30, 40, 50]
    for element in a.iter() {
    println!("the value is: {}", element);
    }
    }
  • 由于for循环的安全,简洁性,所以它在Rust里用的最多

4.16. Range

  • 标准库提供
  • 指定一个开始数字和一个结束数字,Range可以生成它们之间的数字(不包含介绍)
  • rev方法可以反转range
  • 例子
    1
    2
    3
    4
    5
    6
    fn main() {
    for number in (1..4).rev() {
    println!("{}!", number);
    }
    println!("LIFTOFF!");
    }

5. 第四章: 所有权

5.1. 4.1 什么是所有权

  • Rust的核心特性就是所有权
  • 所有程序在运行时都必须管理它们使用计算机内存的方式
    • 有些语言有垃圾收集机制,在程序运行时, 它们不断地寻找不再使用的内存
    • 在其他语言中,程序员必须显示地分配和释放内存
    • Rust采用了第三种方式
    • 内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则.
    • 当程序运行时,所有权特性不会减慢程序运行速度

5.2. Stack vs Heap

  • Stack按值的接收顺序来存储,按相反的顺序将它们移除(后进先出, LIFO)
    • 添加数据叫做压入栈
    • 移除数据叫做弹出栈
  • 所有存储在stack上的数据必须拥有已知的固定的大小
    • 编译时大小未知的数据或运行时大小可能变化的数据必须存放在heap上
  • Heap内存组织性差一些
    • 当你把数据放入heap时, 你会请求一定数量的空间
    • 操作系统在heap里找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址
    • 这个过程叫做在heap上进行分配, 有时仅仅称为分配
    • 把值压到stack上不叫分配
    • 因为指针是已知固定大小的, 可以把指针存放在stack上.
      但如果想要实际数据,你必须使用指针来定位
    • 把数据压倒stack上要比在heap上分配快得多:
      • 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远在stack的顶端
    • 在heap上分配空间需要做更多的工作:
      • 操作系统首先需要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配
      • 访问heap中的数据要比访问stack中的数据慢,因为需要通过指针才能找到heap中的数据
      • 对于现代的处理器来说, 由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快.
    • 如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(stack上)
    • 如果数据之间的距离比较远,那么处理速度就会慢一些(heap上)
      • 在heap上分配大量的空间也是需要时间的
    • 当你的代码调用函数时,值被传入到函数(也包括指向heap的指针).函数本地的变量被压到stack上,当函数结束后,这些值会从stack上弹出.

5.3. 所有权存在的原因

  • 所有权解决的问题
    • 跟踪代码的哪些部分正在使用heap的哪些数据
    • 最小化heap上的重复数据量
    • 清理heap上未使用的数据以避免空间不足
  • 一旦你懂得了所有权,那么就不需要经常去想stack或heap了
  • 但是知道管理heap数据是所有权存在的原因,这有助于解释它为什么会这样工作.

5.4. 所有权,内存与分配

5.4.1. 所有权规则

  • 每个值都有一个变量,这个变量是该值的所有者
  • 每个值同时只能有一个所有者
  • 当所有者超出作用域的时候,该值将被删除

5.5. 变量作用域

  • scope就是程序中一个项目的有效范围
  • 例子
    1
    2
    3
    4
    5
    fn main() {
    //s 不可以
    let s = "hello"; // s可用
    //可以对s进行相关操作
    } //s 作用域到此结束,s不再可用

5.5.1. string 类型

  • String 比那些基础标量数据类型更复杂
  • 字符串字面值: 程序里手写的那些字符串值.它们是不可变的
  • Rust还有第二种字符串类型: String
    • 在heap上分配,能够存储在编译时未知数量的文本
5.5.1.1. 创建String类型的值
  • 可以使用from函数从字符串字面值创建出String类型
1
let s = String::from("hello");// :: 表示from是String类型下的函数

这类字符串是可以被修改的

1
2
3
4
5
6
fn main() {
let mut s = String::from("Hello");
s.push_str(", world");
pringln!(s);
}

  • 字符串字面值,在编译时就知道它的内容, 其文本内容直接被编码到最终的可执行文件里
  • 速度快,高效,得益于其不可变性
  • String类型,为了支持可变性, 需要在heap上分配内存来保存编译时未知的文本内容:
  • 操作系统必须在运行时来请求内存
    • 这步通过调用String::from来实现
  • 当用完String之后,需要使用某种方式将内存返回给操作系统
    • 这步,在拥有GC的语言中,GC会跟踪并清理不再使用的内存
    • 没有GC的语言中,就需要我们去识别内存何时不再使用,并调用代码将它释放
      • 如果忘了,那就浪费内存.
      • 如果提前做了,变量就会非法
      • 如果做了两次,也是bug. 必须一次分配对应一次释放
    • rust采用了不同的方式: 对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的归还给操作系统.
    • drop函数

5.5.2. 变量和数据交互的方式: 移动 move

  • 多个变量可以与同一个数据使用一种独特的方式来交互

    1
    2
    let x = 5;
    let y = x;

    整数是已知且固定大小的简单的值, 这两个5被压到了stack中

    1
    2
    let s1 = String::from("hello");
    let s2 = s1;

    一个String 由3部分组成:

    • 一个指向存放字符串内容的内存的指针
    • 一个长度
    • 一个容量
  • 上面这些东西被放在stack上.

  • 存放字符串内容的部分在heap上

  • 长度len, 就是存放字符串内容所需的字节数

  • 当把s1 赋值给S2, String 的数据被复制了一份

  • 在stack上复制了一份指针, 长度, 容量

  • 并没有复制指针所指向的heap上的数据

  • 当变量离开作用域时,Rust会自动调用Drop函数,并将变量使用的heap内存释放.
    当S1, S2离开作用域时, 它们都会尝试释放相同的内存

  • 二次释放(double free) bug
    为了保证内存安全:

  • Rust没有尝试复制被分配的内存

  • Rust让s1失效

    • 当s1离开作用域的时候, rust不需要释放任何东西

    • 试试看当s2创建后,再使用s1是什么效果

      1
      2
      3
      4
      5
      fn main() {
      let s1 = String::from("hello");
      let s2 = s1;
      println!("{}", s1); //这里会有编译错误,这里s1已经失效了
      }
      • 浅拷贝(shallow copy)
      • 深拷贝(deep copy)
      • 你也许会将复制, 指针, 长度, 容量视为浅拷贝, 但是由于Rust让s1失效了, 所以我们用一个新的术语:移动move
      • 隐藏了一个设计原则,Rust不会自动创建数据的深拷贝
      • 就运行时性能而言, 任何自动赋值的操作都是廉价的

5.5.3. 变量和数据交互的方式: 克隆(Clone)

  • 如果真想对heap上面的String 数据进行深拷贝, 而不仅仅是stack上的数据,可以使用clone方法
1
2
3
4
5
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}", s1);
}

5.5.4. Stack上的数据: 复制

  • Copy trait, 可以容易想到整数这样完全放在stack上面的类型
    如果一个类型实现了Copy这个trait,那么旧的变量在赋值后仍然可用
  • 如果一个类型或者该类型的一部分实现了Drop trait,那么Rust不允许让它再去实现Copy trait了

5.5.5. 一些拥有copy trait的类型

  • 任何简单标量的组合类型都可以是copy的
  • 任何需要分配内存或某种资源的都不是copy的
  • 一些拥有Copy trait的类型
    • 所有的整数类型, 例如u32
      • bool
      • char
      • 所有的浮点类型, 例如f64
      • tuple 元组, 如果其所有的字段都是Copy的
        (i32, i32) 是
        (i32, String) 不是

5.5.6. 所有权与函数

  • 在语义上, 将值传递给函数和把值赋给变量是类似的:
  • 将值传递给函数将发生移动或复制

5.5.7. 返回值与作用域

  • 函数在返回值的过程中同样也会发生所有权的转移
1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s1 = gives_ownership();
let s2 = String::from("hello");
let s3 = take_and_gives_back(s2);
}

fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string
}
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
  • 一个变量的所有权总是遵循同样的模式:
    • 把一个值赋给其它变量时就会发生移动
    • 当一个包含heap数据的变量离开作用域时,它的值就会被Drop函数清理,除非数据的所有权移动到另一个变量上了

5.5.8. 如何让函数使用某个值,但是不获得其所有权?

1
2
3
4
5
6
7
8
9
10
fn main() {
let s1 = String::from("hello");
let(s2, len)= calculate_length(s1);
println!("the length of '{}' is {}", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s,length)
}

5.6. 引用和借用

  • 以下例子中参数的类型是&String而不是String
  • &符号就表示引用:允许你引用某些值而不取得所有权
1
2
3
4
5
6
7
8
9
fn main() {
let s1 = String::from("hello");
let(s2, len)= calculate_length(s1);
println!("the length of '{}' is {}", s2, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

我们把引用作为函数参数这个行为叫做借用
是否可以修改借用的东西?不行
和变量一样,引用默认也是不可变的

1
2
3
4
5
6
7
8
9
10
fn main() {
let s1 = String::from("hello");
let(s2, len)= calculate_length(s1);
println!("the length of '{}' is {}", s2, len);
}

fn calculate_length(s: &String) -> usize {
s.push_str(", world!") //编译期就会报错, can't borrow *s as mutable, as it's behind a '&' reference
s.len()
}

5.6.1. 可变引用

可变引用有一个重要的限制: 在特定作用域内,对某一块数据, 只能有一个可变引用

1
2
3
4
5
6
7
  
fn main() {
let mut s = String::from("hello");
let s1 = &mut s;
let s2 = &mut s; // 这里会报编译错误 can't borrow s as mutable more than once at a time
}

  • 这样做的好处是可在编译时防止数据竞争

以下三种行为下会发生数据竞争:

  • 两个或多个指针同时访问同一个数据
  • 至少有一个指针用于写入数据
  • 没有使用任何机制来同步对数据的访问
  • 可以通过创建新的作用域, 来允许非同时的创建多个可变引用(例子)
  • 不可以同时拥有一个可变引用和一个不变的引用
1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut s1 = String::from("hello");
let len = calculate_length(&mut s1);
println!("Then length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &mut String) -> usize {
s.push_str(", world")
s.len()
}

5.6.2. 悬空引用Dangling References

  • 悬空指针(Dangling Pointer): 一个指针引用了内存中的某个地址, 而这块内存可能已经释放并分配给其它人使用了.
  • 在Rust里, 编译器可保证引用永远都不是悬空引用:
    • 如果你引用了某些数据, 编译器将保证在引用离开作用域之前数据不会离开作用域
    1
    2
    3
    4
    5
    6
    7
    8
    fn main() {
    let r = dangle();
    }
    fn dangle() -> &String { // 这里编译器会报错,因为s出了此作用域将会被释放,而返回值是一个指向已经被释放区域的指针,这会导致问题,而rust在编译期就杜绝了这种错误.
    let s = String::from("hello");
    &s
    }

5.6.3. 引用的规则

  • 在任何给定的时刻, 只能满足下列条件之一:
    • 一个可变的引用
    • 任何数量不可变的引用

5.7. 切片

Rust的另一种不持有所有权的数据类型: 切片(slice)

一道题, 编写一个函数

  • 它接收字符串作为参数
  • 返回它在这个字符串里找到的第一个单词
  • 如果函数没有找到任何空格, 那么整个字符串就被返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

fn main() {
println!("Hello, world!")
let mut s = String::from("Hello world");
let wordIndex = first_word(&s);

println!("{}", wordIndex);
}

fn first_word(s: &string) -> usize {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}

字符串切片

  • 字符串切片是指向字符串中一部分内容的引用
  • 例子
  • 形式:[开始索引..结束索引]
    • 开始索引就是切片起始位置的索引值
    • 结束索引是终止位置的下一个索引值
1
2
3
4
5
6
7
8
9
10
11

fn main() {
let s = String::from("Hello world")

let hello= &s[0..5] // 或者&s[..5]
let world= &s[6..11] // 或者&s[6..]

println!("{}, {}", hello, world)
}


注意:

  • 字符串切片的范围索引必须发生在有效的UTF-8 字符边界内。
  • 如果尝试从一个多字节的字符中创建字符串切片,程序会报错并退出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

fn main() {
println!("Hello, world!")
let mut s = String::from("Hello world");
let wordIndex = first_word(&s);

println!("{}", wordIndex);
}

fn first_word(s: &string) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
s
}

将字符串切片作为参数传达

  • fn first_word(s: &String) -> &str {
  • 有经验的Rust开发者会采用&str作为参数类型,因为这样就可以同时接收字符串和&str类型的参数了
  • fn first_word(s: &str) -> &str {
    • 使用字符串切片,直接调用该函数
    • 使用String, 可以创建一个完整的String切片来调用该函数
  • 定义函数时使用字符串切片来代替字符串引用会使得我们的API更加通用,且不丧失任何功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

fn main() {
println!("Hello, world!")
let mut s = String::from("Hello world");
let wordIndex = first_word(&s);

println!("{}", wordIndex);
}

fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
s
}

其他类型的切片

  • 例子
1
2
3
4
5
6

fn main() {
let a = [1,2,3,4,5]
let slice = &a[1..3] //&[i32]
}

5. Struct

定义并实例化struct

什么是struct

  • struct结构体
    • 自定义的数据类型
    • 为相关联的值命名, 打包=>有意义的组合

定义struct

  • 使用struct关键字, 并为整个struct命名
  • 在花括号内, 为所有字段(Field)定义名称和类型

例如:

struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

实例化struct

  • 想要使用struct, 需要创建struct的实例:
    • 为每个字段指定具体值
    • 无需按声明的顺序进行指定

例子:

1
2
3
4
5
6
7
8
9
10
11
fn main() {

println!("Hello, world!");

let user1 = User {
email: String::from("acb@126.com"),
username: String::from("Nikky"),
active: true,
sign_in_count: 556,
}
}

获取struct里面的某个值

  • 使用点标记法:
1
2
3
4
5
6
7
8
9
10

let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("Nikky"),
active: true,
sign_in_count: 556,
}

user1.email = String::from("anotheremail@example.com"),

注意:

一旦struct的实例是可变的,那么实例中所有的字段都是可变的。rust不允许我们声明struct中一部分字段可变,而另一部分字段不可变。

struct作为函数的返回值

1
2
3
4
5
6
7
8
9
10

fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}

字段初始化简写

  • 当字段名与字段值对应的变量名相同时,就可以使用字段初始化简写的方式:
1
2
3
4
5
6
7
8
9
10

fn build_user(email: String, username: String) -> User {
User{
email,
username,
active: true,
sign_in_count: 1,
}
}

struct 更新语法

  • 当你想基于某个struct实例来创建一个新实例的时候,可以使用struct的更新语法

不使用更新语法的例子:

1
2
3
4
5
6
7
8

let user2 = User {
email: String::from("someone@example.com"),
username: String::from("Nikky"),
active: user1.active,
sign_in_count: user1.sign_in_count,
}

使用更新语法的例子:

1
2
3
4
5
6
7

let user2 = User {
email: String::from("someone@example.com"),
username: String::from("Nikky"),
..user1
}

6. Rust web框架

2022年可选择的三个Rust Web框架:actix-web、warp和axum。

  • actix-web:4.0.0-rc.35,134,720Actix Web 是一个功能强大、实用且速度极快的 Rust Web 框架
  • Warp: 0.3.24,114,095以翘曲的速度服务于网络
  • axum: 专注于人体工程学和模块化的 Web 框架(由 tokio 团队提供)

比较:

  • axum有最干净的 API,它建立在hyper之上,它(当然)是 Rust 中经过测试最可靠的 HTTP 堆栈,并且因为它是由 tokio 团队开发的。但它的年轻可能会让一些人感到不舒服。
  • 对于较大的项目,我认为这actix-web是无可争议的赢家。这就是为什么它是我选择Bloom的原因。
  • 对于较小的项目(最多 50 条路由)warp,尽管它有原始的 API,但它非常好,因为它也是建立在其之上的hyper,因此受益于它的可靠性和性能。

详细比较:

  • JSON反序列化:所有框架都使用泛型来提供简单的 JSON 反序列化。话虽如此,我发现两者都axum可以actix-web更直接地与他们的助手一起使用来自动提取类型化的正文有效负载。
  • 路由:axum是明显的赢家,紧随其后的是actix-web,然后是warp有利于组合的功能性 API,这与我们通常对 Web 框架的期望相去甚远。
  • 中间件:warp, 毫无疑问…
  • 状态:在构建 Web 服务时,您需要共享一些变量,例如数据库连接池或一些用于外部服务的客户端。所有框架的人机工程学都非常相似。

7. 后记

本文原文位于鹏叔的技术博客 - Rust 编程语言入门教程, 若需要获取最近更新, 请访问原文.

7.1. web框架选择参考文档

两张图展示当前 Rust Web 生态
2022年选择哪个Rust Web框架