文章

Rust 学习笔记

The Rust Programming Language 的阅读笔记

Rust 学习笔记

1. 入门指南

1.1 安装

Rust 安装见 https://kaisery.github.io/trpl-zh-cn/ch01-01-installation.html 。

更新和卸载

Rust 是可以更新的,不像 Python 那样每个版本都需要重新装。

1
rustup update

卸载 Rust 用如下命令

1
rustup self uninstall

查看离线文档

1
rustup doc

1.2 Hello, World!

rustc 编译文件

创建一个文件 main.rs,编辑如下内容

1
2
3
fn main() {
    println!("Hello rust");
}

在命令行输入

1
rustc main.rs

会生成一个可执行文件,运行该文件即可打印 Hello rust

1.3 Hello, Cargo!

使用 rustc 来编译源码,可以,但有点繁琐。Cargo 是 Rust 的构建系统和包管理器,适合复杂项目。

创建项目

1
cargo new hello_cargo --vcs=none

该命令创建一个名为 hello_cargo 的文件夹,里面包含一些必要文件。默认情况下也会添加 git 版本控制,添加 --vcs=none 可以取消。

构建项目

1
2
cd hello_cargo
cargo build

编译项目,默认是 Debug 模式,会在 target/debug 下生成项目同名的可执行文件。

1
cargo run

编译项目之后运行可执行文件,更方便,因此比 cargo build 更常用。

1
cargo check

检查代码是否可编译但不实际编译项目,因此速度比 cargo build 更快。

发布构建

1
cargo build --release

target/release 下生成可用于发布的可执行文件。相比于 Debug 模式,编译时间可能更久(因为需要执行一些运行优化),但运行更快(因为有运行优化)。

3. 常见编程概念

3.1 变量与可变性

变量

Rust 的变量用 let 声明,当变量类型可以被推断时,可以不用显式指明类型,当类型不可推断时,需要显式指明类型。

1
2
let a = 1; //可以推断是 i32 类型
let b: i32; //只声明未赋值,不能推断类型,需要指明

Rust 的变量默认都是不可变的,即赋值后便不可更改,要创建可变的变量需要用 let mut 声明。

1
2
let mut c = 3;
c += 2; //赋新值或者自增等都属于改变

常量

Rust 的常量用 const 声明,永不可变,不能加 mut 修饰。

常量 必须 指明类型。

常量值必须在编译时就能确定,不能赋一个运行时才能知道的值,即只能是常量表达式。

1
const a: i32 = 3;

常量可以定义在全局作用域,变量只能定义在函数里。

1
2
3
4
5
6
const a: i32 = 3; //ok
let b = 3; //error

fn main() {
    let c = 4; //ok
}

隐藏

Rust 可以用 let 多次声明同一变量,这一点与 JavaScript 不同。

1
2
let x = 3;
let x = x + 4;

这里第一行的 x 依然是不可变的,只不过第二行用 let 重新声明后这个 x 与第一行的 x 就是两个完全不同的变量了,只不过恰好名称相同。这一操作叫隐藏(Shadowing)。

因为是两个不同的变量了,所以类型啥的也没什么要求,下面的代码是可行的

1
2
let x = 3;
let x = "hello";

其中第一行的 x 的类型是 i32,而第二行的是 &str。两个变量除了名称相同外没什么联系。

但如下代码是不可行的

1
2
let mut x = 3;
x = "hello";

第一行声明了 x 这一变量为 i32 类型的,虽然是可变的,但这只代表其值可变,类型是不能变的,因此第二行试图赋值一个字符串时会失败。

因为第二行的 x 没有用 let 重新声明,因此它与第一行的 x 是同一个变量,虽然可变,但必须是 i32 类型的。

3.2 数据类型

整型

长度有符号无符号
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

isize 在 32 位操作系统上是 i32,在 64 位操作系统上是 i64usize 同理。

数字字面值例子
Decimal (十进制)98_222
Hex (十六进制)0xff
Octal (八进制)0o77
Binary (二进制)0b1111_0000
Byte (单字节字符)(仅限于u8)b'A'

数字字面值中可以添加下划线来分组,如 1_000

Rust 的整型字面值默认是 i32 类型的,但可以通过添加类型后缀来指明类型

1
2
let a = 24;
let b = 24u8;

变量 ai32 类型的,变量 bu8 类型的。变量 b 的字面值也可以写作 24_u8

整型溢出在 Debug 模式下会使程序 panic,在 Release 模式下则不会。

浮点型

