How to store a *trait* as field of a struct?

How can an object of any type that implements a specific trait be stored in a field of a struct?

For example, suppose we want to have a Read, which could be, e.g., a File?

Problem is, the foo: Read + 'static syntax can only be used with function param, not struct field.

foo: dyn Read is not possible as field of a struct either, as the size is "cannot be known" :face_with_raised_eyebrow:

The best, that I have come up with, is wrapping the dyn Read in an Arc<T> :sweat_smile:

Is this the "recommended" approach ???

Problem remains: From Arc<T> we can not get a mutable reference to call the read() method :weary:

use std::{io::Read, sync::Arc, fs::File};

struct Test {
    readable: Arc<dyn Read>,
}

impl Test {
    pub fn new(readable: impl Read + 'static) -> Self {
        Self {
            readable: Arc::new(readable),
        }
    }

    pub fn read(&self, buf: &mut [u8]) {
        self.readable.read(buf); // <-- ERROR: cannot borrow data in an `Arc` as mutable
    }
}

fn main() {
    let file = File::open("foo.txt").unwrap();
    let test = Test::new(file);
}

Thank you!

1 Like

Keep in mind that a trait is not, itself, a type. dyn SomeTrait is, but as you discovered, it's not sized, so it can't be used as a value in a context where the size of the value must be known.

The most common ways to abstract over types implementing a trait are:

  • Take a reference. Given a type Example which implements the Read trait, a value of type &example or &mut example can be freely converted to type &dyn Read or &mut dyn Read respectively.

    This would give

    use std::{io::Read, fs::File};
    
    struct Test<'r> {
        readable: &'r mut dyn Read,
    }
    
    impl<'r> Test<'r> {
        pub fn new(readable: &'r mut dyn Read) -> Self {
            Self { readable }
        }
    
        pub fn read(&mut self, buf: &mut [u8]) {
            self.readable.read(buf); // <-- ERROR: cannot borrow data in an `Arc` as mutable
        }
    }
    
    fn main() {
        let mut file = File::open("foo.txt").unwrap();
        let test = Test::new(&mut file);
    }
    
  • Put the value behind some kind of pointer type. This includes Box<dyn Read>, Rc<dyn Read>, Arc<dyn Read>, and more esoteric options. Which one is appropriate depends on your needs around allocation and sharing.

  • Genericize your code over the trait. A function that can work with anything implementing Read can be defined using a type parameter: fn with_read<R>(reader: R) where R: Read, giving a unique implementation for each type R that's actually used in your program somewhere. For example, let ex: Example = โ€ฆ; with_read(ex); would work. Note that you generally do not abstract struct fields over traits - the definition of the struct itself usually doesn't depend on the traits implemented by the fields, even if any given impl on that type may have such a dependency.

    This would give:

    use std::{io::Read, fs::File};
    
    struct Test<R> {
        readable: R,
    }
    
    impl<R> Test<R> {
        pub fn new(readable: R) -> Self {
            Self { readable }
        }
    }
    
    impl<R> Test<R>
        where R: Read,
    {
        pub fn read(&mut self, buf: &mut [u8]) {
            self.readable.read(buf); 
        }
    }
    
    fn main() {
        let mut file = File::open("foo.txt").unwrap();
        let test = Test::new(&mut file);
    }
    
13 Likes

You can use Box instead of Arc to allow mutable access.

3 Likes

Thanks for explanation! I think I will go with a Box<T> for now, as I need the struct to actually own the trait object, not just hold a reference.

1 Like

Note that Box<T> is not a trait object and is not doing dynamic dispatch. It is a Box pointing to a concrete type. A trait object using Box would be written Box<dyn Trait>.

You will choose between storing a trait object or a struct of a concrete type whether you want that the same instance of Test is able to own a struct of changing types (all implementing the specified trait) or if a specific instance of Test is tied to a concrete type T. You probably do not need to dynamically change to a inner struct of another type in the same Test instance, but it depends on your specific use case.

But if you were really considering to use Box<T>, then you probably should just store a struct of type T instead, as Box<T> does not give you any benefit. And the user of your struct will still be able to define T: Box<U> if they really need a box. This would still work because if U: Read then Box<U>: Read.

I would say that it is preferable to stick to concrete type T and avoid trait object if possible. You will be more flexible. You would be able to give a references &T/&mut T to let caller use the inner struct as a T (which would not be possible using trait objects).

And you will be able to provide some functions based on which traits is implemented by the inner struct and let the caller be able to give you structs that do not implement all supported traits:

struct<R> Test {
  readable: R
}


impl<R> Test<R>
    where R: Read,
{
    pub fn read(&mut self, buf: &mut [u8]) {
        self.readable.read(buf); 
    }
}


