Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

1.Rust、Cargo简介

hello_world

第一个 Rust 程序:

新建完文件夹后,新建一个后缀名为 .rs 的文件(Rust 源文件总是以 .rs 拓展名结尾

新建后编写下面的代码:

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

保存文件,使用命令运行:

1
rustc main.rs

Rust 程序分析:

Rust 和 C 语言类似,需要有一个 main函数。

1
2
3
fn main() {

}

官方文档中的描述:

main 函数是一个特殊的函数:在可执行的 Rust 程序中,它总是最先运行的代码。第一行代码声明了一个叫做 main 的函数,它没有参数也没有返回值。如果有参数的话,它们的名称应该出现在小括号 () 中。

函数体被包裹在 {} 中。Rust 要求所有函数体都要用花括号包裹起来。一般来说,将左花括号与函数声明置于同一行并以空格分隔,是良好的代码风格。

main 函数中有下面的代码:

1
println!("Hello, world");

需要注意的有:

  • 首先,Rust 的缩进风格是使用 4 个空格,而不是 1 个制表符(tab)
  • println!调用了一个 Rust 宏(macro)。如果是调用函数,应该输入println(没有 ! )。**==当看到符号!的时候,就意味着调用的是宏而不是普通函数,并且宏并不总是遵循与函数相同的规则==**
  • "hello, world"是一个字符串。把这个字符串作为一个参数传递给println!,字符串将被打印到屏幕上。
  • 大部分 Rust 代码行以分号结尾。

在 Rust 中,编译个运行时彼此独立的步骤

在运行Rust程序之前,必须使用 Rust 编译器编译它。即输入 rustc 命令并传入源代码名称:

1
rustc main.rs

与 C 语言类型,Rust 编译成功后会输出一个二进制的可执行文件

在 Linux 和 macOS,可以看到两个文件。在 Windows PowerShell 中,可以看到的三个文件。.exe .pdb .rs

其中拓展名为*.pdb*的文件是一个包含调试信息的文件。

==Rust 是一种 预编译静态类型ahead-of-time compiled)语言,这意味着你可以编译程序,并将可执行文件送给其他人,他们甚至不需要安装 Rust 就可以运行。==

Cargo

可参考:Rust 圣经-初始Cargo

定义:(来自官方文档)

Cargo 是 Rust 的构建系统和包管理器。大多数 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它可以为你处理很多任务,比如构建代码、下载依赖库并编译这些库。(我们把代码所需要的库叫做 依赖dependencies))。

在编写一些较复杂的 Rust 程序时,需要添加依赖项,如果使用 Cargo 启动项目,则添加依赖项将更加容易。

使用 Cargo 创建项目

1
2
cargo new hello_cargo
cd hello_cargo

第一行命令新建了一个名为*hello_cargo的目录和项目。我们将项目命名为hello_cargo*,同时 Cargo 在一个同名目录中创建项目文件

进入 hello_cargo 目录并列出文件。可以看到 Cargo 生成了两个文件和一个目录:一个 Cargo.toml 文件,一个 src 目录,以及位于src 目录中的*main.rs文件。*

这也会在 hello_cargo 目录初始化了一个 git 仓库,以及一个 .gitignore 文件。如果在一个已经存在的 git 仓库中运行 cargo new,则这些 git 相关文件则不会生成;可以通过运行 cargo new --vcs=git 来覆盖这些行为。

Cargo.toml

这个文件使用 TOML*(Tom’s Obvious,Minimal Language)*格式,这是 Cargo 配置文件的格式。

1
2
3
4
5
6
7
8
9
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

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

[dependencies]

上面的示例:*cargo new*命令生成的 *Cargo.toml*的内容

  • 第一行,[package],是一个片段(section)标题,表明下面的语句用来配置一个包。随着我们在这个文件增加更多的信息,还将增加其他片段(section)。
  • 接下来的三行设置了**Cargo编译程序所需的配置:**
    • 项目名称
    • 项目版本
    • 要使用的 Rust 版本
  • 最后一行,**[dependencies],是罗列项目以来的片段的开始。在 Rust 中,代码包被称为*crates***,这个项目不需要其他的 crate,不过在后面会用到依赖,那是会用得上这个片段。

