第2章 | Rust 导览

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 第2章 | Rust 导览

2.1 rustup 与 Cargo

安装 Rust 的最佳方式是使用 rustup。请转到 rustup.rs 网站并按照那里的说明进行操作。

还可以到 Rust 网站获取针对 Linux、macOS 和 Windows 的预构建包。Rust 也已经包含在某些操作系统的发行版中。建议使用 rustup,因为它是专门管理 Rust 安装的工具,就像 Ruby 中的 RVM 或 Node 中的 NVM。例如,当 Rust 发布新版本时,你就可以通过键入 rustup update来实现一键升级。

无论采用哪种方式,完成安装之后,你的命令行中都会有 3 条新命令:

$ cargo --version
cargo 1.49.0 (d00d64df9 2020-12-05)
$ rustc --version
rustc 1.49.0 (e1884a8e3 2020-12-29)
$ rustdoc --version
rustdoc 1.49.0 (e1884a8e3 2020-12-29)

在这里,$是命令提示符,在 Windows 上,则会是 C:>之类的文本。在刚才的记录中,我们运行了 3 条已安装的命令,并要求每条命令报告其版本号。下面来逐个看看每条命令。

cargo是 Rust 的编译管理器、包管理器和通用工具。可以用 Cargo 启动新项目、构建和运行程序,并管理代码所依赖的任何外部库。

rustc是 Rust 编译器。通常 Cargo 会替我们调用此编译器,但有时也需要直接运行它。

rustdoc是 Rust 文档工具。如果你在程序源代码中以适当形式的注释编写文档,那么 rustdoc就可以从中构建出格式良好的 HTML。与 rustc一样,通常 Cargo 会替我们运行 rustdoc。

为便于使用,Cargo 可以为我们创建一个新的 Rust 包,并适当准备一些标准化的元数据:

$ cargo new hello
     Created binary (application) `hello` package

该命令会创建一个名为 hello 的新包目录,用于构建命令行可执行文件。 查看包的顶层目录:

$ cd hello
$ ls -la
total 24
drwxrwxr-x.  4 jimb jimb 4096 Sep 22 21:09 .
drwx------. 62 jimb jimb 4096 Sep 22 21:09 ..
drwxrwxr-x.  6 jimb jimb 4096 Sep 22 21:09 .git
-rw-rw-r--.  1 jimb jimb    7 Sep 22 21:09 .gitignore
-rw-rw-r--.  1 jimb jimb   88 Sep 22 21:09 Cargo.toml
drwxrwxr-x.  2 jimb jimb 4096 Sep 22 21:09 src

我们看到 Cargo 已经创建了一个名为 Cargo.toml 的文件来保存此包的元数据。目前这个文件还没有多少内容:

[package]
name = "hello"
version = "0.1.0"
edition = "2021"

如果程序依赖于其他库,那么可以把它们记录在这个文件中,Cargo 将为我们下载、构建和更新这些库。第 8 章会详细介绍 Cargo.toml 文件。 Cargo 已将我们的包设置为与版本控制系统 git一起使用,并为此创建了一个元数据子目录 .git 和一个 .gitignore 文件。可以通过在命令行中将 --vcs none传给 cargo new来要求 Cargo 跳过此步骤。

src 子目录包含实际的 Rust 代码:

$ cd src
$ ls -l
total 4
-rw-rw-r--. 1 jimb jimb 45 Sep 22 21:09 main.rs

Cargo 似乎已经替我们写好一部分程序了。main.rs 文件包含以下文本:

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

在 Rust 中,你甚至不需要编写自己的“Hello, World!”程序。这是 Rust 新程序样板的职责,该程序样板包括两个文件,总共 13 行代码。 可以在包内的任意目录下调用 cargo run命令来构建和运行程序:

