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
fn default() -> Selfis the only method...Default::default()is the struct update syntax from the structs module: "the rest of the fields come from the default". It's the idiomatic way to override a few settings.- Lots of the standard library builds on
Default:unwrap_or_default(),HashMap'sentry(k).or_default(),std::mem::take, and#[derive(Default)]on a struct whose fields are your types.
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
- Implementing
Defaultwith values that aren't really defaults. A default port of 8080 is reasonable. A default user name of"admin"is a security bug waiting to happen. If there's no safe default, don't implementDefault: make callers choose. - Calling
as_ref()without a type. Many types implement severalAsRefs (StringisAsRef<str>,AsRef<[u8]>,AsRef<Path>...), solet s = owned.as_ref();needs an annotation:let s: &str = owned.as_ref();.
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