Read and Write traits

This code works, but I have questions about it. See my questions/comments in the TODO comments below.

use std::io::{Error, ErrorKind, Read, Result, Write};

#[derive(Debug)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
}

impl Color {    
    fn new() -> Color {
        Color { r: 0, g: 0, b: 0 }
    }
}

impl Read for Color {
    // Writes bytes from a Color instance into a byte buffer.
    //TODO: Why does the Read trait require self to be mutable here?
    fn read(&mut self, buf: &mut[u8]) -> Result<usize> {
        if buf.len() < 3 {
            return Err(Error::new(ErrorKind::Other, "buffer is too short"));
        }
        buf[0] = self.r;
        buf[1] = self.g;
        buf[2] = self.b;
        Ok(3) 
    }
}

impl Write for Color {
    // Writes bytes from a byte buffer into a Color instance.
    fn write(&mut self, buf: &[u8]) -> Result<usize> {
        if buf.len() < 3 {
            return Err(Error::new(ErrorKind::Other, "buffer is too short"));
        }
        self.r = buf[0];
        self.g = buf[1];
        self.b = buf[2];
        Ok(3) 
    }
    
    fn flush(&mut self) -> Result<()> {
        Ok(())
    }
}

fn main() {
    let mut color = Color {
        r: 100,
        g: 0,
        b: 150,
    };
    
    let mut buffer = [0; 3];
    //TODO: This reads backwards to me. I would guess this is
    // reading from the buffer INTO the color, but it's the opposite.
    color.read(&mut buffer).unwrap();
    println!("buffer = {:?}", buffer);
    
    let mut new_color = Color::new();
    //TODO: This reads backwards to me. I would guess this is
    // writing the color into the buffer, but it's the opposite.
    new_color.write(&buffer).unwrap();
    println!("new_color = {:?}", new_color);
}

(Playground)

Output:

buffer = [100, 0, 150]
new_color = Color { r: 100, g: 0, b: 150 }

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.91s
     Running `target/debug/playground`

I'll address the Read portion.

A more typical use of a Read trait is to read from some source into a buffer, perhaps over many calls to read; once the source is exhausted, further attempts to read return Ok(0) indicating there isn't a problem, but there isn't any more data either.

Your implementation doesn't allow incremental reading (e.g. into a buffer of size 1) and it doesn't stop once the Color has been read. For example, try adding this to your code:

    let mut buffer = [0; 12];
    color.read_exact(&mut buffer).expect("read_exact failed");
    dbg!(buffer);

So, one thing you could do to make your implementation act more typically is to have a ColorReader type that keeps track of how many colors have been written to a buffer. Then you could fill a buffer with less than all three colors at a time, and you could stop filling buffers once you've exhausted the struct. Naturally you would have to track this state somehow across calls to read -- you would need to utilize the mutability of &mut self.

2 Likes

I don’t think it is a very good idea to try to implement Read and Write for a type such as yours. Furthermore, your implementation is not really "correct". E.g. for Write, your type would need to keep some internal state on "how much it’s filled". Basically, a sequence like (pseudo-code without error handling)

foo.write(&[x])
foo.write(&[y])
foo.write(&[z])

is supposed to do the same as a single

foo.write(&[x, y, z])

and in multiple calls like

foo.write(&[x, y, z])
foo.write(&[a, b, c])

the second is not supposed to overwrite the effects of the first.

For example if you call write_all on your Color type, it will fail in weird ways if the buffer provided doesn’t have a length that is a factor of 3. Look at how write_all is implemented for perhaps some better understanding of how write is expected to be used.

Read and Write are supposed to give an interface to things that contain or can provide some raw data, such as Files, arrays/vectors of u8, stdin/stdout, etc.. look at the list of implementors

If you want to have ways to convert a datatype into binary data and back, you should look into serialization and deserialization, in particular involving the serde crate.

4 Likes