14

After reading the std::io::BufReader docs, I'm not sure how to best pass a BufReader between functions. Multiple permutations are allowed, but which is best?

I have a function that takes a file:

use std::{fs::File, io::BufReader};

fn read_some_data(f: &mut std::fs::File) {
    let mut reader = BufReader::new(f);
    read_some_other_data(&mut reader);
}

While this can be made to work, which permutation of reference access should be used when passing the reader around to other functions?

  • &mut BufReader<&mut File>
  • BufReader<&mut File>
  • &mut BufReader<File>
  • BufReader<File>

Since there is no need for each function to own the data I was thinking it would be best to pass as &mut BufReader<&mut File>, but the example in the docs uses <File>.

Whats a good rule of thumb to use here?

While this example uses BufReader, I assume the same answer would apply to BufWriter too.

3k-
  • 2,467
  • 2
  • 23
  • 24
ideasman42
  • 42,413
  • 44
  • 197
  • 320

2 Answers2

22

The most idiomatic way is probably not to reference std::io::BufReader at all. You actually want to refer to the traits Read and/or BufRead

use std::io:BufRead;

// Could also take by move if needed
fn read_data<R: BufRead>(r: &mut R);

The function usually doesn't really care whether a reader is specifically the type std::io::BufReader, merely that it has the same functionality.

This also gives you complete freedom to choose between BufReader<File>, BufReader<&mut File> or whichever other specialization you need. (It doesn't even have to be a file, which can help for testing!)

As for whether to use &mut versus a move, generally in Rust it's standard to only request what you need. If you (and the functions you call) only require an immutable reference (&T), use that, if you require mutability, use &mut T.

Move is a bit more flexible, because while it can be used simply based on whether you need to use a function that takes something by value, it's also frequently used to assert that the function will "use up" the data in some way.

This is why BufReader usually takes a File and not a reference, and why most high-level "parse this file" IO functions tend to move by value. It's generally not the case that you consume part of a File or reader with one adapter, and the rest with another.

In fact, this is conceptually so strong that rather than giving a reference, it's much more common to just move the File into a higher-level reader and call a function like into_inner to retrieve the file whenever you need to switch adapters.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Linear
  • 21,074
  • 4
  • 59
  • 70
  • 2
    I'm very new to rust, but wouldn't fn read_data(r: &mut R) be a better solution? – Philip Ridout Jun 18 '19 at 07:51
  • 2
    @PhilipRidout that entirely depends on whether you're using the `BufRead` methods. Ones like `split` or `lines` are really common. – Linear Jun 19 '19 at 23:37
3

Looking at the way BufReader's methods are invoked (on &mut self or self) I would say you will usually want to pass &mut BufReader, or BufReader if the function needs to own the BufReader in order to e.g. convert it into a Bytes or Chars iterator.

The reason why docs describe BufReader<File> and not BufReader<&mut File> is because the BufReader owns its underlying Read instance.

ljedrz
  • 20,316
  • 4
  • 69
  • 97
  • Hi, I'm learning Rust. The link is broken but I can't help fix it, because I don't understand the sentence "`BufReader` owns its underlying `Read` instance" ... [`BufReader` is a struct](https://doc.rust-lang.org/std/io/struct.BufReader.html#), [`Read` is a trait](https://doc.rust-lang.org/std/io/trait.Read.html), right?. Wouldn't it be more accurate to say ["`BufReader` **implements** the `BufRead` trait"](https://doc.rust-lang.org/std/io/struct.BufReader.html#impl-BufRead-for-BufReader%3CR%3E)? Or maybe "`BufReader` owns its underlying `R` instance"? – Nate Anderson Feb 23 '23 at 05:55