$ cargo run
   Compiling hello v0.1.0 (/home/jimb/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `/home/jimb/rust/hello/target/debug/hello`
Hello, world!

注意!!

如果出现运行报错 error: linker link.exe not found 的情况,可能是没有安装 Visual Studio 生成工具

这里 Cargo 先调用 Rust 编译器 rustc,然后运行了它生成的可执行文件。Cargo 将可执行文件放在此包顶层的 target 子目录中:

$ ls -l ../target/debug
total 580
drwxrwxr-x. 2 jimb jimb   4096 Sep 22 21:37 build
drwxrwxr-x. 2 jimb jimb   4096 Sep 22 21:37 deps
drwxrwxr-x. 2 jimb jimb   4096 Sep 22 21:37 examples
-rwxrwxr-x. 1 jimb jimb 576632 Sep 22 21:37 hello
-rw-rw-r--. 1 jimb jimb    198 Sep 22 21:37 hello.d
drwxrwxr-x. 2 jimb jimb     68 Sep 22 21:37 incremental
$ ../target/debug/hello
Hello, world!

完工之后,Cargo 还可以帮我们清理生成的文件。

$ cargo clean
$ ../target/debug/hello
bash: ../target/debug/hello: No such file or directory

2.2 Rust 函数

Rust 在语法设计上刻意减少了原创性。如果你熟悉 C、C++、Java 或 JavaScript,那么就能通过 Rust 程序的一般性构造找到自己的快速学习之道。这是一个使用欧几里得算法计算两个整数的最大公约数的函数。可以将这些代码添加到 src/main.rs 的末尾:

fn gcd(mut n: u64, mut m: u64) -> u64 {
    assert!(n != 0 && m != 0);
    while m != 0 {
        if m < n {
            let t = m;
            m = n;
            n = t;
        }
        m = m % n;
    }
    n
}

fn(发音为 /fʌn/)关键字引入了一个函数。这里我们定义了一个名为 gcd 的函数,它有两个参数(nm),每个参数都是 u64 类型,即一个无符号的 64 位整数。-> 标记后面紧跟着返回类型,表示此函数返回一个 u64 值。4 空格缩进是 Rust 的标准风格。

Rust 的“机器整数类型名”揭示了它们的大小和符号:i32 是一个带符号的 32 位整数,u8 是一个无符号的 8 位整数(“字节”值),以此类推。isize 类型和 usize 类型保存着恰好等于“指针大小”的有符号整数和无符号整数,在 32 位平台上是 32 位长,在 64 位平台上则是 64 位长。Rust 还有 f32f64 这两种浮点类型,它们分别是 IEEE 单精度浮点类型和 IEEE 双精度浮点类型,就像 C 和 C++ 中的 floatdouble

默认情况下,一经初始化,变量的值就不能再改变了,但是在参数 nm 之前放置 mut(发音为 /mjuːt/,是 mutable 的缩写)关键字将会允许我们在函数体中赋值给它们。实际上,大多数变量是不需要被赋值的,而对于那些确实需要被赋值的变量,mut 关键字相当于用一个醒目的提示来帮我们阅读代码。

let 语句会声明一个局部变量,比如本函数中的 t。只要 Rust 能从变量的使用方式中推断出 t 的类型,就不需要标注其类型。在此函数中,通过匹配 mn,可以推断出唯一适用于 t 的类型是 u64。Rust 只会推断函数体内部的类型,因此必须像之前那样写出函数参数的类型和返回值的类型。如果想明确写出 t 的类型,那么可以这样写:

let t: u64 = m;

Rust 有 return 语句,但这里的 gcd 函数并不需要。如果一个函数体以没有尾随着分号的表达式结尾,那么这个表达式就是函数的返回值。事实上,花括号包起来的任意代码块都可以用作表达式。例如,下面是一个打印了一条信息然后以 x.cos() 作为其值的表达式:

{
    println!("evaluating cos x");
    x.cos()
}

在 Rust 中,当控制流“正常离开函数的末尾”时,通常会以上述形式创建函数的返回值,return 语句只会用在从函数中间显式地提前返回的场景中。

2.3 编写与运行单元测试

Rust 语言内置了对测试的简单支持。为了测试 gcd 函数,可以在 src/main.rs 的末尾添加下面这段代码:

#[test]
fn test_gcd() {
    assert_eq!(gcd(14, 15), 1);
    assert_eq!(gcd(2 * 3 * 5 * 11 * 17,
                   3 * 7 * 11 * 13 * 19),
               3 * 11);
}

这里我们定义了一个名为 test_gcd 的函数,该函数会调用 gcd 并检查它是否返回了正确的值。此定义顶部的 #[test]test_gcd 标记为“测试函数”,在正常编译时会跳过它,但如果用 cargo test 命令运行我们的程序,则会自动包含并调用它。可以让测试函数分散在源代码树中,紧挨着它们所测试的代码,cargo test 会自动收集并运行它们。

#[test] 标记是属性(attribute)的示例之一。属性是一个开放式体系,可以用附加信息给函数和其他声明做标记,就像 C++ 和 C# 中的属性或 Java 中的注解(annotation)一样。属性可用于控制编译器警告和代码风格检查、有条件地包含代码(就像 C 和 C++ 中的 #ifdef 一样)、告诉 Rust 如何与其他语言编写的代码互动,等等。后面还会介绍更多的属性示例。

gcdtest_gcd 的定义添加到本章开头创建的 hello 包中,如果当前目录位于此包子树中的任意位置,可以用如下方式运行测试。

$ cargo test
   Compiling hello v0.1.0 (/home/jimb/rust/hello)
    Finished test [unoptimized + debuginfo] target(s) in 0.35s
     Running unittests (/home/jimb/rust/hello/target/debug/deps/hello-2375...)
running 1 test
test test_gcd ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

2.4 处理命令行参数

为了让我们的程序接收一系列数值作为命令行参数并打印出它们的最大公约数,可以将 src/main.rs 中的 main 函数替换为以下内容:

use std::str::FromStr;
use std::env;
fn main() {
    let mut numbers = Vec::new();
    for arg in env::args().skip(1) {
        numbers.push(u64::from_str(&arg)
                     .expect("error parsing argument"));
    }
    if numbers.len() == 0 {
        eprintln!("Usage: gcd NUMBER ...");
        std::process::exit(1);
    }
    let mut d = numbers[0];
    for m in &numbers[1..] {
        d = gcd(d, *m);
    }
    println!("The greatest common divisor of {:?} is {}",
             numbers, d);
}

我们来逐段分析一下:

use std::str::FromStr;
use std::env;

第一个 use 声明将标准库中的 FromStr特型引入了当前作用域。特型是可以由类型实现的方法集合。任何实现了 FromStr 特型的类型都有一个 from_str 方法,该方法会尝试从字符串中解析这个类型的值。u64 类型实现了 FromStr,所以我们将调用 u64::from_str 来解析程序中的命令行参数。尽管我们从未在程序的其他地方用到 FromStr 这个名字,但仍然要 use(使用)它,因为要想使用某个特型的方法,该特型就必须在作用域内。第 11 章会详细介绍特型。

第二个 use 声明引入了 std::env 模块,该模块提供了与执行环境交互时会用到的几个函数和类型,包括 args 函数,该函数能让我们访问程序中的命令行参数。

继续看程序中的 main 函数:

fn main() {

main 函数没有返回值,所以可以简单地省略 -> 和通常会跟在参数表后面的返回类型。

let mut numbers = Vec::new();

我们声明了一个可变的局部变量 numbers 并将其初始化为空向量。Vec 是 Rust 的可增长向量类型,类似于 C++ 的 std::vector、Python 的列表或 JavaScript 的数组。虽然从设计上说向量可以动态扩充或收缩,但仍然要标记为 mut,这样 Rust 才能把新值压入末尾。

numbers 的类型是 Vec<u64>,这是一个可以容纳 u64 类型的值的向量,但和以前一样,不需要把类型写出来。Rust 会推断它,一部分原因是我们将 u64 类型的值压入了此向量,另一部分原因是我们将此向量的元素传给了 gcd,后者只接受 u64 类型的值。

for arg in env::args().skip(1) {

这里使用了 for 循环来处理命令行参数,依次将变量 arg 指向每个参数并运行循环体。

std::env 模块的 args 函数会返回一个迭代器,此迭代器会按需生成1每个参数,并在完成时给出提示。各种迭代器在 Rust 中无处不在,标准库中也包括一些迭代器,这些迭代器可以生成向量的元素、文件每一行的内容、通信信道上接收到的信息,以及几乎任何有意义的循环变量。Rust 的迭代器非常高效,编译器通常能将它们翻译成与手写循环相同的代码。第 15 章会展示迭代器的工作原理并给出相关示例。

1“生成”只是沿袭普遍译法,事实上,这里并不会创建任何新条目,只是把已有条目提供给消费者。——译者注

除了与 for 循环一起使用,迭代器还包含大量可以直接使用的方法。例如,args 返回的迭代器生成的第一个值永远是正在运行的程序的名称。如果想跳过它,就要调用迭代器的 skip 方法来生成一个新的迭代器,新迭代器会略去第一个值。

numbers.push(u64::from_str(&arg)
             .expect("error parsing argument"));

这里我们调用了 u64::from_str 来试图将命令行参数 arg 解析为一个无符号的 64 位整数。u64::from_str 并不是 u64 值上的某个方法,而是与 u64 类型相关联的函数,类似于 C++ 或 Java 中的静态方法。from_str 函数不会直接返回 u64,而是返回一个指明本次解析已成功或失败的 Result 值。Result 值是以下两种变体之一:

  • 形如 Ok(v) 的值,表示解析成功了,v 是所生成的值;
  • 形如 Err(e) 的值,表示解析失败了,e 是解释原因的错误值。

执行任何可能会失败的操作(例如执行输入或输出或者以其他方式与操作系统交互)的函数都会返回一个 Result 类型,其 Ok 变体会携带成功结果(传输的字节数、打开的文件等),而其 Err 变体会携带错误码,以指明出了什么问题。与大多数现代语言不同,Rust 没有异常(exception):所有错误都使用 Resultpanic 进行处理,详见第 7 章。

我们用 Resultexpect 方法来检查本次解析是否成功。如果结果是 Err(e),那么 expect 就会打印出一条包含 e 的消息并直接退出程序。但如果结果是 Ok(v),则 expect 会简单地返回 v 本身,最终我们会将其压入这个数值向量的末尾。

if numbers.len() == 0 {
    eprintln!("Usage: gcd NUMBER ...");
    std::process::exit(1);
}

空数组没有最大公约数,因此要检查此向量是否至少包含一个元素,如果没有则退出程序并报错。这里我们用 eprintln! 宏将错误消息写入标准错误流。

let mut d = numbers[0];
for m in &numbers[1..] {
    d = gcd(d, *m);
}

该循环使用 d 作为其运行期间的值,不断地把它更新为已处理的所有数值的最大公约数。和以前一样,必须将 d 标记为可变,以便在循环中给它赋值。

这个 for 循环有两个值得注意的地方。首先,我们写了 for m in &numbers[1..],那么这里的 & 运算符有什么用呢?其次,我们写了 gcd(d, *m),那么 *m 中的 * 又有什么用呢?这两个细节是紧密相关的。

迄今为止,我们的代码只是在对简单的值(例如适合固定大小内存块的整数)进行操作。但现在我们要迭代一个向量,它可以是任意大小,而且可能会非常大。Rust 在处理这类值时非常慎重:它想让程序员控制内存消耗,明确每个值的生存时间,同时还要确保当不再需要这些值时能及时释放内存。

所以在进行迭代时,需要告诉 Rust,该向量的所有权应该留在 numbers 上,我们只是为了本次循环而借用它的元素。&numbers[1..] 中的 & 运算符会从向量中借用从第二个元素开始的引用for 循环会遍历这些被引用的元素,让 m 依次借出每个元素。*m 中的 * 运算符会将 m解引用,产生它所引用的值,这就是要传给 gcd 的下一个 u64。最后,由于 numbers 拥有着此向量,因此当 main 末尾的 numbers 超出作用域时,Rust 会自动释放它。

Rust 的所有权规则和引用规则是 Rust 内存管理和并发安全的关键所在,第 4 章和第 5 章会对此进行详细讨论。只有熟悉了这些规则,才算熟练掌握了 Rust。但是对于这个介绍性的导览,你只需要知道 &x 借用了对 x 的引用,而 *r 访问的是 r 所引用的值就足够了。

继续我们的程序:

println!("The greatest common divisor of {:?} is {}",
         numbers, d);

遍历 numbers 的元素后,程序会将结果打印到标准输出流。println! 宏会接受一个模板字符串,在模板字符串中以 {...} 形式标出的位置按要求格式化并插入剩余的参数,最后将结果写入标准输出流。

C 和 C++ 要求 main 在程序成功完成时返回 0,在出现问题时返回非零的退出状态,而 Rust 假设只要 main 完全返回,程序就算成功完成。只有显式地调用像 expectstd::process::exit 这样的函数,才能让程序以表示错误的状态码终止。

cargo run 命令可以将参数传给程序,因此可以试试下面这些命令行处理:

$ cargo run 42 56
   Compiling hello v0.1.0 (/home/jimb/rust/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `/home/jimb/rust/hello/target/debug/hello 42 56`
The greatest common divisor of [42, 56] is 14
$ cargo run 799459 28823 27347
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `/home/jimb/rust/hello/target/debug/hello 799459 28823 27347`
The greatest common divisor of [799459, 28823, 27347] is 41
$ cargo run 83
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `/home/jimb/rust/hello/target/debug/hello 83`
The greatest common divisor of [83] is 83
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `/home/jimb/rust/hello/target/debug/hello`
Usage: gcd NUMBER ...

对于一个前端开发选手来讲,Rust目前的代码看着不习惯

本节使用了 Rust 标准库中的一些特性。如果你好奇还有哪些别的特性,强烈建议看看 Rust 的在线文档。它具有实时搜索功能,能让你的探索更容易,其中还包括指向源代码的链接。安装 Rust 时,rustup 命令会自动在你的计算机上安装一份文档副本。你既可以在 Rust 网站上查看标准库文档,也可以使用以下命令打开浏览器查看

$ rustup doc --std

这点很不错,直接把文档本地化,很方便

2.5 搭建 Web 服务器

Rust 的优势之一是在 crates.io 网站上发布的大量免费的可用包。cargo 命令可以让你的代码轻松使用 crates.io 上的包:它将下载包的正确版本,然后会构建包,并根据用户的要求更新包。一个 Rust 包,无论是库还是可执行文件,都叫作 crate(发音为 /kreɪt/,意思是“板条箱”)2。Cargo 和 crates.io 的名字都来源于这个术语。

2相对于包,crate 更强调自己的硬边界,也暗示着更高的安全性。助记:马车(Cargo)上装着一些板条箱。——译者注

为了展示这种工作过程,我们将使用 actix-web(Web 框架 crate)、serde(序列化 crate)以及它们所依赖的各种其他 crate 来组装出一个简单的 Web 服务器。如图 2-1 所示,该网站会提示用户输入两个数值并计算它们的最大公约数。

image.png

图 2-1:计算最大公约数的网页

首先,让 Cargo 创建一个新包,命名为 actix-gcd

$ cargo new actix-gcd
     Created binary (application) `actix-gcd` package
$ cd actix-gcd

然后,编辑新项目的 Cargo.toml 文件以列出所要使用的包,其内容应该是这样的:

[package]
name = "actix-gcd"
version = "0.1.0"
edition = "2021"
# 请到“The Cargo Book”查看更多的键及其定义
[dependencies]
actix-web = "1.0.8"
serde = { version = "1.0", features = ["derive"] }

Cargo.toml 中 [dependencies] 部分的每一行都给出了 crates.io 上的 crate 名称,以及我们想要使用的那个 crate 的版本。在本例中,我们需要 1.0.8 版的 actix-web crate 和 1.0 版的 serde crate。crates.io 上这些 crate 的版本很可能比此处展示的版本新,但通过指明在测试此代码时所使用的特定版本,可以确保即使发布了新版本的包,这些代码仍然能继续编译。3第 8 章会更详细地讨论版本管理。

3理想情况下确实如此,但现实中并不是每个第三方包都能实现这种兼容性。——译者注

crate 可能具备某些可选特性:一部分接口或实现不是所有用户都需要的,但将其包含在那个 crate 中仍然有意义。例如,serde crate 就提供了一种非常简洁的方式来处理来自 Web 表单的数据,但根据 serde 的文档,只有选择了此 crate 的 derive 特性时它才可用,因此我们在 Cargo.toml 文件中请求了它。

请注意,只需指定要直接用到的那些 crate 即可,cargo 会负责把它们自身依赖的所有其他 crate 带进来。

在第一次迭代中,我们将实现此 Web 服务器的一个简单版本:它只会给出让用户输入要计算的数值的页面。actix-gcd/src/main.rs 的内容如下所示:

use actix_web::{web, App, HttpResponse, HttpServer};
fn main() {
    let server = HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(get_index))
    });
    println!("Serving on http://localhost:3000...");
    server
        .bind("127.0.0.1:3000").expect("error binding server to address")
        .run().expect("error running server");
}
fn get_index() -> HttpResponse {
    HttpResponse::Ok()
        .content_type("text/html")
        .body(
            r#"
                <title>GCD Calculator</title>
                <form action="/gcd" method="post">
                <input type="text" name="n"/>
                <input type="text" name="m"/>
                <button type="submit">Compute GCD</button>
                </form>
            "#,
        )
}

use 声明可以让来自 actix-web crate 的定义用起来更容易些。当我们写下 use actix_web::{...} 时,花括号中列出的每个名称都可以直接用在代码中,而不必每次都拼出全名,比如 actix_web::HttpResponse 可以简写为 HttpResponse。(稍后还会提及 serde crate。)

main 函数很简单:它调用 HttpServer::new 创建了一个响应单个路径 "/" 请求的服务器,打印了一条信息以提醒我们该如何连接它,然后监听本机的 TCP 端口 3000。

我们传给 HttpServer::new 的参数是 Rust 闭包表达式 || { App::new() ... }。闭包是一个可以像函数一样被调用的值。这个闭包没有参数,如果有参数,那么可以将参数名放在两条竖线 || 之间。{ ... } 是闭包的主体。当我们启动服务器时,Actix 会启动一个线程池来处理传入的请求。每个线程都会调用这个闭包来获取 App 值的新副本,以告诉此线程该如何路由这些请求并处理它们。

闭包会调用 App::new 来创建一个新的空白 App,然后调用它的 route 方法为路径 "/" 添加一个路由。提供给该路由的处理程序 web::get().to(get_index) 会通过调用函数 get_index 来处理 HTTP 的 GET 请求。route 方法的返回值就是调用它的那个 App,不过其现在已经有了新的路由。由于闭包主体的末尾没有分号,因此此 App 就是闭包的返回值,可供 HttpServer 线程使用。

get_index 函数会构建一个 HttpResponse 值,该值表示对 HTTP GET / 请求的响应。HttpResponse::Ok() 表示 HTTP 200 OK 状态,意味着请求成功。我们会调用它的 content_type 方法和 body 方法来填入该响应的细节,每次调用都会返回在前一次基础上修改过的 HttpResponse。最后会以 body 的返回值作为 get_index 的返回值。

由于响应文本包含很多双引号,因此我们使用 Rust 的“原始字符串”语法来编写它:首先是字母 r、0 到多个井号(#)标记、一个双引号,然后是字符串本体,并以另一个双引号结尾,后跟相同数量的 # 标记。任何字符都可以出现在原始字符串中而不被转义,包括双引号。事实上,Rust 根本不认识像 " 这样的转义序列。我们总是可以在引号周围使用比文本内容中出现过的 # 更多的 # 标记,以确保字符串能在期望的地方结束。

编写完 main.rs 后,可以使用 cargo run 命令来执行为运行它而要做的一切工作:获取所需的 crate、编译它们、构建我们自己的程序、将所有内容链接在一起,最后启动 main.rs。

$ cargo run
    Updating crates.io index
 Downloading crates ...
  Downloaded serde v1.0.100
  Downloaded actix-web v1.0.8
  Downloaded serde_derive v1.0.100
...
   Compiling serde_json v1.0.40
   Compiling actix-router v0.1.5
   Compiling actix-http v0.2.10
   Compiling awc v0.2.7
   Compiling actix-web v1.0.8
   Compiling gcd v0.1.0 (/home/jimb/rust/actix-gcd)
    Finished dev [unoptimized + debuginfo] target(s) in 1m 24s
     Running `/home/jimb/rust/actix-gcd/target/debug/actix-gcd`
Serving on http://localhost:3000...

此刻,在浏览器中访问给定的 URL 就会看到图 2-1 所示的页面。

但很遗憾,单击“Compute GCD”除了将浏览器导航到一个空白页面外,没有做任何事。为了继续解决这个问题,可以往 App 中添加另一个路由,以处理来自表单的 POST 请求。

现在终于用到我们曾在 Cargo.toml 文件中列出的 serde crate 了:它提供了一个便捷工具来协助处理表单数据。首先,将以下 use 指令添加到 src/main.rs 的顶部:

use serde::Deserialize;

Rust 程序员通常会将所有的 use 声明集中放在文件的顶部,但这并非绝对必要:Rust 允许这些声明以任意顺序出现,只要它们出现在适当的嵌套级别即可。

接下来,定义一个 Rust 结构体类型,用以表示期望从表单中获得的值:

#[derive(Deserialize)]
struct GcdParameters {
    n: u64,
    m: u64,
}

上述代码定义了一个名为 GcdParameters 的新类型,它有两个字段(nm),每个字段都是一个 u64,这是我们的 gcd 函数想要的参数类型。

struct 定义上面的注解是一个属性,就像之前用来标记测试函数的 #[test] 属性一样。在类型定义之上放置一个 #[derive(Deserialize)] 属性会要求 serde crate 在程序编译时检查此类型并自动生成代码,以便从 HTML 表单 POST 提交过来的格式化数据中解析出此类型的值。事实上,该属性足以让你从几乎任何种类的结构化数据(JSONYAMLTOML 或许多其他文本格式和二进制格式中的任何一种)中解析 GcdParameters 的值。serde crate 还提供了一个 Serialize 属性,该属性会生成代码来执行相反的操作,获取 Rust 值并以结构化的格式序列化它们。

有了这个定义,就可以很容易地编写处理函数了:

fn post_gcd(form: web::Form<GcdParameters>) -> HttpResponse {
    if form.n == 0 || form.m == 0 {
        return HttpResponse::BadRequest()
            .content_type("text/html")
            .body("Computing the GCD with zero is boring.");
    }
    let response =
        format!("The greatest common divisor of the numbers {} and {} \
                 is <b>{}</b>\n",
                form.n, form.m, gcd(form.n, form.m));
    HttpResponse::Ok()
        .content_type("text/html")
        .body(response)
}

对于用作 Actix 请求处理程序的函数,其参数必须全都是 Actix 知道该如何从 HTTP 请求中提取出来的类型。post_gcd 函数接受一个参数 form,其类型为 web::Form<GcdParameters>。当且仅当 T 可以从 HTML 表单提交过来的数据反序列化时,Actix 才能知道该如何从 HTTP 请求中提取任意类型为 web::Form<T> 的值。由于我们已经将 #[derive(Deserialize)] 属性放在了 GcdParameters 类型定义上,Actix 可以从表单数据中反序列化它,因此请求处理程序可以要求以 web::Form<GcdParameters> 值作为参数。这些类型和函数之间的关系都是在编译期指定的。如果使用了 Actix 不知道该如何处理的参数类型来编写处理函数,那么 Rust 编译器会直接向你报错。

来看看 post_gcd 内部,如果任何一个参数为 0,则该函数会先行返回 HTTP 400 BAD REQUEST 错误,因为如果它们为 0,我们的 gcd 函数将崩溃。同时,post_gcd 会使用 format! 宏来为此请求构造出响应体。format!println! 很像,但它不会将文本写入标准输出,而是会将其作为字符串返回。一旦获得响应文本,post_gcd 就会将其包装在 HTTP 200 OK 响应中,设置其内容类型,并将它返回给请求者。

还必须将 post_gcd 注册为表单处理程序。为此,可以将 main 函数替换成以下这个版本:

fn main() {
    let server = HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(get_index))
            .route("/gcd", web::post().to(post_gcd))
    });
    println!("Serving on http://localhost:3000...");
    server
        .bind("127.0.0.1:3000").expect("error binding server to address")
        .run().expect("error running server");
}

