I/O

输入最基本的功能是读(Read),输出最基本的功能是写(Write)实现了 Read 接口的叫 reader,而实现了 Write 的叫 writer。Rust里面的 Trait可以带默认实现,比如用户定义的 reader 只需要实现 read 一个方法就可以调用 Read trait 里面的任意其它方法,而 writer 也只需要实现 writeflush 两个方法。

Read 由于每调用一次 read 方法都会调用一次系统API与内核交互,效率比较低,如果给 reader 增加一个 buffer,这就是所谓的 BufRead Trait。一个普通的 reader 通过 io::BufReader::new(reader) 或者 io::BufReader::with_capacity(bufSize, reader) 就可以得到一个 BufReader 。BufReader 比较常用的两个方法是按行读: read_line(&mut self, buf: &mut String) -> Result<usize>lines(&mut self) -> Lines<Self>,需要注意的是后者返回的是一个迭代器。

标准输入与输出

通用的标准输入与输出定义在 std::io 模块里,调用 std::io::stdin()std::io::stdout() 两个函数分别会得到输入句柄和输出句柄,这两个句柄默认会通过互斥锁同步,也就是说不让多个进程同时读或写标准输入输出,我们还可以显式地在句柄上调用 .lock()。输入输出句柄实现了前面讲的读写 Trait,所以是 reader/writer,就可以调接口来读写标准输入与输出了.

  1. use std::io;
  2. fn read_from_stdin(buf: &mut String) -> io::Result<()> {
  3. io::stdin().read_line(buf)?;
  4. Ok(())
  5. }
  1. use std::io;
  2. fn write_to_stdout(buf: &[u8]) -> io::Result<()> {
  3. io::stdout().write(&buf)?;
  4. Ok(())
  5. }

上面的例子都是返回了 io::Result<()> 类型,这是 IO 操作通用的写法, IO 操作是程序与外界打交道,都是有可能失败的,用 io::Result<T> 把结果包起来,io::Result<T> 只是标准 Result<T,E>E 固定为 io::Error 后类型的别名,而作为有副作用的操作我们一般是不用关心其返回值的,因为执行这类函数其真正的意义都体现在副作用上面了,所以返回值只是用来表示是否成功执行,而本身 Result 类型本身已经可以表示执行状态了,里面的 T 是什么则无关紧要,既然 T 没什么意义,那我们就选没什么意义的 unit 类型好了,所以 IO 操作基本上都是使用 io::Result<()>

由于 IO 操作可能会失败所以一般都是和 ? 一起使用的,但是 ? 在遇到错误时会把错误 return 出去的,所以需要保证包含 ? 语句的函数其返回类型是 io::Result<T>,还有 main 函数是没有返回值的,而 ? 会返回 io::Result<T>,所以不能直接把 ?main 函数里。需要主要的是 Rust 里面没有办法从键盘获取一个数字类型的值。实际上像 C 这样的语言也不是直接获取了数字类型,它只不过是做了一种转换。如果想要从键盘获取一个数字类型:

  1. fn main() {
  2. let mut input = String::new();
  3. std::io::stdin()
  4. .read_line(&mut input)
  5. .expect("Failed to read line");
  6. // 这里等效的写法是:
  7. // let num: i32 = input.trim().parse().unwrap();
  8. let num = input.trim().parse::<i32>().unwrap();
  9. println!("您输入的数字是:{}", num);
  10. }

如果有很多地方都需要输入数字可以自行编写一个 numin 宏:

  1. macro_rules! numin {
  2. () =>{
  3. {
  4. let mut input = String::new();
  5. std::io::stdin()
  6. .read_line(&mut input)
  7. .expect("Failed to read line");
  8. input.trim().parse().unwrap()
  9. }
  10. };
  11. }

于是上面的程序可以被改写成:

  1. fn main() {
  2. let num: i32 = numin!();
  3. println!("您输入的数字是:{}", num);
  4. }

如果用户输入的不是数字,那么就会导致错误。这一点和 C 里面是非常相似的。当然您可以把程序写得再复杂一点儿来保证用户输入的一定是数字。如何从命令行接受输入参数,在 Rust 里面被归为环境变量,可以通过 std::env::args() 获取,这个函数返回一个 Args 迭代器,其中第一个就是程序名,后面的都是输入给程序的命令行参数。

  1. use std::env;
  2. fn main() {
  3. let args = env::args();
  4. for arg in args {
  5. println!("{}", arg);
  6. }
  7. }

将上面的程序存为 args.rs 然后编译执行,结果如下

  1. $ rustc args.rs
  2. $ ./args a b c
  3. ./args
  4. a
  5. b
  6. c

print! 宏

标准输出的行缓冲。它一个表现就是 print! 宏。在 print! 宏后面接上一个输入就会发现这种按行缓冲的机制。

  1. fn main() {
  2. print!("hello!\ninput:");
  3. let mut input = String::new();
  4. std::io::stdin()
  5. .read_line(&mut input)
  6. .expect("Failed to read line");
  7. println!("line:{}",input);
  8. }

您可以编译并运行这段程序试一试,您会发现我们并没有得到预期的(下划线代表光标的位置):

  1. hello!
  2. input:_

而是得到了:

  1. hello!
  2. _

这就是由于标准输出中的这种行缓冲机制,在遇到换行符之前,输出的内容并不会隐式的刷新,这就导致 print! 宏和 println! 宏实际上并不完全相同。在标准库中 print! 宏是这样的:

  1. macro_rules! print {
  2. ($($arg:tt)*) => { ... };
  3. }

由此,我们可以对它进行改进,使它和 println! 宏被自动刷新,不过这种刷新是一种显式的刷新。

  1. use std::io::{self, Write};
  2. macro_rules! printf {
  3. ($($arg:tt)*) =>{
  4. print!($($arg)*);
  5. io::stdout().flush().unwrap();
  6. }
  7. }

此外,当您需要刷新还没有遇到换行符的一行内容的时候您都可以使用 io::stdout().flush().unwrap(); 进行刷新,需要注意的是要先 use std::io::{self, Write};