Loading

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中的很多实现者都需要或者提供一个实现了Readtrait的类型。

上一句有点难以理解,如果你学过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有如下实现者:

img

BufReaderBufRead的一个实现,允许通过组合方式对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
}

如果你理清了ReadBufReadBufReader之间的关系,我们将上面的代码改成BufRead版本。如果你的脑袋很乱,那么提示一下,ReadBufRead是特质,是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的委托:

img

StdinLock本身就实现了BufRead

img

所以,上面的代码可以改成:

let mut read_from: Box<dyn BufRead> = if args.len() > 1 {
    // ...
    Box::new(BufReader::new(file))
} else {
    // 直接使用`stdin().lock()`
    Box::new(stdin().lock())
};

关于BufReadTrait其它方法的使用,已经超出本篇博客的内容了,关键的关键,不是我们学会了ReadBufReadBufReader的区别以及用法,而是我们看到了Rust标准库中的一个设计思路,我们学会了使用Trait继承、默认Trait实现、组合来进行装饰器模式设计(管它在Rust中叫什么呢),当以后我们遇到类似的设计需求时,我们也可以使用这一模式进行设计。我们还学会了使用Box<dyn Trait类型参数>来对一个Trait的不同实现进行整合,以向后面隐藏具体的实现,这也是多态性在Rust中的体现。

总结

  • Read Trait可以从一个字节来源中进行读取
  • Read Trait的实现者称为Reader,他们必须实现read()方法,而其它方法都有默认实现
  • BufRead Trait继承自Read,提供了额外的一些方法,提供内置缓冲区以及按行读取,我们无需再手动处理buffer了
  • BufReaderBufRead的一个实现,使用装饰器模式对底层Reader进行功能扩展
  • FileStdin实现了ReadStdin中的Read实现就是对其内部StdinLock的委托
  • StdinLock实现了BufRead
posted @ 2022-12-20 17:08  yudoge  阅读(1218)  评论(0编辑  收藏  举报