Beginng_Rust(译):输入/输出和错误处理(第十七章)

在本章中,您将学习:

•如何从用于启动程序的命令行获取参数

•退出程序时如何将状态代码返回给操作系统

•如何获取和设置过程环境变量

•处理运行时错误的技术和最佳实践

•如何从控制台键盘读取以及如何写入控制台屏幕

•原始类型如何转换为字符串

•如何读取或写入二进制文件

•如何一次读取一行文本

命令行参数

程序输入的最基本形式是通过命令行。

let command_line: std::env::Args = std::env::args();
for argument in command_line {
	println!("[{}]", argument);
}

如果编译这个程序来创建一个名为main的文件,并且这个文件在编写命令行“./main first second”时启动,它将打印:

扫描二维码关注公众号,回复: 3732748 查看本文章
[./main]
[first]
[second]

args标准库函数返回命令行参数的迭代器。 这样的迭代器具有类型Args,并且它生成String值。 生成的第一个值是程序名称,以及用于到达它的路径。 其他是程序参数。

通常会删除任何空白; 要保留空格,您必须将参数括在引号中,这些参数将被删除。 如果你启动./main“first argument”“second argument”,它将打印:

[./main]
[ first argument]
[second argument ]

该程序可以缩写为:

for a in std::env::args() {
println!("[{}]", a);
}

流程返回代码

程序输出的最基本形式是返回代码。

std::process::exit(107);

该程序在调用“exit()”功能时立即终止,并返回编号107的启动过程。

如果这个程序是从Unix,Linux或MacOS的控制台启动的,那么如果之后你编写命令“echo $?”,你将在控制台上打印107。 相应的Windows命令是“echo%errorlevel%”。

环境变量

另一种输入/输出形式是通过环境变量。

for var in std::env::vars() {
println!("[{}]=[{}]", var.0, var.1);
}

该程序将为每个环境变量打印一行。 但是,要读取或写入特定的环境变量,此代码更好:

print!("[{:?}]", std::env::var("abcd"));
std::env::set_var("abcd", "This is the value");
print!(" [{:?}]", std::env::var("abcd"));

也许,这将打印:“[Err(NotPresent)] [Ok(“This is the value”)]”。首先,可能,“abcd”环境变量尚未定义,因此“var”函数的调用返回 “结果”值的“Err”变体。特定类型的错误是枚举“NotPresent”。然后,使用“set_var”函数的调用为当前进程设置这样的环境变量。所以, 在下一次尝试获取它时,它被找到,并且其字符串值在“Ok”变体内返回。
这个类似的程序是这样的:

print!("{}", if std::env::var("abcd").is_ok() {
	"Already defined"
} else {
	"Undefined"
});
std::env::set_var("abcd", "This is the value");
print!(", {}.", match std::env::var("abcd") {
	Ok(value) => value,
	Err(err) => format!("Still undefined: {}", err),
});

It will print: “Undefined, This is the value.”.

从控制台读取

对于面向命令行的程序,获取输入的典型方法是从键盘读取一行,直到用户按Enter键。 可以将这样的输入重定向为从文件或从另一个进程的输出读取。

let mut line = String::new();
println!("{:?}", std::io::stdin().read_line(&mut line));
println!("[{}]", line);

当该程序启动时,它会等待您从键盘输入,直到您按某些键然后按Enter键。 例如,如果您键入“Hello”,然后按Enter键,它将打印:

Ok(6)
[Hello
]

“stdin”函数返回当前进程的标准输入流的句柄。 在该句柄上,可以应用“read_line”函数。 它等待标准输入流中的行尾或文件结束字符,然后尝试读取输入缓冲区中存在的所有字符。 该读取可能会失败,因为另一个线程正在同时读取它。

如果读取成功,则读取的字符放在字符串对象中,分配给“line”变量,通过引用可变对象作为参数接收,“read_line”函数返回“Ok”结果对象,其数据 是读取的字节数。 请注意,这个数字是“6”,因为除了字符串“Hello”的五个字节之外,还有行尾控制字符。 实际上,当打印“line”变量时,终止闭合括号被打印在一个单独的行中,因为结束了行字符也被打印出来。

