Best way to construct a struct with many fields set to default values?

Usually I would just follow the Struct { field1, field2, field3, ..Default::default() } pattern, but now I have a struct where some of the fields are of a type that does not implement Default.

What's a good way to allow constructing the struct without having to specify all the fields? The two approaches I could think of are:

  • Use two separate structs for the required and optional fields, and add a method to one or both that takes in the other and produces a merged Struct, e.g. StructBuilder{field1, field2}.build(StructOptional{field3, ..Default::default()})

  • Add a constructor function that takes in the required arguments, and then just set the other fields on the result, e.g. let mut s = Struct::new(field1, field2); s.field3 = field3;.

I don't particularly like either approach. The first one requires two extra types, and the second one removes the names from the call site, making ::new() less clear when used with literals, e.g. ::new(5, "foo").

Are there good examples / a convention for something like this? Do I need to design my APIs differently (e.g. only use two separate types instead of one unified type)?

The typical builder pattern in Rust would be to add methods named after the fields, so that the call would look roughly like StructBuilder::new(field1_value, field1_value).field3(field3_value).finish(). There’s different variants where finish() might only borrow the builder and clone the fields vs. take it by value, so it can be re-used (probably useful when fields are cheap to clone and Rust is expected to optimize those clones away when they’re not used).

Edit: One more thing to note: In case that you can come up with reasonable default values for all your fields, even if some of the field’s types don’t implement Default, you can still, manually, implement the Default trait for your struct. Then Struct { field1, field2, field3, ..Default::default() } is still usable.

That seems more or less the same as the second option, but with an extra intermediate type and additional code for each optional field. I can see the value of using a builder if the fields are non-public, or if there's additional validation to perform (e.g. field4 can only be set if field3 is not). But in the case of a struct with all public fields (which they would have to be in order to use the ..Default::default() method), I'm not sure I see the value of adding a builder.

1 Like

An option you didn't cover is just to put all of the optional fields into its own type. So you'd have something like

Renderer {
    target: get_render_target(),
    gl_engine: initialize_opengl(),
    config: RendererConfig {
        window_size: (1600, 900),
        fullscreen: false,
        ..Default::default()
    },
}

This has a potential advantage of clearly making what fields are optionally defaulted, or maybe loading the config from serialization, or whatever. It's probably better than the "two builder parts" approach, though not necessarily better than a full (typesafe?) builder approach (except in being much simpler in construction).

Yeah, that seems like a good improvement over the two-part builder system. It still feels somewhat more complicated than it should be (when consuming the struct, because now there's a .config.field instead of just .field), but it's not unreasonable. Thanks!

I had a similar problem and came up with a simple hack that I'm not too worried about because I'm writing a prototype. I replaced

foo: TypeWithNoDefault

with

foo: Vec<TypeWithNoDefault>

I can then use ..Default::default() and refer to the value as foo[0]. That's only 3 extra characters. It's weird, so I documented the heck out of it.

I'd rather use Option for that, even if it is somewhat longer. I think that gets you some optimization in certain cases, and is easier to explain :wink:

1 Like

you can fix this inconvenience for a lot of cases by implementing Deref

#![allow(unused)]

use std::ops::{Deref, DerefMut};

impl Deref for Renderer {
    type Target = RendererConfig;
    fn deref(&self) -> &Self::Target {
        &self.config
    }
}
impl DerefMut for Renderer {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.config
    }
}

fn foo() {
    let r = Renderer {
        target: get_render_target(),
        gl_engine: initialize_opengl(),
        config: RendererConfig {
            window_size: (1600, 900),
            fullscreen: false,
            ..etc()
        },
    };
    
    if r.fullscreen {
        
    }
}




// Default::default() abbreviation
fn etc<T: Default>() -> T {
    Default::default()
}

struct Target;
struct GlEngine;
fn get_render_target() -> Target {
    Target
}
fn initialize_opengl() -> GlEngine {
    GlEngine
}
struct Renderer {
    target: Target,
    gl_engine: GlEngine,
    config: RendererConfig,
}
struct RendererConfig {
    window_size: (u32,u32),
    fullscreen: bool,
    memory_limit: u32,
}
impl Default for RendererConfig {
    fn default() -> Self {
        RendererConfig {
            window_size: (100,100),
            fullscreen: false,
            memory_limit: 10*2^20,
        }
    }
}

I tried that, but I don't like to have .unwrap() without explaining why it won't panic, which made it more than just a few characters. Arguing against my own case (one of my hobbies), I don't explain why foo[0] won't panic. Hey, nobody's ever accused me of being consistent.

1 Like

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.