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 theDefault
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:
Type | Default value |
---|---|
bool | false |
char | '\0' |
Option | None |
String , &str | "" |
Vec<usize> | [] |
[usize; N] | [0, 0, …, 0] |
&[u32] | [] |
f64 , f32… | 0. |
usize , u32… | 0 |
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. ↩︎
I’ve slightly edited the patch and the code samples from rusqlite_migration to make those shorter and easier to grasp. ↩︎
Liked this post? Subscribe:
Discussions
This blog does not host comments, but you can reply via email or participate in one of the discussions below:
Featured on: