I have a struct that can be converted to text through a complex series of method calls which contain numerous write!
calls. This text can be written to a file or to a debug log. I am trying to decide whether to use fmt::Write
or io::Write
. I can't really use both because then all of the writing methods would need to be duplicated.
Simplified example:
impl MyType {
fn write_it(&self, writer: &mut impl ???) {
// ^ fmt::Write or io::Write?
self.write_header(writer);
self.write_contents(writer);
self.write_footer(writer);
}
fn write_header(&self, writer: &mut impl ???) {
write!(writer, "...")
}
// and more...
}
The docs for fmt::Write
say,
...This is similar to the standard library's io::Write trait, but it is only intended for use in libcore.
So this leads me to believe I should use io::Write
. This (obviously) will work well for I/O types like BufWriter
. And it seems notable that serde_json does it this way.
// if I use io::Write, I can do this
my_type.write_it(&mut BufWriter::new(File::create("my-file.txt")?)?;
I also want to use my type with format!
and similar macros. So I need to implement Display
. In fact, isn't Display
the de-facto trait for a type that can be represented as a String
?
// I want to do this
println!("Contents:\n{}", my_type);
// or this
let mut s = String::new();
write!(s, "{}", my_type);
So I think I'll just tie in my implementation with Display
. But here's the problem:
impl Display for MyType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_it(f)?;
// ^ the trait `std::io::Write` is not implemented for `std::fmt::Formatter<'_>`
}
}
I can fix this by using impl fmt::Write
, but then I lose the I/O capability mentioned before. There is no adapter between the two types (I assume there would be one if that were an intended usage). So I am caught between the two. Which trait should I use? Is there an idiomatic way to get the functionality of both?
Edit: Adapter?
@ÖmerErden suggested creating an adapter like the following. I'm just not sure what conditions will cause the UTF-8 conversion to fail. Is that conversion guaranteed to not fail as long as I write valid UTF-8 with write!
? It seems too risky to me.
struct Adapter<'a> {
f: &'a mut dyn fmt::Write,
}
impl<'a> io::Write for Adapter<'a> {
fn write(&mut self, b: &[u8]) -> Result<usize, io::Error> {
let s = str::from_utf8(b)
.map_err(|_| io::Error::from(io::ErrorKind::Other))?;
self.f.write_str(s)
.map_err(|_| io::Error::from(io::ErrorKind::Other))?;
Ok(b.len())
}
fn flush(&mut self) -> Result<(), io::Error> {
Ok(())
}
}
The correct thing to do is implement Display
for your type, and not have a write_it
method at all. Ideally, your header
, content
, and footer
also implement Display
, because then you can write something like this:
impl Display for MyType {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}{}{}", self.header, self.content, self.footer)
}
}
However, if that's not possible, you can still write out your implementation manually like you did in your sample code.
In fact, isn't Display the de-facto trait for a type that can be represented as a String?
It's more accurate to say that Display
is the de-facto trait for a type that can be written to something. Converting things to strings is just one of the many use cases for Display
, but on its own it doesn't create any intermediary strings.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With