TL;DR

The Default trait can enhance the maintenability of your code. Default values for common types are listed at the end.

A PR Review

Recently, while reviewing a PR1, I noticed that part of the patch was introducing a new field to a struct:

 1diff --git a/src/lib.rs b/src/lib.rs
 2index eba9a3a..8619e06 100644
 3--- a/src/lib.rs
 4+++ b/src/lib.rs
 5@@ -106,8 +108,9 @@ use std::{
 6 #[derive(Debug, PartialEq, Clone)]
 7 pub struct M<'u> {
 8     up: &'u str,
 9     down: Option<&'u str>,
10+    foreign_key_check: bool,
11 }
12 
13 impl<'u> M<'u> {
14@@ -137,8 +140,9 @@ impl<'u> M<'u> {
15     pub const fn up(sql: &'u str) -> Self {
16         Self {
17             up: sql,
18             down: None,
19+            foreign_key_check: false,
20         }
21     }

That prompted me to reflect on the code I had initially written. Prior to the patch, it looked roughly2 like this:

 1#[derive(Debug, PartialEq, Clone)]
 2pub struct M<'u> {
 3    up: &'u str,
 4    down: Option<&'u str>,
 5}
 6
 7impl<'u> M<'u> {
 8    pub const fn up(sql: &'u str) -> Self {
 9        Self {
10            up: sql,
11            down: None,
12        }
13    }
14}

The Default Trait

What if I had used the Default trait here? The code could have looked like this:

 1#[derive(Debug, Default, PartialEq, Clone)]
 2pub struct M<'u> {
 3    up: &'u str,
 4    down: Option<&'u str>,
 5}
 6
 7impl<'u> M<'u> {
 8    pub const fn up(sql: &'u str) -> Self {
 9        Self {
10            up: sql,
11            ..Default::default()
12        }
13    }
14}

On the first line, the #[derive(Default)] attribute makes the structure M implement the Default trait. Thanks to this trait, a call to M::default() will create a struct with default values for its fields: M { up: "", down: None }. Note that when a structure M is expected, Default::default() is equivalent to M::default().

We then need to initialize the two fields of that structure, overriding some defaults:

  • up is defined directly as before. That’s the value we want to override.
  • down is set by the Default trait. This is done by ..Default::default() on line 11. Default::default() provides the values. Then the .. syntax fills out the fields that were not directly set. down is thus set to the same value as before, None.

The code is just as long as before, when we were not using the Default trait. But then, line 19 of the above patch would have been unnecessary: false is the default for a bool, so the new foreign_key_check field would have been covered by the ..Default::default().

This results in a shorter patch:

 1diff --git a/src/lib.rs b/src/lib.rs
 2index eba9a3a..8619e06 100644
 3--- a/src/lib.rs
 4+++ b/src/lib.rs
 5@@ -106,8 +108,9 @@ use std::{
 6 #[derive(Debug, PartialEq, Clone)]
 7 pub struct M<'u> {
 8     up: &'u str,
 9     down: Option<&'u str>,
10+    foreign_key_check: bool,
11 }
12 
13 impl<'u> M<'u> {

Conclusion

In the example of this post, we are doing only one instantiation of that particular struct, and it has very few fields anyway. But if there were many instantiations of that struct, we would have had to change all of those. Then, using Default would have been quite beneficial.

This pattern were the return type is built with a call to Default::default() seems relatively common in the Rust organization. It can enhance maintainability, much more than I initially thought.

Of course this is a balancing act. For instance, this pattern could be abused by defining custom default values on primitive types for a particular structure. That would lead to Default::default() filling surprising values and the code would be less predictable.

EDIT(2022-07-29): As SpudnikV pointed out, using defaults as explained in this post can hide the implications of a change made to a structure. There could be code in various places relying on invariants that may break due to the change. Without Default::default(), the change might make visible edits in these places, drawing the attention of reviewers on these invariants.

That’s another case where it might not be wise to use the Default trait. Again, it’s a balancing act!


Appendix: Defaults for Some Common Types With derive

Why did I not use the Default trait initially? Part of it might be the fear of introducing incorrect code. It was slightly unclear to me what derive uses as a default value for common primitive types. And you don’t want a field set explicitly to false becoming a true once you use Default, right?

Let’s take a closer look by running the following program:

#[derive(Debug, Default)]
struct D<'a> {
    b: bool,
    c: char,
    o: Option<usize>,

    string: String,
    str: &'a str,

    v: Vec<usize>,
    a: [usize; 10],
    s: &'a [u32],

    f: f64,
    u: (usize, u32, u64)
}

fn main() {
    println!("{:#?}", D::default());
}

Output:

D {
    b: false,
    c: '\0',
    o: None,
    string: "",
    str: "",
    v: [],
    a: [
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
    ],
    s: [],
    f: 0.0,
    u: (
        0,
        0,
        0,
    ),
}

It turns out that in general, the default for common types in the standard library is a 0 byte in the underlying data structure. Note that arrays are fixed size in rust and thus the default is an array of the right size, filled with defaults for the inner type. That’s quite similar to go.

Quick Reference

Here is a table for future reference:

TypeDefault value
boolfalse
char'\0'
OptionNone
String, &str""
Vec<usize>[]
[usize; N][0, 0, …, 0]
&[u32][]
f64, f32…0.
usize, u32…0

  1. Please don’t take anything in this post as critical of the PR’s author work. I’m very grateful that they took some time to contribute to the project↩︎

  2. I’ve slightly edited the patch and the code samples from rusqlite_migration to make those shorter and easier to grasp. ↩︎