这里会介绍edition的值。

src 目录

打开 ***src目录,可以看到一个main.rs***文件:

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

Cargo 会自动生成一个”Hello ,world!”程序

目前为止,我们自己的项目与使用 Cargo 生成项目的区别是 Cargo 将代码放到*src目录中,同时项目根目录包含一个Cargo.toml*配置文件

Cargo 期望源文件存放在 src 目录中。项目根目录只存放 README、license 信息、配置文件和其他跟代码无关的文件。使用 Cargo 帮助你保持项目干净整洁,一切井井有条。

如果没有使用 Cargo 开始项目,我们可以将其转化成为一个 Cargo 项目,将代码放入*src目录,并创建一个合适的Cargo.toml*文件。

构建&运行 Cargo 项目

通过 Cargo 构建和运行”Hello, world!” 程序有什么不同?

Cargo build

hello_cargo 目录下,输入下面的命令来构建项目:

1
2
3
cargo build
Compiling hello_cargo v0.1.0 (F:\Rust\projects\hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.38s

上述命令会创建一个可执行文件target/debug/hello_cargo.exe,而不是放在目前目录下。由于默认的构建方法是调试构建(debug building),Cargo 会将可执行文件放在名为 *debug*的目录中。**可通过这个命令运行可执行文件:

1
2
.\target\debug\hello_cargo.exe
Hello, world!

首次运行cargo build时,也会使Cargo在项目根目录创建一个新文件*Cargo.lock*。这个文件记录项目以来的实际版本因为这个项目没有以来,所以其中内容较少。

Cargo run

刚刚使用cargo build构建了项目,并使用.\target\debug\hello_cargo来运行了程序,也可以使用cargo run 在一个命令中同时编译并运行生成的可执行文件:

1
2
3
4
cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target\debug\hello_cargo.exe`
Hello, world!

Cargo check

Cargo 还提供了一个叫cargo check的命令。**cargo check命令可以快速检查代码确保其可以编译,但并不产生可执行文件:**

1
2
3
cargo check
Checking hello_cargo v0.1.0 (F:\Rust\projects\hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.05s

小结:

  • 可以使用 cargo new 创建项目。
  • 可以使用 cargo build 构建项目。
  • 可以使用 cargo run 一步构建并运行项目。
  • 可以使用 cargo check 在不生成二进制文件的情况下构建项目来检查错误。
  • 有别于将构建结果放在与源码相同的目录,Cargo 会将其放到 target/debug 目录。

使用 Cargo 的一个额外的优点是,不管你使用什么操作系统,其命令都是一样的。所以从现在开始本书将不再为 Linux 和 macOS 以及 Windows 提供相应的命令。

发布(release)构建

当项目最终准备好发布时,可以使用cargo build --release来优化编译项目。这会在*target/release*而不是target/debug 生成可执行文件。这些优化可以让 Rust 代码运行的更快不过启用这些优化也需要消耗更长的编译时间。这也就是为什么会有两种不同的配置:**一种是为了开发,需要经常快速重新构建;另一种是为了用户构建最终程序,它们不会经常重新构建,并且希望程序运行得越快越好。

如果在测试代码的运行时间,确保运行cargo build --release并使用*target/release*下的可执行文件进行测试。

不仅仅是 Hello world

多国语言的“hello world”

使用 VSCode 新建一个 world_hello 工程,进入 main.rs文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn greet_world() {
let southern_germany = "Grüß Gott!";
let chinese = "世界,你好";
let english = "World, hello";
let regions = [southern_germany, chinese, english];
for region in regions.iter() {
println!("{}", &region);
}
}

fn main() {
greet_world();
}

首先,Rust 原生支持 UTF-8 编码的字符串,这意味着你可以很容易的使用世界各国文字作为字符串内容。

其次,关注下 println 后面的 !,是在 Rust 中的 操作。目前可认为是一种特殊类型函数。

对于 println 来说,是使用 {}作为输出占位符,因为 Rust 在底层帮我们做了大量工作,会自动识别输出数据的类型,例如当前例子,会识别为 String 类型。

最后,和其它语言不同,Rust 的集合类型不能直接进行循环,需要变成迭代器(这里是通过 .iter() 方法),才能用于迭代循环。在目前来看,你会觉得这一点好像挺麻烦,不急,以后就知道这么做的好处所在。

实际上这段代码可以简写,在 2021 edition 及以后,支持直接写 for region in regions,原因会在迭代器章节的开头提到,是因为 for 隐式地将 regions 转换成迭代器。

至于函数声明、调用、数组的使用,和其它语言没什么区别.

Rust 初印象

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
35
36
37
38
39
40
41
42
43
fn main() {
let penguin_data = "\
common name,length (cm)
Little penguin,33
Yellow-eyed penguin,65
Fiordland penguin,60
Invalid,data
";

let records = penguin_data.lines();

for (i, record) in records.enumerate() {
if i == 0 || record.trim().len() == 0 {
continue;
}

// 声明一个 fields 变量,类型是 Vec
// Vec 是 vector 的缩写,是一个可伸缩的集合类型,可以认为是一个动态数组
// <_>表示 Vec 中的元素类型由编译器自行推断,在很多场景下,都会帮我们省却不少功夫
let fields: Vec<_> = record
.split(',')
.map(|field| field.trim())
.collect();
if cfg!(debug_assertions) {
// 输出到标准错误输出
eprintln!("debug: {:?} -> {:?}",
record, fields);
}

let name = fields[0];
// 1. 尝试把 fields[1] 的值转换为 f32 类型的浮点数,如果成功,则把 f32 值赋给 length 变量
//
// 2. if let 是一个匹配表达式,用来从=右边的结果中,匹配出 length 的值:
// 1)当=右边的表达式执行成功,则会返回一个 Ok(f32) 的类型,若失败,则会返回一个 Err(e) 类型,if let 的作用就是仅匹配 Ok 也就是成功的情况,如果是错误,就直接忽略
// 2)同时 if let 还会做一次解构匹配,通过 Ok(length) 去匹配右边的 Ok(f32),最终把相应的 f32 值赋给 length
//
// 3. 当然你也可以忽略成功的情况,用 if let Err(e) = fields[1].parse::<f32>() {...}匹配出错误,然后打印出来,但是没啥卵用
if let Ok(length) = fields[1].parse::<f32>() {
// 输出到标准输出
println!("{}, {}cm", name, length);
}
}
}

上面代码中,值得注意的 Rust 特性有:

  • 控制流:forcontinue 连在一起使用,实现循环控制。
  • 方法语法:由于 Rust 没有继承,因此 Rust 不是传统意义上的面向对象语言,但是它却从 OO 语言那里偷师了方法的使用 record.trim()record.split(',') 等。
  • 高阶函数编程:函数可以作为参数也能作为返回值,例如 .map(|field| field.trim()),这里 map 方法中使用闭包函数作为参数,也可以称呼为 匿名函数lambda 函数
  • 类型标注:if let Ok(length) = fields[1].parse::<f32>(),通过 ::<f32> 的使用,告诉编译器 length 是一个 f32 类型的浮点数。这种类型标注不是很常用,但是在编译器无法推断出你的数据类型时,就很有用了。
  • 条件编译:if cfg!(debug_assertions),说明紧跟其后的输出(打印)只在 debug 模式下生效。
  • 隐式返回:Rust 提供了 return 关键字用于函数返回,但是在很多时候,我们可以省略它。因为 Rust 是 基于表达式的语言

在终端中运行上述代码时,会看到很多 debug: ... 的输出,上面有讲,这些都是 条件编译 的输出

cargo run 默认是运行 debug 模式。因此想要消灭那些 debug: 输出,需要更改为其它模式,其中最常用的模式就是 --release 也就是生产发布的模式。