Rust 的浮点型有 f32f64 两种。字面值默认是 f64 的。

1
2
3
4
let a = 0.4; //f64
let b: f32 = 0.4;
let c = 0.4f32;
let d = 0.4_f32;

后三个变量都是 f32 类型的。

注意,没有 fsize 这种类型。

Rust 的整数除法与 C 是一样的,不会产生浮点数,而是向 0 舍入到最近的整数。

1
2
let a = 7 / 2; //3
let b = 7.0 / 2.0; //3.0

貌似不能让浮点数与整数进行运算。

布尔型

Rust 的布尔类型是 bool,只有两个值:truefalse

字符类型

Rust 的字符类型是 char,大小是 4 个字节,代表一个 Unicode 标量值,因此 Rust 的一个字符可以是任意 Unicode 字符。

元组类型

元组类型,长度固定,元素随意。

1
let a: (i32, f64, char) = (30, 2.4, 'a');

元组可以解构

1
2
3
let t = (30, 2.4, 'a');
let (i, f, c) = t;
println!("i: {i}, f: {f}, c: {c}");

用点和索引可以访问元组元素

1
2
let t = (30, 2.4, 'a');
let i = t.0;

单元(unit)元组是一种特殊值,写作 (),类型也是 (),表示空。如果一个函数没有显式返回任何值,则会隐式返回 ()

数组类型

数组类型,长度 固定,元素类型也必须相同。

1
2
let a: [i32; 3] = [1, 2, 3];
let b: [f64; 3] = [4.0; 3];

第二行是声明 3 个 4.0 的快捷方式。

此处的类型注解不是必须的,只是为了说明数组类型的注解方式。

使用方括号索引数组元素

1
2
let a = [1, 2, 3];
let t = a[0];

3.3 函数

Rust 的函数要求 必须声明每个参数的类型

1
2
3
4
5
6
7
fn main() {
   say_age('B', 34);
}

fn say_age(name: char, age: i32) {
   println!("{name} is {age} years old.");
}

语句和表达式

Rust 是一门基于表达式的语言。

表达式 计算并产生一个值,语句 执行一些操作,但不返回值。

let a = 3; 是一个语句,函数定义也是语句。

5 + 6 是一个表达式,5 也是一个表达式。函数调用是表达式,宏调用也是表达式,大括号建立的块作用域也是表达式

1
2
3
4
5
6
7
fn main() {
    let y = {
        let x = 3;
        x + 1
    };
    println!("y: {y}");
}

注意,上述 x + 1 后没有分号,表示它是一个表达式,也是整个块作用域的返回值。

有返回值的函数

函数的返回值等于函数体最后一个表达式的值。如果使用 return 可以提前返回。

1
2
3
4
5
6
7
8
fn five() -> i32 {
    5 //这里没有分号
}

fn main() {
    let f = five();
    println!("{f}");
}

函数返回值需要声明类型。

3.5 控制流

if 表达式

if 是一个表达式,因此 if 的结果可以返回给一个变量

1
2
3
4
5
6
7
let n = 10;
let a = if n > 4 {
    n + 1
} else {
    n - 2
};
println!("n: {n}, a: {a}");

注意,n + 1n - 2 后都没有分号,表示它们都是表达式,其结果会赋值给 a

形如

1
2
3
4
5
6
7
fn is_even(num: i32) -> bool {
    if num % 2 == 0 {
        true
    } else {
        false
    }
}

这样的函数也可能常见,一个函数体的最后是一个 if 表达式,每个 if 分支的最后都是一个表达式,某个符合条件的表达式就会作为返回值返回。虽然看上去不是很直观,但的确是这么回事。

if 表达式的判定条件必须是 bool,像数字啥的就不行。

三元运算可以像下面这样表达

1
2
let r = 1;
let a = if r == -1 { "No" } else { "Yes" };

if 每个分支中返回的值的类型必须相同

循环

Rust 有三种循环:loopwhilefor

loop 循环没啥条件,只能用 break 终止。

loop 是一个表达式,因此也可以返回值,break 也是个表达式,可以像用 return 一样使用,可以理解为中断循环时返回一个值。

1
2
3
4
5
6
7
8
let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2;
    }
};
println!("result: {result}");

当多层循环嵌套时,可以给循环加标签,然后在 break 的时候指明要跳出哪层循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let mut count = 0;
'outer: loop {
    println!("count = {count}");
    let mut remain = 10;
    loop {
        println!("remain = {remain}");
        if remain == 9 {
            break;
        }
        if count == 2 {
            break 'outer;
        }
        remain -= 1;
    }
    count += 1;
}