如果“read_line”函数无法从标准输入流中读取字符,则它返回“Err”结果对象,并且不会更改“line”变量的值。

让我们看看当从标准输入流中读取多行时会发生什么。

let mut text = format!("First: ");
let inp = std::io::stdin();
inp.read_line(&mut text).unwrap();
text.push_str("Second: ");
inp.read_line(&mut text).unwrap();
println!("{}: {} bytes", text, text.len());

如果你运行这个程序,你键入“e耔,然后按Enter键,然后键入“Hello”,然后再次按Enter键,它将打印:

First: eè€
Second: Hello
: 28 bytes

如果您的键盘不允许您键入这些字符,请尝试键入任何非ASCII字符。

首先,请注意最后一行打印的字符串跨越三行,因为它包含两个行尾字符。此外,它包含7字节ASCII字符串“First:”和8字节ASCII字符串“Second:”。 “Hello”也是一个ASCII字符串,它包含5个字节。正如我们在另一章中看到的那样,“e耔字符串包含6个字节,因此我们有7 + 6 + 1 + 8 + 5 + 1 = 28个字节。

其次,让我们看看如何构建“text”变量的内容。请注意,“read_line”函数将键入的行附加到其参数指定的对象,而不是覆盖它。 “text”变量初始化为包含“First:”。然后,在第三行中,将第一个键入的行附加到那些内容。然后,在第四行中,附加文字字符串“Second:”。最后,在第五行中,附加第二个打印的行。

第三,注意当“read_line”函数读取输入缓冲区时,它会清除它,因为当第二次调用该函数时,不再读取原始缓冲区内容。

第四,注意每次调用“read_line”后,都会调用“unwrap”,但忽略其返回值。 这种调用可以省略。

let mut text = format!("First: ");
let inp = std::io::stdin();
inp.read_line(&mut text);
text.push_str("Second: ");
inp.read_line(&mut text);
println!("{}: {} bytes", text, text.len());

但是,当编译该程序时,编译器会为“read_line”的调用发出警告“未使用的std :: result :: Result,必须使用”。 这意味着“read_line”返回“Result”类型的值,该值被忽略或不使用。 Rust认为忽略“Result”类型的返回值是危险的,因为这样的类型可能表示运行时错误,因此程序逻辑不会考虑这种错误。 这在生产代码中很危险,但它也不适用于调试代码,因为它隐藏了您正在寻找的错误。

因此,在调试代码中,始终至少写一个“.unwrap()”子句是合适的。

但在生产代码中,问题并非如此简单。

正确的运行时错误处理

在真实世界的软件中,通常会发生许多函数调用,返回“Result”类型值。让我们称之为“易犯”的这类功能。易错函数通常会返回“Ok”,但在特殊情况下它会返回“Err”

在C ++,Java和其他面向对象语言中,标准错误处理技术基于所谓的“异常”,以及“抛出”“尝试”和“捕获”关键字。在鲁斯特,没有这样的东西;所有错误处理都基于“结果”类型,其功能和“匹配”语句。

假设你正在编写一个函数“f”,为了完成它的任务,它必须调用几个易错函数,“f1”,“f2”,“f3”和“f4”。如果失败则返回错误消息,如果成功则返回结果。如果函数失败,则应通过“f”函数立即返回该错误消息作为其错误消息。如果函数成功,则其结果应作为参数传递给下一个函数。最后一个函数的结果作为“f”函数的结果传递。
一种可能是写这个:

fn f1(x: i32) -> Result<i32, String> {
if x == 1 {
Err(format!("Err. 1"))
} else {
Ok(x)
}
}

fn f1(x: i32) -> Result<i32, String> {
if x == 1 {
Err(format!("Err. 1"))
} else {
Ok(x)
}
}

