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.exenot 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
的函数,它有两个参数(n
和 m
),每个参数都是 u64
类型,即一个无符号的 64 位整数。->
标记后面紧跟着返回类型,表示此函数返回一个 u64
值。4 空格缩进是 Rust 的标准风格。
Rust 的“机器整数类型名”揭示了它们的大小和符号:i32
是一个带符号的 32 位整数,u8
是一个无符号的 8 位整数(“字节”值),以此类推。isize
类型和 usize
类型保存着恰好等于“指针大小”的有符号整数和无符号整数,在 32 位平台上是 32 位长,在 64 位平台上则是 64 位长。Rust 还有 f32
和 f64
这两种浮点类型,它们分别是 IEEE 单精度浮点类型和 IEEE 双精度浮点类型,就像 C 和 C++ 中的 float
和 double
。
默认情况下,一经初始化,变量的值就不能再改变了,但是在参数 n
和 m
之前放置 mut
(发音为 /mjuːt/,是 mutable 的缩写)关键字将会允许我们在函数体中赋值给它们。实际上,大多数变量是不需要被赋值的,而对于那些确实需要被赋值的变量,mut
关键字相当于用一个醒目的提示来帮我们阅读代码。
let
语句会声明一个局部变量,比如本函数中的 t
。只要 Rust 能从变量的使用方式中推断出 t
的类型,就不需要标注其类型。在此函数中,通过匹配 m
和 n
,可以推断出唯一适用于 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 如何与其他语言编写的代码互动,等等。后面还会介绍更多的属性示例。
将 gcd
和 test_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):所有错误都使用 Result
或 panic
进行处理,详见第 7 章。
我们用 Result
的 expect
方法来检查本次解析是否成功。如果结果是 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
完全返回,程序就算成功完成。只有显式地调用像 expect
或 std::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 所示,该网站会提示用户输入两个数值并计算它们的最大公约数。
图 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
的新类型,它有两个字段(n
和 m
),每个字段都是一个 u64
,这是我们的 gcd
函数想要的参数类型。
此 struct
定义上面的注解是一个属性,就像之前用来标记测试函数的 #[test]
属性一样。在类型定义之上放置一个 #[derive(Deserialize)]
属性会要求 serde
crate 在程序编译时检查此类型并自动生成代码,以便从 HTML 表单 POST 提交过来的格式化数据中解析出此类型的值。事实上,该属性足以让你从几乎任何种类的结构化数据(JSON
、YAML
、TOML
或许多其他文本格式和二进制格式中的任何一种)中解析 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 所示。
图 2-2:展示计算最大公约数结果的网页
注意!
这里的代码经本地测试无法正常运行,Web服务器可参考
Actix
官方文档
2.6 并发
Rust 的一大优势是它对并发编程的支持。Rust 中用来确保内存安全的那些规则也同样可以让线程在共享内存的时候避免数据竞争。
- 如果使用互斥锁来协调对共享数据结构进行更改的多个线程,那么 Rust 会确保只有持有锁才能访问这些数据,并会在完工后自动释放锁。而在 C 和 C++ 中,互斥锁和它所保护的数据之间的联系只能体现在注释中。
- 如果想在多个线程之间共享只读数据,那么 Rust 能确保你不会意外修改数据。而在 C 和 C++ 中,虽然类型系统也可以帮你解决这个问题,但很容易出错。
- 如果将数据结构的所有权从一个线程转移给另一个线程,那么 Rust 能确保你真的放弃了对它的所有访问权限。而在 C 和 C++ 中,要由你来检查发送线程上的任何代码是否会再次接触数据。如果你弄错了,那么后果可能取决于处理器缓存中正在发生什么,以及你最近对内存进行过多少次写入。
曼德博集(一组分形几何图形,包括著名的海龟图等),这是一种对复数反复运行某个简单函数而生成的分形图。人们通常把“绘制曼德博集”称为易并行算法,因为其线程之间的通信模式非常简单
笛卡尔坐标系
笛卡尔坐标系(Cartesian coordinates)就是直角坐标系和斜坐标系的统称。
相交于原点的两条数轴,构成了平面仿射坐标系。如两条数轴上的度量单位相等,则称此仿射坐标系为笛卡尔坐标系。两条数轴互相垂直的笛卡尔坐标系,称为笛卡尔直角坐标系,否则称为笛卡尔斜角坐标系。
曼德博集合
曼德博集合(Mandelbrot set,或译为曼德布洛特复数集合)是一种在复平面上组成分形的点的集合,以数学家本华·曼德博的名字命名。曼德博集合与朱利亚集合有些相似的地方,例如使用相同的复二次多项式来进行迭代。
将曼德博集合无限放大都能够有精妙的细节在内,而这瑰丽的图案仅仅由一个简单的公式生成。因此有人认为曼德博集合是“人类有史以来做出的最奇异、最瑰丽的几何图形”,曾被称为“上帝的指纹”。
通过阅读 《第二章 Rust 导览》对 Rust 有了更多的了解,也看到了 Rust 强大而丰富的能力,后面将开启 Rust 基本数据类型,开始边看加重点代码实战练习了