break 把标签和返回值同时使用时返回值需要在后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let mut count = 0;
let res = 'outer: loop {
    println!("count = {count}");
    let mut remain = 10;
    loop {
        println!("remain = {remain}");
        if remain == 9 {
            break;
        }
        if count == 2 {
            break 'outer count;
        }
        remain -= 1;
    }
    count += 1;
};
println!("res: {res}");

while 没啥新东西,不说了。

for 循环的话举两个例子就清楚了

1
2
3
4
5
6
7
8
9
let a = [1, 2, 3, 5, 6];
for i in a {
    print!("{i} ");
}
print!("\n");
for j in 2..5 {
    print!("{j} "); // 2 3 4
}
print!("\n");

其中 2..5 不包括 5

4. 认识所有权

这里先简单记一点关于权限的东西,稍后再细节整理:

R:可读取数据,可拷贝数据
W:可原地更改数据
O:可转交所有权,可释放内存

当一个不可变变量创建的时候(包括用 let 声明赋值和初始化函数参数),它拥有 R-O 权限,如果是一个可变的变量,或者可变函数参数,则拥有 RWO 权限。

当一个变量被不可变引用时,失去 -WO 权限,如果被可变引用,则失去 RWO 权限。

一个引用的变量,其解引用的权限,可以根据类型判断,如果是 &T 则解引用后只有 R-- 权限,如果是 &mut T 则解引用后有 RW- 权限。因为是引用,所以都没有 --O 权限。

为什么要关心一个引用变量解引用后的权限?因为我们操作一个引用变量的时候,大部分时候都是在操作其解引用后的数据,只不过 Rust 自动隐式地帮我们解引用我们不用直接明写出来,但还是需要考虑的。

详细的内容请查看 Rust 所有权

5. 使用结构体组织相关联的数据

5.1 结构体的定义和实例化

结构体基本使用

定义结构体

1
2
3
4
5
6
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

创建实例

1
2
3
4
5
6
7
8
fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}

实例的字段顺序不需要与定义时的一致。

使用结构体的字段

1
println!("name: {}", user1.username);

如果结构体实例是可变的,则可以修改字段

1
2
3
4
5
6
7
8
9
10
11
fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("username123"),
        email: String::from("userone@example.com"),
        sign_in_count: 1,
    };

    user1.sign_in_count += 1;
    println!("{} has signed in {} times", user1.username, user1.sign_in_count);
}

注意,不能只指定结构体的某个字段可变。要变整个都变。

结构体实例也是表达式,因此可以放在函数最后返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fn build_user(username: String, email: String) -> User {
    User { 
        active: true,
        username: username,
        email: email, 
        sign_in_count: 1
    }
}

fn main() {
    let mut user1 = build_user(
        String::from("username123"), 
        String::from("userone@example.com")
    );

    user1.sign_in_count += 1;
    println!("{} has signed in {} times", user1.username, user1.sign_in_count);
}

上述函数中,结构体字段名和函数参数名相同,这种情况也很常见,因此有一种简写语法

1
2
3
4
5
6
7
8
fn build_user(username: String, email: String) -> User {
    User { 
        active: true,
        username,
        email, 
        sign_in_count: 1
    }
}

有时候我们创建的新结构体实例的大部分字段与已有的相同,只有部分不同,就可以用结构体更新语法

1
2
3
4
5
let user2 = User {
    email: String::from("another@example.com"),
    ..user1
};
println!("{} 's email is {}", user2.username, user2.email);

注意,..user1 必须在结构体所有字段的最后,且之后不能有逗号。

..user1 这样的更新语法等同于 = 赋值,因此也可能会转移所有权。比如上例中,user1.username 就被转移了所有权,在创建 user2 之后就不能使用 user1.username 了,但是 user.active 或者 user.sign_in_count 还可以使用,因为它们不是堆数据,支持 Copy。

元组结构体

元组结构体的字段只有类型而没有名称,使用方法类似元组

1
2
3
4
5
6
7
8
9
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let grey = Color(127, 127, 127);
    let center = Point(5, 10, 5);

    println!("R: {}, y: {}", grey.0, center.1);
}

类单元结构体

类单元结构体就跟单元元组类似,没有任何字段,通常用于定义一个不需要存储任何数据只需要实现 trait 的结构体(之后讨论)

1
2
3
4
5
struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

5.2 结构体示例程序

5.3 方法语法

本文由作者按照 CC BY 4.0 进行授权