重构以提高模块化和错误处理
为了改进我们的程序,我们将修复与
程序的结构以及它如何处理潜在错误。首先,我们的main
函数现在执行两项任务:解析参数和读取文件。作为我们的
程序增长,则单独任务的数量main
function 句柄将
增加。随着职能部门的责任增加,它变得更加困难
reason about,更难测试,更难在不破坏其
部件。最好将功能分开,以便每个功能都负责
一个任务。
这个问题也与第二个问题有关:虽然query
和file_path
是我们程序的配置变量,像contents
已使用
以执行程序的逻辑。更长的main
become 时,变量越多
我们需要纳入范围;我们在 SCOPE 中的变量越多,就越难
这将是跟踪每个的目的。最好将
配置变量合并到一个结构中,以明确其用途。
第三个问题是我们使用了expect
在以下情况下打印错误消息
读取文件失败,但只打印错误消息Should have been able to read the file
.读取文件可能以多种方式失败:对于
例如,文件可能丢失,或者我们可能没有打开它的权限。
现在,无论情况如何,我们都会为
everything,这不会给用户任何信息!
第四,我们使用expect
来处理错误,并且如果用户运行我们的程序
如果没有指定足够的参数,他们将得到一个index out of bounds
错误
这并没有清楚地解释问题。如果所有
错误处理代码在一个地方,所以未来的维护者只有一个地方
来查阅代码(如果需要更改错误处理逻辑)。拥有所有
错误处理代码也将确保我们打印消息
这对我们的最终用户来说将是有意义的。
让我们通过重构我们的项目来解决这四个问题。
二进制项目的关注点分离
将多个任务的责任分配给
这main
function 是许多二进制项目通用的。因此,Rust
community 已经制定了用于拆分 a 的单独关注点的指导方针
binary program 时main
开始变大。此过程包括以下内容
步骤:
- 将程序拆分为 main.rs 文件和 lib.rs 文件,并将 程序的 logic 进行 lib.rs。
- 只要您的命令行解析逻辑很小,它就可以保持 main.rs。
- 当命令行解析逻辑开始变得复杂时,将其提取 从 main.rs 并将其移动到 lib.rs。
保留在main
函数
应限于以下内容:
- 使用参数值调用命令行解析逻辑
- 设置任何其他配置
- 调用
run
函数 lib.rs - 在以下情况下处理错误
run
返回错误
此模式是关于分离关注点的:main.rs 处理运行
程序和 lib.rs 处理手头任务的所有逻辑。因为您
无法测试main
函数,此结构允许您测试所有
将程序的 logic 移动到 lib.rs 中的 Functions。代码
仍然在 main.rs 中,则足够小,以便通过读取来验证其正确性
它。让我们按照这个过程重新设计我们的程序。
提取 Argument Parser
我们将用于解析参数的功能提取到一个函数中,该函数main
将调用以准备将命令行解析逻辑移动到 src/lib.rs。示例 12-5 显示了main
这会调用新的
功能parse_config
,我们现在将在 src/main.rs 中定义它。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let (query, file_path) = parse_config(&args);
// --snip--
println!("Searching for {query}");
println!("In file {file_path}");
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let file_path = &args[2];
(query, file_path)
}
parse_config
函数从main
我们仍然将命令行参数收集到一个 vector 中,但不是
将索引 1 处的参数值分配给变量query
和
变量索引 2 处的 argument valuefile_path
在main
函数中,我们将整个 vector 传递给parse_config
功能。这parse_config
function 然后保存确定哪个参数的 logic
放入哪个变量中,并将值传递回main
.我们仍在创造
这query
和file_path
变量main
但main
不再具有
确定命令行参数和变量
通信。
对于我们的小程序来说,这种返工似乎有点矫枉过正,但我们正在重构 以小的、渐进的步骤进行。进行此更改后,再次运行程序以 验证参数解析是否仍然有效。检查您的进度是件好事 通常,在问题发生时帮助确定问题的原因。
对配置值进行分组
我们可以再迈出一小步来改进parse_config
功能进一步。
目前,我们返回了一个 Tuples,但随后我们立即中断了它
tuple 转换为单独的部分。这是一个迹象,也许我们没有
这是正确的抽象。
另一个表明有改进空间的指标是config
部分
之parse_config
,这意味着我们返回的两个值是相关的,并且
都是 Configuration 值的一部分。我们目前没有传达此内容
在数据结构中的含义,而不是通过将两个值分组为
一个元组;我们将这两个值放入一个结构体中,并为每个
struct 字段设置有意义的名称。这样做将使未来更容易
此代码的维护者,以了解不同的值与每个值之间的关系
其他以及它们的目的是什么。
示例 12-6 显示了对parse_config
功能。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
// --snip--
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
parse_config
返回Config
结构我们添加了一个名为Config
定义为具有名为query
和file_path
.的签名parse_config
now 表示它返回一个Config
价值。在 bodyparse_config
,我们过去经常返回的地方
string 切片,该String
的值args
,我们现在定义Config
包含 ownedString
值。这args
变量main
是
参数值,并且只让parse_config
函数借用
them,这意味着如果Config
尝试拿
中值的所有权args
.
我们可以通过多种方式来管理String
数据;最容易的,
虽然效率有些低下,但 route 是将clone
method 的值。
这将为Config
instance 来拥有,其中
比存储对字符串数据的引用需要更多的时间和内存。
但是,克隆数据也使我们的代码非常简单,因为我们
不必管理引用的生命周期;在这种情况下,
为了获得简单性而放弃一点性能是一个值得的权衡。
使用clone
许多 Rustacean 倾向于避免使用clone
修复
所有权问题。在第 13 章中,您将学习如何更高效地使用
方法。但就目前而言,复制一些是可以的
strings 继续进行,因为您只会制作这些副本
once,并且您的文件路径和查询字符串非常小。最好有
一个比尝试超优化代码效率低下的工作程序
在你的第一次通过时。随着您对 Rust 的经验越来越丰富,它会
从最有效的解决方案开始更容易,但就目前而言,它是
完全可以接受clone
.
我们更新了main
因此,它将Config
返回者parse_config
转换为名为config
,我们更新了
以前使用单独的query
和file_path
变量,因此它现在使用
的Config
struct 代替。
现在,我们的代码更清楚地传达了这一点query
和file_path
是相关的,并且
他们的目的是配置程序的工作方式。任何
使用这些值知道在config
字段中的实例
以他们的目的命名。
为 创建 ConstructorConfig
到目前为止,我们已经提取了负责解析命令行的 logic
arguments frommain
并将其放置在parse_config
功能。这样做
帮助我们看到query
和file_path
值是相关的,并且
关系应该在我们的代码中传达。然后,我们添加了一个Config
struct 设置为
name 的相关用途query
和file_path
并能够返回
values 的名称作为 struct 字段名称的parse_config
功能。
所以现在parse_config
函数是创建一个Config
实例中,我们可以更改parse_config
从普通函数到函数
叫new
与Config
结构。进行此更改
将使代码更加地道。我们可以在
标准库,例如String
,通过调用String::new
.同样,通过
改变parse_config
转换为new
函数关联Config
,我们将
能够创建Config
通过调用Config::new
.示例 12-7
显示了我们需要进行的更改。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
// --snip--
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
parse_config
到Config::new
我们更新了main
我们打电话的地方parse_config
改为调用Config::new
.我们更改了parse_config
自new
并移动了它
在impl
块,它将new
函数替换为Config
.尝试
再次编译此代码以确保其正常工作。
修复错误处理
现在,我们将着手修复我们的错误处理。回想一下,尝试访问
的args
vector 的 intent 值将导致程序
panic 如果向量包含的项目少于 3 个。尝试运行该程序
没有任何参数;它看起来像这样:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
生产线index out of bounds: the len is 1 but the index is 1
是错误
消息。它不会帮助我们的最终用户了解什么
他们应该这样做。现在让我们解决这个问题。
改进错误消息
在示例 12-8 中,我们在new
函数验证
slice 在访问索引 1 和索引 2 之前足够长。如果切片不是
足够长的时间,程序会 panic 并显示更好的错误消息。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("not enough arguments");
}
// --snip--
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
此代码类似于这Guess::new
我们在 清单 中编写的函数
9-13,我们调用panic!
当value
参数超出有效值的范围。而不是检查
一个值范围,我们检查args
至少是3
函数的其余部分可以在假设 this
条件已得到满足。如果args
的项目少于 3 项,则此条件
将是true
,我们调用panic!
宏立即结束程序。
有了这额外的几行代码new
,让我们运行程序,不要
arguments 再次查看错误现在是什么样子的:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
这个输出更好:我们现在有一个合理的错误消息。但是,我们也
包含我们不想提供给用户的无关信息。也许
我们在示例 9-13 中使用的技术并不是最好在这里使用的技术:对panic!
更适合于编程问题而不是使用问题,如第 9 章所述。相反
我们将使用您在第 9 章中学到的另一种技术—返回一个Result
,这表示成功或错误。
返回Result
而不是调用panic!
我们可以改为返回一个Result
值,它将包含Config
实例
成功案例,并将描述 Error Case 中的问题。我们也是
将函数名称从new
自build
因为许多
程序员期望new
函数永远不会失败。什么时候Config::build
是
通信对象main
,我们可以使用Result
键入以表示存在
问题。然后我们可以改变main
要将Err
变体转换为更多
实际错误对于我们的用户来说,没有周围的文本关于thread 'main'
和RUST_BACKTRACE
对panic!
原因。
示例 12-9 显示了我们需要对
函数Config::build
以及所需函数的主体
要返回Result
.请注意,在我们更新main
如
好吧,我们将在下一个列表中进行。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
Result
从Config::build
我们build
函数返回一个Result
替换为Config
实例
case 和 Error case 中的 String 文本。我们的 error 值将始终为
字符串文本,这些文本具有'static
辈子。
我们对函数的主体进行了两项更改:而不是调用panic!
当用户没有传递足够的参数时,我们现在返回一个Err
value 和
我们已经包装了Config
返回值在Ok
.这些更改使
函数的 JSON JSON 中的调用。
返回Err
值来自Config::build
允许main
函数设置为
处理Result
从build
函数并退出
在错误情况下更干净地处理。
叫Config::build
和处理错误
要处理错误情况并打印用户友好的消息,我们需要更新main
来处理Result
被退回Config::build
,如
示例 12-10.我们还将负责退出命令行
错误代码远离panic!
而是通过以下方式实现它
手。非零退出状态是向进程发出信号的约定
调用我们的程序,该程序退出时出现错误状态。
use std::env;
use std::fs;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
Config
失败在此清单中,我们使用了一种尚未详细介绍的方法:unwrap_or_else
,该Result<T, E>
由 Standard 库。
用unwrap_or_else
允许我们定义一些自定义的、非panic!
错误
处理。如果Result
是一个Ok
value 时,此方法的行为类似
自unwrap
:它返回Ok
正在包装。但是,如果
value 是一个Err
值,此方法调用 Closure 中的代码,即
我们定义并作为参数传递给unwrap_or_else
.
我们将在第 13 章中更详细地介绍闭包。为
现在,您只需要知道unwrap_or_else
将传递
这Err
,在本例中为静态字符串"not enough arguments"
我们在示例 12-9 中添加到参数中的闭包err
那
显示在垂直管道之间。然后,闭包中的代码可以使用err
值。
我们添加了一个新的use
line 带来process
从 Standard 库到
范围。在错误情况下将运行的闭包中的代码只有 2
lines:我们打印err
值,然后调用process::exit
.这process::exit
函数将立即停止程序并返回
作为退出状态代码传递的号码。这类似于panic!
-,但我们不再获取所有
extra 输出。让我们试一试:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
伟大!此输出对我们的用户更友好。
提取逻辑main
现在我们已经完成了配置解析的重构,让我们转向
程序的 logic。正如我们在“Separation of Concerns for Binary” 中所说
Projects“,我们将
提取名为run
,它将保存当前在main
不涉及设置配置或处理的函数
错误。完成后,main
将简洁易用
检查,我们将能够为所有其他 logic编写测试。
示例 12-11 显示了提取的run
功能。目前,我们只是在制作
提取函数的小型增量改进。我们仍然
在 src/main.rs 中定义函数。
use std::env;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) {
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
run
包含其余程序逻辑的函数这run
函数现在包含main
开始
从读取文件。这run
函数采用Config
instance 作为
论点。
从run
功能
将剩余的程序逻辑分离到run
函数,我们可以
改进错误处理,就像我们对Config::build
在示例 12-9 中。
而不是通过调用expect
这run
函数将返回一个Result<T, E>
当出现问题时。这将允许
我们进一步将处理错误的逻辑整合为main
在
用户友好的方式。示例 12-12 显示了我们需要对
签名和正文run
.
use std::env;
use std::fs;
use std::process;
use std::error::Error;
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
run
函数返回Result
我们在此处进行了三项重大更改。首先,我们将
这run
函数设置为Result<(), Box<dyn Error>>
.此函数以前
返回了 Unit 类型 ,并将其保留为()
Ok
箱。
对于 error 类型,我们使用了 trait 对象 Box<dyn Error>
(我们已经
带std::error::Error
into 范围替换为use
声明)。
我们将在第 17 章中介绍 trait 对象。目前,只需
知道Box<dyn Error>
表示该函数将返回一个类型,该
实现Error
trait 的 trait 中,但我们不必指定什么特定的类型
返回值将为。这使我们能够灵活地返回 error 值
在不同的错误情况下可能属于不同的类型。这dyn
关键字简短
用于动态。
其次,我们删除了对expect
赞成?
运算符,因为我们
在第 9 章中讨论过。而不是panic!
在错误时,?
将返回当前函数的错误值
供调用方处理。
第三,run
函数现在返回一个Ok
值。
我们已经宣布了run
函数的成功类型,如签名中所示,
这意味着我们需要将 Unit type 值包装在()
Ok
价值。这Ok(())
语法乍一看可能有点奇怪,但像这样使用
表示我们正在调用()
run
的副作用
只;它不会返回我们需要的值。
当您运行此代码时,它将编译,但将显示警告:
$ cargo run -- the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
--> src/main.rs:19:5
|
19 | run(config);
| ^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
19 | let _ = run(config);
| +++++++
warning: `minigrep` (bin "minigrep") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
Rust 告诉我们,我们的代码忽略了Result
value 和Result
价值
可能表示发生了错误。但是,我们不会检查是否或
没有出现错误,编译器提醒我们,我们可能是故意的
这里有一些错误处理代码!现在让我们纠正这个问题。
处理从run
在main
我们将检查错误并使用类似于我们使用的技术来处理它们
跟Config::build
在示例 12-10 中,但略有不同:
文件名: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
我们使用if let
而不是unwrap_or_else
检查是否run
返回一个Err
值并调用process::exit(1)
如果是的话。这run
功能
不会返回我们想要的值unwrap
以同样的方式Config::build
返回Config
实例。因为run
返回
成功案例中,我们只关心检测一个错误,所以我们不需要()
unwrap_or_else
返回 unwrapped 值,该值仅为 .()
的if let
和unwrap_or_else
函数在
两种情况:我们打印错误并退出。
将代码拆分到库 crate 中
我们minigrep
到目前为止,项目看起来不错!现在我们将拆分 src/main.rs 文件并将一些代码放入 src/lib.rs 文件中。这样,我们
可以测试代码并拥有职责较少的 src/main.rs 文件。
让我们移动所有不在main
从 src/main.rs 到 src/lib.rs 的函数:
- 这
run
功能定义 - 相关的
use
语句 - 的定义
Config
- 这
Config::build
功能定义
src/lib.rs 的内容应该有示例 12-13 中所示的签名 (为简洁起见,我们省略了函数的主体)。请注意,这不会 编译,直到我们修改示例 12-14 中的 src/main.rs。
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub file_path: String,
}
impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
// --snip--
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
// --snip--
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
Config
和run
到 src/lib.rs 中我们自由地使用了pub
关键词: onConfig
、其字段及其build
方法,在run
功能。我们现在有一个 library crate,它有
我们可以测试的公共 API!
现在我们需要将移动到 src/lib.rs 的代码放入 binary crate 的 src/main.rs 中,如示例 12-14 所示。
use std::env;
use std::process;
use minigrep::Config;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = minigrep::run(config) {
// --snip--
println!("Application error: {e}");
process::exit(1);
}
}
minigrep
src/main.rs 中的库 crate我们添加了一个use minigrep::Config
行将Config
type 从
library crate 添加到二进制 crate 的范围内,并在run
功能
替换为我们的 crate 名称。现在所有功能都应该已连接,并且应该
工作。使用 运行 程序cargo run
并确保一切正常。
呼!这需要做很多工作,但我们已经为 前途。现在,处理错误要容易得多,并且我们使代码更加 模块 化。从现在开始,我们几乎所有的工作都将在 src/lib.rs 中完成。
让我们通过做一些可以 使用旧代码很困难,但使用新代码很容易:我们将 编写一些测试!
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准