Rust Read、BufRead、BufReader..
今天写代码的时候有一个需求,我希望在某个代表路径的字符串不为空时,以这个路径来读取文件,得到一个File
对象:
if xxx is not empty str {
let file = File::open(Path::new(fpath))
.expect(format!("cannot open {}", fpath).as_str());
}
而当它为空时,我希望从Stdin
来读取:
else {
let stdin = std::io::stdin();
}
我现在需要在if-else
块下面进行读取,我希望使用一个变量来接收file
或者stdin
,也就是像如下这样:
// 这段代码还不能编译,需要补全xxx部分的内容
let mut read_from: ??? =
if xxx is not empty str {
let file = File::open(Path::new(fpath))
.expect(format!("cannot open {}", fpath).as_str());
???
} else {
let stdin = std::io::stdin();
???
};
后来,我摸索了几下,最后发现可以写成这样:
let mut read_from: Box<dyn Read> =
if xxx is not empty str {
let file = File::open(Path::new(fpath))
.expect(format!("cannot open {}", fpath).as_str());
Box::new(file)
} else {
let stdin = std::io::stdin();
Box::new(stdin)
};
Stdin
类型以及File
类型都实现了Read
trait,所以,这样写是没有问题的。下面先来看一下Read
trait。
Read Trait
Read
Trait允许从一个给定字节来源进行读取。
Read
Trait的实现者称为readers
Read
被定义成只有一个必要方法——read()
。每一个read()
调用都将尝试从源中拉取数据到一个指定buffer。还有一系列方法都是根据read()
实现的,所以实现者只需要实现一个read()
方法,就获得了一系列其它的方法。
Reader(Read
的实现者)之间应该是可组合的,std::io
中的很多实现者都需要或者提供一个实现了Read
trait的类型。
上一句有点难以理解,如果你学过Java,你应该知道IO流的装饰器模式,这里就有点像装饰器模式,即使用一个
Read
实现包装另一个Read
实现,稍后介绍BufReader
时会看到。
请注意,每一个read()
调用都有可能卷入一个系统调用,因此,使用一些BufRead
的实现会更高效,例如:BufReader
让我们看看实际的代码:
use std::io::{Read, stdin};
use std::env;
use std::fs::File;
use std::path::Path;
use std::io;
fn main() {
let args: Vec<String> = env::args().collect();
let mut read_from: Box<dyn Read> = if args.len() > 1 {
let fpath = args.get(1).expect("cannot get argument!");
let file = File::open(Path::new(fpath))
.expect(format!("cannot open {}", fpath).as_str());
Box::new(file)
} else {
Box::new(stdin())
};
// add some code here
}
在add some code here
处,可以添加对read_from
的调用,它是一个Read
trait的实现者,现在,我们已经向下面的代码隐藏了它具体是一个文件还是来自于stdin了。
Read.read
下面会简单的介绍一下
Read
trait的API,但并不会像官方文档一样面面俱到。如果想了解更多,请移步:Rust官方文档#Read
从这个来源中拉取一些字节到指定的buffer上,返回读取了多少字节。
// 你可以把下面的代码追加到上面的 add some code here处
// 它会从file或者stdin中读取,这取决于是否有一个参数指示了文件路径
let mut buf: [u8; 1024] = [0; 1024];
while let Ok(nbytes_read) = read_from.read(&mut buf) {
// 如果没有字节可读,跳出循环
if nbytes_read == 0 {
break
}
// 从buffer构造字符串
let content = String::from_utf8_lossy(&buf[0..nbytes_read]);
print!("{}", content);
}
当read()
返回的n
为0时,可能意味着reader已经到达了“end of file(EOF)”,并且有可能不会再产生新字节了。但是请注意,这并不意味着reader将永远不会产生新的的字节。举个例子,在linux上,这个方法将为一个TcpStream
调用recv
系统调用,此时返回0代表着连接已经被正确关闭。而当在处理文件时,有可能你到达了文件尾,并且得到了0这个结果,但是,如果更多数据被追加到文件中,未来的调用可能会返回更多数据。
Read.read_to_end
很容易理解,读取到末尾。
可以使用以下代码简化之前的代码,但想想这会带来什么问题。
let mut vec: Vec<u8> = Vec::new();
read_from.read_to_end(&mut vec);
println!("{}", String::from_utf8(vec).unwrap())
当你并没有使用管道重定向stdin,并且也没通过参数指定一个文件,此时,你需要手动的在stdin中输入。直到你退出程序,read_to_end
都不会认为已经读取到了末尾,所以,你输入的内容并不会被重新打印在终端上。
这个函数将持续调用read()
来向buf
中添加数据,直到read()
返回Ok(0)
或者返回非ErrorKind::Interrupted
类型的错误。
Read.read_to_string
直接读取到字符串中。
let mut string = String::new();
read_from.read_to_string(&mut string);
println!("{}", string);
Read.read_exact
read_exact(&mut self, buf: &mut [u8])
读取指定字节以填满buf
,当尚未填满时遇到EOF
,返回ErrorKind::UnexpectedEof
BufRead Trait
Read
就介绍到这里,我们知道了,Read
的实现者叫Reader,其中一个必须的方法是read()
,其它的都具有基于read()
的默认实现,并且,我们通过它的官方文档也了解到了,官方文档其实并不推荐直接使用Read
,因为每一次调用read
,都有可能卷入一次系统调用。官方比较推荐的是BufRead
——一个带缓存的Read Trait。
pub trait BufRead: Read {
// ...
}
Rust是支持Trait之间的继承的,BufRead
继承自Read
。好家伙,又学到了,我发现看Rust的源码也能学到不少啊。
BufRead
是一种具有内部buffer的Reader,允许执行几种额外的读取。比如,在不使用buffer时,按行读取是低效的,所以如果你想要按行读取,你需要BufRead
,它包含read_line
方法,可以作为一个lines
的迭代器使用。
有点像Java里面普通Reader和BufferedReader的区别。
通过Idea中的实现者查看功能,我们可以看到,这个trait有如下实现者:
BufReader
是BufRead
的一个实现,允许通过组合方式对Reader进行包装扩展,这很类似Java的装饰器模式。
#[stable(feature = "rust1", since = "1.0.0")]
pub struct BufReader<R> {
inner: R,
buf: Buffer,
}
impl<R: Read> BufReader<R> {
// ...
}
inner
是一个Read
的实现者,也就是一个Reader,所以我们可以通过BufReader
,让任意一个Reader具有BufRead
的功能,即具有内部缓冲区以及支持按行读取。
// 使用BufReader包装原始Reader
let mut buf_reader = BufReader::new(reader);
// 现在,它具有了BufRead Trait中的所有功能
for line in buf_reader.lines() {
// xxx
}
如果你理清了Read
、BufRead
、BufReader
之间的关系,我们将上面的代码改成BufRead
版本。如果你的脑袋很乱,那么提示一下,Read
和BufRead
是特质,是trait,BufReader
是实现,请把特质和实现分开。
// 使用BufRead Trait替代Read Trait
let mut read_from: Box<dyn BufRead> = if args.len() > 1 {
let fpath = args.get(1).expect("cannot get argument!");
let file = File::open(Path::new(fpath))
.expect(format!("cannot open {}", fpath).as_str());
// File是一个Read,使用BufReader包装它
Box::new(BufReader::new(file))
} else {
Box::new(BufReader::new(stdin()))
};
// 按行读取
for line in read_from.lines() {
println!("{}", line.unwrap());
}
如果再进一步,我们发现,Stdin
类型中的Read
Trait的实现,其实完全是对其内部StdinLock
的委托:
而StdinLock
本身就实现了BufRead
所以,上面的代码可以改成:
let mut read_from: Box<dyn BufRead> = if args.len() > 1 {
// ...
Box::new(BufReader::new(file))
} else {
// 直接使用`stdin().lock()`
Box::new(stdin().lock())
};
关于BufRead
Trait其它方法的使用,已经超出本篇博客的内容了,关键的关键,不是我们学会了Read
、BufRead
、BufReader
的区别以及用法,而是我们看到了Rust标准库中的一个设计思路,我们学会了使用Trait继承、默认Trait实现、组合来进行装饰器模式设计(管它在Rust中叫什么呢),当以后我们遇到类似的设计需求时,我们也可以使用这一模式进行设计。我们还学会了使用Box<dyn Trait类型参数>
来对一个Trait的不同实现进行整合,以向后面隐藏具体的实现,这也是多态性在Rust中的体现。
总结
Read
Trait可以从一个字节来源中进行读取Read
Trait的实现者称为Reader,他们必须实现read()
方法,而其它方法都有默认实现BufRead
Trait继承自Read
,提供了额外的一些方法,提供内置缓冲区以及按行读取,我们无需再手动处理buffer了BufReader
是BufRead
的一个实现,使用装饰器模式对底层Reader进行功能扩展File
和Stdin
实现了Read
,Stdin
中的Read
实现就是对其内部StdinLock
的委托StdinLock
实现了BufRead