RustArena

Step 6 of 7

Default and AsRef

Two small traits that make your types pleasant to use.

Default: a sensible starting value

#[derive(Default)] fills every field with zero, false or empty. When the right starting value is something else, implement Default yourself:

#[derive(Debug)]
struct ServerConfig {
    host: String,
    port: u16,
    workers: u32,
}

impl Default for ServerConfig {
    fn default() -> Self {
        ServerConfig { host: String::from("localhost"), port: 8080, workers: 4 }
    }
}

fn main() {
    let dev = ServerConfig::default();
    let prod = ServerConfig { host: String::from("0.0.0.0"), workers: 16, ..Default::default() };
    println!("{}:{} x{}", dev.host, dev.port, dev.workers);
    println!("{}:{} x{}", prod.host, prod.port, prod.workers);

    let missing: Option<u32> = None;
    println!("{}", missing.unwrap_or_default());
}
localhost:8080 x4
0.0.0.0:8080 x16
0

For an enum, derive it and mark the default variant with #[default]:

#[derive(Debug, Default)]
enum Theme {
    #[default]
    Light,
    Dark,
}

Clippy's new_without_default lint suggests adding Default whenever a type has a new() that takes no arguments; teams usually implement both, with one calling the other.

AsRef: "anything I can borrow as a &str"

AsRef<T> is a cheap reference conversion: x.as_ref() borrows x as a &T. It's used for parameters that should accept several types:

fn shout<S: AsRef<str>>(text: S) -> String {
    let s: &str = text.as_ref();
    format!("{}!", s.to_uppercase())
}

fn main() {
    let owned = String::from("gg");
    println!("{}", shout("hello"));
    println!("{}", shout(&owned));
    println!("{}", shout(owned));
}
HELLO!
GG!
GG!

&str, &String and String all implement AsRef<str>. You'll see this all over the standard library: std::fs::read_to_string takes impl AsRef<Path>, which is why you can pass it a &str, a String or a PathBuf.

AsRef or Into? AsRef borrows (no copy, no allocation): use it when you only need to look at the data. Into converts and takes ownership: use it when you need to store an owned value, like User::new(name: impl Into<String>). For plain functions that take text, a simple &str parameter is still the most common choice; AsRef shines in generic code and when callers would otherwise write & everywhere.

Common mistakes

Your task

Implement Default for GameSettings (volume 80, difficulty "normal", fullscreen true), and write fn quiet_settings() -> GameSettings that returns the defaults with only the volume changed to 10, using ..Default::default().

Challenge 2

Derive Default for the Theme enum with Dark as the default variant (use #[default]), then implement Default for Prefs by hand: the default theme (Theme::default()) and a font size of 14.

Challenge 3

Write fn word_count<S: AsRef<str>>(text: S) -> usize that counts the words (split on whitespace), so it accepts a &str, a String or a &String. Then implement AsRef<str> for the Headline newtype, so word_count(&headline) works too.

Previous: Ordering: PartialOrd and Ord for sorting Next: Drop and Deref