impl<R> Test<R>
    where R: Other,
{
    pub fn make_something(&mut self) {
        self.readable.make_something(); 
    }
}

With a trait object, you will be force to make the bound in Test struct definition, and forcing all the user of Test to provide structs which implements all traits at once:

struct Test {
  readable: Box<dyn Read + Other>,
}
3 Likes

I don't know how to replace the Box with generics in this situation:

use std::io::{Result, Read};
use std::fmt::Debug;

use crate::net::{StreamIO, Buffer};

use super::StatusCode;
use super::content_type::ContentType;

#[derive(Debug)]
pub struct Response { 
    header: String,
    body: Body,
}

enum Body {
    None,
    Text(&'static str),
    Data(Buffer),
    File(Box<dyn Read>),
}

impl Response {
    pub fn from_text(status_code: StatusCode, text: &'static str) -> Self {
        Self {
            header: create_header(status_code, text.len() as u64, Some(ContentType::Text)),
            body: Body::Text(text)
        }
    }

    pub fn from_data(status_code: StatusCode, data: Buffer, content_type: Option<ContentType>) -> Self {
        Self {
            header: create_header(status_code, data.len() as u64, content_type),
            body: Body::Data(data)
        }
    }

    pub fn from_file(status_code: StatusCode, source: Read + 'static, size: u64, content_type: Option<ContentType>) -> Self {
        Self {
            header: create_header(status_code, size, content_type),
            body: Body::File(Box::new(source))
        }
    }

    pub fn from_size(status_code: StatusCode, size: u64, content_type: Option<ContentType>) -> Self {
        Self {
            header: create_header(status_code, size, content_type),
            body: Body::None,
        }
    }

    pub fn send(self, mut writer: StreamIO) -> Result<()> {
        writer.write(&self.header.as_bytes()[..])?;
        match self.body {
            /* ... */
        }
    }
}

If I make Body generic, then type parameter T creeps into Response.

But from_data() or from_text() has no reasonable type to set T to, because it doesn't need T :thinking:

Also, the different "types" of bodies should ideally be abstracted away in the Response...

Yes trait objets can simplify the code by avoiding to have to repeat the type bound everywhere. My point was that if you used Box<T>, it would be easy to refactor to be generic just over T.

Do you need to store the different ways you obtained the response then ?

If you do not plan to treat the data specifically from the kind of source, why not just defining Response as such:

struct Response {
  header: String,
  body: Vec<u8>,
}

You could still be able to define different ways to build a Response struct :

pub fn from_file<S: Read>(status_code: StatusCode, source: S, size: u64, content_type: Option<ContentType>) -> Self {
        Self {
            header: create_header(status_code, size, content_type),
            body: vec![] // TODO: extract bytes from source
        }
    }

You'd do this.

6 Likes

If you do not plan to treat the data specifically from the kind of source, why not just defining Response as such:

struct Response {
   header: String,
   body: Vec<u8>,
}

This would require to read the whole file (which could be rather big!) into Vec to build a response :thinking:

I thought it would be good to be able to just pass a dyn Read (e.g. File).

In this case, the Response::send() would simply "pump through" the contents from the source (e.g. File), in a "chunk by chunk" fashion, without ever having to load the whole thing in memory.

You'd do this .

Looks good! Didn't know/expect this is possible :exploding_head:

What becomes of type parameter R for Response<R> in those cases where it's left totally unspecified?

What part in particular seemed impossible?

An inference error. The compiler will try to infer a specific type at the end of the day, but if there are absolutely no constraints on it that could be used, then you'll get a compile error.

Ok, I better see your use case now. You want to construct a Response on the server which would then be transferred and abstract the actual source of bytes.

One solution is to consider that you will ultimately only need to write bytes, so a solution is to only consider that body must be Read. You'll avoid to have to specify a weird dummy R type when you are storing an enum variant that is not related to this generic type bound.

You can use std::io::Cursor for types that do not implement Read directly but are AsRef<[u8]>.

struct Response<BodyRead> {
  header: String,
  body: BodyRead,
}

impl Response<std::io::Cursor<Vec<u8>>> {
    fn from_string(s: String) -> Self {
        Self {
            header: todo!(),
            body: std::io::Cursor::new(s.into_bytes()),
        }
    }
    
    fn from_data(data: Vec<u8>) -> Self {
        Self {
            header: todo!(),
            body: std::io::Cursor::new(data),
        }
    }
}

impl<BodyRead: Read> Response<BodyRead> {
    fn from_read(read: BodyRead) -> Self {
        Self {
            header: todo!(),
            body: read,
        }
    }
    
    pub fn send<W: std::io::Write>(mut self, writer: &mut W) -> Result<()> {
        writer.write(&self.header.as_bytes()[..])?;
        std::io::copy(&mut self.body, writer)?;
        Ok(())
    }
}

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.