fn f4(x: i32) -> Result<i32, String> {
if x == 4 {
Err(format!("Err. 4"))
} else {
Ok(x)
}
}
fn f(x: i32) -> Result<i32, String> {
match f1(x) {
Ok(result) => {
match f2(result) {
Ok(result) => {
match f3(result) {
Ok(result) => f4(result),
Err(err_msg) => Err(err_msg),
}
}
Err(err_msg) => Err(err_msg),
}
}

Err(err_msg) => Err(err_msg),
}
}
match f(2) {
Ok(y) => println!("{}", y),
Err(e) => println!("Error: {}", e),
}
match f(4) {
Ok(y) => println!("{}", y),
Err(e) => println!("Error: {}", e),
}
match f(5) {
Ok(y) => println!("{}", y),
Err(e) => println!("Error: {}", e),
}

这将打印:

Error: Err. 2
Error: Err. 4
5

很明显,随着调用次数的增加,这种模式变得难以处理,因为在每次调用时,缩进级别都会增加2。
通过使用以下代码替换“f”函数,可以使此代码成为线性的:

fn f(x: i32) -> Result<i32, String> {
let result1 = f1(x);
if result1.is_err() { return result1; }
let result2 = f2(result1.unwrap());
if result2.is_err() { return result2; }
let result3 = f3(result2.unwrap());
if result3.is_err() { return result3; }
f4(result3.unwrap())
}

每个中间结果都存储在一个临时变量中,然后使用“is_err”函数检查这个变量。 如果失败,则退回; 如果成功,“解包”功能用于提取实际结果。

这种模式非常典型,语言中引入了语言功能。 这是“f”函数的等效版本:

fn f(x: i32) -> Result<i32, String> {
f4(f3(f2(f1(x)?)?)?)
}

问号是一个特殊的宏,当应用于像“e?”这样的表达式时,如果“e”是泛型类型“Result <T,E>”,它将扩展为表达式“匹配e {Some (v)=> v,_ => return e}“;相反,如果“e”属于通用类型“Option ”,则将其扩展为表达式“match e {Ok(v)=> v,_ => return e}”。换句话说,这样的宏检查它的参数是“Some”还是“Ok”,并且在这种情况下解包它,或以其他方式将其作为包含函数的返回值返回。

它只能应用于“Result <T,E>”或“Option ”类型的表达式,当然,如果只能在具有正确返回值类型的函数内使用。如果封闭函数返回值类型为“Result <T1,E>”,则问号宏只能应用于“Result <T2,E>”类型的表达式,其中“T2”可以与“T1”不同,但“E”必须相同;相反,如果封闭函数返回值类型为“Option ”,则问号宏只能应用于“Option ”类型的表达式。

因此,构建强大错误处理的正确模式如下。 包含对易错函数的调用的每个函数都应该是一个错误的函数,或者应该在“匹配”语句或类似的处理中处理“Result”值。 在第一种情况下,每次调用易错函数时都应该跟一个问号来传播错误条件。 “main”函数(或辅助线程的启动函数)不能是一个错误的函数,因此,在调用链的某个点上,“Result”值应该有一个“匹配”语句。

写入控制台

我们已经在几乎所有编写的程序片段中写入了控制台,但我们总是使用“print”或“println”宏,它们是使用标准库函数实现的。 但是,您也可以直接使用库函数将一些文本打印到控制台。

use std::io::Write;
//ILLEGAL: std::io::stdout().write("Hi").unwrap();
//ILLEGAL: std::io::stdout().write(String::from("Hi")).unwrap();
std::io::stdout().write("Hello ".as_bytes()).unwrap();
std::io::stdout().write(String::from("world").as_bytes()).unwrap();

这将打印:“Hello world”。

“stdout”标准库函数返回当前进程的标准输出流的句柄。 在该句柄上,可以应用“写入”功能。

但是,“写入”功能不能直接打印静态或动态字符串,当然也不能打印数字或一般复合对象。

“write”函数获取[u8]“type的参数,它是对一片字节的引用。这些字节作为UTF-8字符串打印到控制台。所以,如果你想打印一个对象是 不是UTF-8格式的一小部分字节,首先你必须把它翻译成这样的东西。

