Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 22 of 26

Easy Primitives

Default, #[default] on an enum variant, and ..Default::default()

Build a configuration from a list of (key, value) pairs, starting from defaults and applying only the keys that are present.

pub fn build_config(pairs: Vec<(String, String)>) -> String

Recognised keys: retries (a u16), name (a String), mode (one of fast, safe, anything else meaning Idle). A retries value that does not parse falls back to the default. Unknown keys are ignored. Later pairs win.

Render as "{retries}|{name}|{mode:?}". With no pairs at all that is "0||Idle".

Rust has no default arguments

No optional parameters, no keyword arguments, no overloading. A function with four configurable knobs cannot be called four different ways. What Rust has instead is the Default trait plus struct update syntax:

let cfg = Cfg { retries: 5, ..Default::default() };

Two knobs, one line, every other field at its default. This is the lightweight alternative to a full builder, and for most types it is enough.

Derived Default gives every field its default: 0 for numbers, false for bool, "" for String, None for Option<T>, an empty Vec.

#[default] on an enum

An enum has no obvious default, so you nominate one:

#[derive(Default, Debug)]
enum Mode {
    #[default]
    Idle,
    Fast,
    Safe,
}

One restriction: #[default] works on unit variants only. Put it on Fast(u32) and you get E0665, because the derive would have no way to invent the payload.

The lint that teaches the idiom

field_reassign_with_default is on by default and fires on this:

let mut cfg = Cfg::default();
cfg.retries = 5;             // error under -D warnings

The starter ships it. The fix is the struct-update form, and the lint is right for reasons beyond style: the poke-after-build version needs mut where the other does not, it constructs a value that is briefly wrong, and with a #[non_exhaustive] struct from another crate it would not even compile.

When Default is the wrong answer

This is the part usually left out. u16::default() is 0 and String::default() is "" — both plausible-looking values that can be silently, badly wrong. A Timeout of zero, a RetryLimit of zero, an empty ApiKey: each of those is a bug that Default will hand you with a straight face.

The alternative is to make the missing case a type error: no Default impl, a constructor that demands the field, or an Option<T> that forces the caller to decide. Default is right when “nothing specified” genuinely has a sensible meaning, and wrong when it does not. Deriving it reflexively on everything is a habit worth resisting.

Neighbouring lints

derivable_impls (which you met earlier) catches a hand-written Default that duplicates the derive. default_trait_access (allow-by-default) prefers Cfg::default() to Default::default() where the type is not already obvious. new_without_default asks for a Default impl alongside a public zero-argument new.

Remember the grade is compile + tests + clippy -D warnings.