这里唯一的变化是添加了另一个 route 调用,确立 web::post().to(post_gcd) 作为路径 "/gcd" 的处理程序。

最后剩下的部分是我们之前编写的 gcd 函数,它位于 actix-gcd/src/main.rs 文件中。有了它,你就可以中断运行中的服务器,重新构建并启动程序了:

$ cargo run
   Compiling actix-gcd v0.1.0 (/home/jimb/rust/actix-gcd)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/actix-gcd`
Serving on http://localhost:3000...

这一次,访问 http://localhost:3000,输入一些数值,然后单击“Compute GCD”按钮,应该会看到一些实质性结果,如图 2-2 所示。

image.png

图 2-2:展示计算最大公约数结果的网页

注意!

这里的代码经本地测试无法正常运行,Web服务器可参考 Actix 官方文档

Getting Started | Actix

2.6 并发

Rust 的一大优势是它对并发编程的支持。Rust 中用来确保内存安全的那些规则也同样可以让线程在共享内存的时候避免数据竞争。

  • 如果使用互斥锁来协调对共享数据结构进行更改的多个线程,那么 Rust 会确保只有持有锁才能访问这些数据,并会在完工后自动释放锁。而在 C 和 C++ 中,互斥锁和它所保护的数据之间的联系只能体现在注释中。
  • 如果想在多个线程之间共享只读数据,那么 Rust 能确保你不会意外修改数据。而在 C 和 C++ 中,虽然类型系统也可以帮你解决这个问题,但很容易出错。
  • 如果将数据结构的所有权从一个线程转移给另一个线程,那么 Rust 能确保你真的放弃了对它的所有访问权限。而在 C 和 C++ 中,要由你来检查发送线程上的任何代码是否会再次接触数据。如果你弄错了,那么后果可能取决于处理器缓存中正在发生什么,以及你最近对内存进行过多少次写入。

曼德博集(一组分形几何图形,包括著名的海龟图等),这是一种对复数反复运行某个简单函数而生成的分形图。人们通常把“绘制曼德博集”称为易并行算法,因为其线程之间的通信模式非常简单

笛卡尔坐标系

笛卡尔坐标系(Cartesian coordinates)就是直角坐标系和斜坐标系的统称。

相交于原点的两条数轴,构成了平面仿射坐标系。如两条数轴上的度量单位相等,则称此仿射坐标系为笛卡尔坐标系。两条数轴互相垂直的笛卡尔坐标系,称为笛卡尔直角坐标系,否则称为笛卡尔斜角坐标系。

曼德博集合

曼德博集合(Mandelbrot set,或译为曼德布洛特复数集合)是一种在复平面上组成分形的点的集合,以数学家本华·曼德博的名字命名。曼德博集合与朱利亚集合有些相似的地方,例如使用相同的复二次多项式来进行迭代。

将曼德博集合无限放大都能够有精妙的细节在内,而这瑰丽的图案仅仅由一个简单的公式生成。因此有人认为曼德博集合是“人类有史以来做出的最奇异、最瑰丽的几何图形”,曾被称为“上帝的指纹”。

通过阅读 《第二章 Rust 导览》对 Rust 有了更多的了解,也看到了 Rust 强大而丰富的能力,后面将开启 Rust 基本数据类型,开始边看加重点代码实战练习了

目录
相关文章
|
2月前
|
Rust 安全 开发工具
探索 Rust:系统编程语言的新纪元
【10月更文挑战第17天】介绍了 Rust 语言的核心特性,如内存安全、强大的并发编程模型和接近 C/C++ 的性能。文章还涵盖了 Rust 的开发工具,如 Cargo 和 Rustup,以及其在业界的应用,包括微软 Azure 和 Firefox 浏览器。Rust 正在成为系统编程领域的新星,为开发者带来高性能和安全性。
|
6月前
|
Rust 自然语言处理 安全
【Rust日报】用 Rust 从头实现一个 C 编译器
【6月更文挑战第9天】使用 Rust 实现 C 编译器的挑战性项目,利用 Rust 的内存安全和高性能优势。涉及词法分析、语法分析等步骤,示例代码展示简单的词法分析过程。实际实现需处理更多复杂环节,如抽象语法树构建和代码生成。此项目能深化对编译器原理的理解,提升 Rust 技能,并有望推动更多高质量 Rust 编译器的诞生。
124 7
|
2月前
|
Rust 安全 Linux
Rust-01 Hello Rust 10分钟上手编写第一个Rust程序 背景介绍 发展历史 环境配置 升级打怪的必经之路
Rust-01 Hello Rust 10分钟上手编写第一个Rust程序 背景介绍 发展历史 环境配置 升级打怪的必经之路
44 0
Rust-01 Hello Rust 10分钟上手编写第一个Rust程序 背景介绍 发展历史 环境配置 升级打怪的必经之路
|
3月前
|
Rust 安全 Java
探索 Rust:系统编程语言的新纪元
Rust 是一种由 Mozilla 研究院开发的开源系统编程语言,以其内存安全、高性能和现代并发工具著称,已连续多年被评为 Stack Overflow 最受喜爱的编程语言。Rust 通过所有权系统和借用检查等机制确保内存安全,并具备无垃圾回收、强大类型系统及高效并发编程特性。它广泛应用于系统级应用、WebAssembly、区块链技术和游戏开发等领域。Rust 提供丰富的学习资源和开发工具,包括官方文档、书籍、Cargo 包管理器和活跃社区支持,正逐渐成为系统编程领域的新力量。
|
3月前
|
Rust 安全 前端开发
30天拿下Rust之图形编程
30天拿下Rust之图形编程
41 0
|
6月前
|
存储 移动开发 Rust
【Rust学习】02_猜谜游戏
让我们一起动手完成一个项目,来快速上手 Rust!本章将介绍 Rust 中一些常用概念,并向您展示如何在实际项目中运用它们。您将会学到 let、match、方法、关联函数、引用外部 crate 等知识!后续章节会深入探讨这些概念的细节。
38 0
|
7月前
|
Rust 安全
Rust采用率不断提高,2022年度Rust调查报告出炉!
Rust采用率不断提高,2022年度Rust调查报告出炉!
|
存储 人工智能 Rust
Rust 正式发布八周年纪念日 2023.5.15
Rust 正式发布八周年纪念日 2023.5.15
214 0
|
Rust IDE 前端开发
「Rust语言」最全的Rust初学者的完全免费资源(下)
「Rust语言」最全的Rust初学者的完全免费资源