要将静态字符串和动态字符串都转换为对字节片的引用,可以使用“as_bytes”函数。 此函数只返回字符串第一个字节的地址,以及字符串对象使用的字节数。 这些值已经包含在字符串对象的标题中,因此该函数非常有效。

最后,请注意“write”函数返回“Result”类型值,也就是说,它是一个错误的函数。 如果你确定它不会失败,你最好在其返回值上调用“unwrap”函数。

将值转换为字符串

如果要打印另一种值的文本表示,可以尝试使用为所有基本类型定义的“to_string”函数。

let int_str: String = 45.to_string();
let float_str: String = 4.5.to_string();
let bool_str: String = true.to_string();
print!("{} {} {}", int_str, float_str, bool_str);

这将打印:“45 4.5 true”。

“to_string”函数分配一个String对象,其标头位于堆栈中,其内容位于堆中。 因此,它不是非常有效。

文件输入/输出

除了读取和写入控制台之外,在Rust中读取和写入二进制文件和文本顺序文件也相当容易。

use std::io::Write;
let mut file = std::fs::File::create("data.txt").unwrap();
file.write_all("eè€".as_bytes()).unwrap();

第二行调用“create”函数在文件系统的当前文件夹中创建名为“data.txt”的文件。 此函数是错误的,如果它成功创建文件,它将返回刚刚创建的文件的文件句柄。

最后一行调用“write_all”函数在新创建的文件中写入一些字节。 保存的字节是表示字符串“e耔的六个字节。

假设在当前目录中有一个名为“data.txt”的文本文件,您刚刚通过运行上一个程序创建该文件,则可以通过运行以下程序来读取该文件。

use std::io::Read;
let mut file = std::fs::File::open("data.txt").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
print!("{}", contents);

该程序将打印:“e耔。

第二行调用“open”函数打开当前文件夹中名为“data.txt”的现有文件。 如果文件不存在,或者由于某种原因无法访问,则此函数将失败。 如果成功,则将此文件的文件句柄分配给“file”变量。

第四行调用“file”句柄上的“read_to_string”函数,将该文件的所有内容读入字符串变量,通过引用传递给可变对象。

最后一行向控制台输出刚刚从文件中读取的内容。

所以现在你可以将文件复制到另一个文件中。 但是如果文件很大,则在写入之前不可能将它全部加载到字符串中。 需要一次读写一部分。 但是,读取和写入小部分是低效的。

这是一个复制文件的相当有效的程序。

use std::io::Read;
use std::io::Write;
let mut command_line: std::env::Args = std::env::args();
command_line.next().unwrap();
let source = command_line.next().unwrap();
let destination = command_line.next().unwrap();
let mut file_in = std::fs::File::open(source).unwrap();
let mut file_out = std::fs::File::create(destination).unwrap();
let mut buffer = [0u8; 4096];
loop {
let nbytes = file_in.read(&mut buffer).unwrap();
file_out.write(&buffer[..nbytes]).unwrap();
if nbytes < buffer.len() { break; }
}

必须通过两个命令行参数启动此程序。 第一个是源文件的路径,第二个是目标文件的路径。

从第三个到第六个的行为“source”变量分配第一个参数的内容,向“destination”变量分配第二个参数的内容。

接下来的两行打开两个文件。 首先打开源文件,并将新句柄分配给“file_in”变量。 然后创建(或截断,如果已存在)目标文件,并将新句柄分配给“file_out”变量。

然后在堆栈中分配4096字节的缓冲区。

最后,循环重复从源文件中读取4096字节的块并将其写入输出文件。 读取的字节数自动与缓冲区的长度一样多。 但是如果文件的剩余部分不够长,则读取的字节小于4096,甚至为零。

因此,读取的字节数被放入“nbytes”变量中。

对于大于4096字节的文件,在第一次迭代时,读取的字节数将为4096,因此将需要一些其他迭代。 对于较小的文件,一次迭代就足够了。

在任何情况下,缓冲区都写入文件,最多读取的字节数。 因此,缓冲区的一部分从开始到读取字节的数量。

然后,如果读取的字节数小于缓冲区的长度,则终止循环,因为已到达输入文件的末尾。 否则循环继续其他迭代。

请注意,无需显式关闭文件。 一旦文件处理退出其作用域,文件就会自动关闭,保存并释放所有内部临时缓冲区。

处理文本文件

我们看到了如何顺序读取或写入任意数据的文件。

但是当文件包含原始文本(如程序源文件)时,一次处理一行就更方便了。

例如,如果我们想要计算文本文件中有多少行,以及有多少行是空的或只包含空格,则可以编写以下程序:

let mut command_line = std::env::args();
command_line.next();
let pathname = command_line.next().unwrap();
let counts = count_lines(&pathname).unwrap();
println!("file: {}", pathname);
println!("n. of lines: {}", counts.0);
println!("n. of empty lines: {}", counts.1);
fn count_lines(pathname: &str)
-> Result<(u32, u32), std::io::Error> {
use std::io::BufRead;
let f = std::fs::File::open(pathname)?;
let f = std::io::BufReader::new(f);

let mut n_lines = 0;
let mut n_empty_lines = 0;
for line in f.lines() {
n_lines += 1;
if line?.trim().len() == 0 {
n_empty_lines += 1;
}
}
Ok((n_lines, n_empty_lines))
}

如果这个程序包含在通常的“main”函数中,保存在名为“countlines.rs”的文件中,然后编译,并使用参数“countlines.rs”运行,它将打印:

file: countlines.rs
n. of lines: 26
n. of empty lines: 2

在第一行中,“args”的调用获取命令行迭代器并将其存储在“command_line”变量中。

在第二行中,将丢弃第0个命令行参数。

在第三行中,第一个命令行参数被使用并分配给“pathname”变量。如果没有这样的争论,该计划会引起恐慌。

在第四行中,调用稍后定义的“count_lines”函数,向其传递对要读取的文件的路径名的引用。这是一个错误的功能。如果成功,则返回两个值的元组:读取文件中计算的总行数,以及这些行为空或仅包含空格的行数。该对被分配给“计数”变量。

第五,第六和第七行是打印语句。

从第九行开始,有“count_lines”函数的声明。它获取一个字符串切片作为参数,并返回一个“结果”,如果成功,则返回一对“u32”数字,如果失败则是标准I / O错误。

调用“open”函数以获取由作为参数接收的路径名指示的文件的句柄。后面的问号意味着如果open函数失败,“count_lines”函数会立即返回“open”函数返回的相同错误代码。

默认情况下,对文件执行的操作不会缓冲。 如果您不需要缓冲,或者您更喜欢应用自己的缓冲,那么这是最佳选择。 但是如果您更喜欢缓冲流,则可以从“原始”文件句柄创建“BufReader”对象。 由于文本行通常比最佳I / O缓冲区大小短得多,因此在读取文本文件时使用缓冲输入流更有效。 在创建“BufReader”对象之后,不再需要显式使用“File”对象,因此可以将新创建的对象分配给另一个名为“f”的变量,以便它将遮蔽预先存在的变量。

然后,声明并初始化两个计数器“n_lines”和“n_empty_lines”。

然后,在文件内容上有循环。 “BufReader”类型提供“lines”函数,该函数返回文件中包含的行的迭代器。请注意,Rust迭代器是惰性的;也就是说,永远不会有包含所有行的内存结构,但每次请求迭代器一行时,它会向文件缓冲读取器请求一行,然后提供获得的行。因此,在每次迭代时,“for”循环将下一行放入“line”变量并执行循环块。

但任何文件读取都可能失败,因此“line”不是简单的字符串;它的类型是“Result <String,std :: io :: Error>”。因此,在使用它时,“line”后面跟一个问号,以获取其字符串值或返回I / O错误。

在循环体中,“n_lines”计数器在任何一行增加1,而“n_empty_lines”仅在该行通过调用“trim”从其中删除任何前导或尾随空白后增加1时为零长度。

最后一个语句返回一个成功的值:“Ok”。这种值的数据是两个计数器。

猜你喜欢

转载自blog.csdn.net/m0_37696990/article/details/82955707