Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a short way to implement Default for a struct that has a field that does not implement Default?

Tags:

rust

I have a struct that has 20 fields:

struct StructA {
    value1: i32,
    value2: i32,
    // ...
    value19: i32,
    day: chrono::NaiveDate,
}

I'd like to impl Default trait for StructA. I tried to add #[derive(Default)] to the struct, but chrono::NaiveDate doesn't implement Default.

I then tried to implement Default for StructA:

impl Default for StructA {
    fn default() -> Self {
        Self {
            value1: Default::default(),
            value2: Default::default(),
            // ...
            value19: Default::default(),
            day: chrono::NaiveDate::from_ymd(2021, 1, 1),
        }
    }
}

This code works fine, but the parts of value1 through value19 are redundant. Is there a solution with less code?

  • I defined StructA to deserialize JSON data via serde-json so I can't change the struct's definition.
  • A value of day: chrono::NaiveDate is always given from JSON data, so I want to avoid day: Option<chrono::NaiveDate>.
like image 936
tomlla Avatar asked Jul 12 '21 10:07

tomlla


2 Answers

The derivative crate makes this kind of thing a breeze:

#[derive(Derivative)]
#[derivative(Default)]
struct StructA {
    value1: i32,
    value2: i32,
    // ...
    value19: i32,
    #[derivative(Default(value = "NaiveDate::from_ymd(2021, 1, 1)"))]
    day: NaiveDate,
}

If you want to avoid external crates, your options are:

  • the approach you already used, with the downside that you must name all the fields. Also note that you don't need to repeat Default::default() for each numeric field, a simple 0 will work as well.
  • make day an Option and derive Default, with the downside that it will default to None, bear a run-time cost, and you'll have to unwrap() to access it.
  • make day a newtype that wraps NaiveDate and implements Default to set it to the desired value, with the downside that you'll need to access the NaiveDate through a (zero-cost) field or method.
like image 50
user4815162342 Avatar answered Oct 22 '22 04:10

user4815162342


That's a rather dirty trick, but you can wrap your date in an Option, and it has an implementation of Default. Then you won't need to implement Default on your own, you can derive it. To keep the same semantics of StructA::default() you'll need to write your own method (luckily Rust allows to define default() method besides already derived Default::default()) Playground

use chrono;

#[derive(Debug, Default)]
struct StructA {
  value1: i32,
  value2: i32,
  value19: i32,
  day: Option<chrono::NaiveDate>,
}

impl StructA {
  fn default() -> Self {
    let mut instance: Self = Default::default();
    instance.day = Some(chrono::NaiveDate::from_ymd(2021, 1, 1));
    instance
  }
}

fn main() {
    println!("{:?}", StructA::default());
    // StructA { value1: 0, value2: 0, value19: 0, day: Some(2021-01-01) }
}

Downsides of this version:

  • Need to .unwrap() the date everywhere it's used
  • Two methods with same name default, but one is Self::default which fills the date as I implemented and the other is Default::default which fills the date with None, you'll need to be careful which you call (calling StructA::default() invokes Self::default())

EDIT. Please be careful with this answer (details in the comments by @user4815162342)

In short - the last downside of having two different .default() methods in one type is dangerous in generic methods with T: Default arguments, because in this case will be called Default::default(), which initializes the day field to None. The worst part of this effect, is that compiler won't ever warn you about it, thus forcing you to spend your time debugging in case of a bug.

There's one similar approach suggested by @ÖmerErden, where you can again wrap the date into another type, to which you implement Default on your own. This will ensure that your field will always be initialized, but still forces you to somehow "unwrap" the value. In case of wrapping NaiveDate into a tuple struct, you can unwrap as simply as instance.day.0 or implement Deref to this wrapper and unwrap with *instance.day

use chrono;
use std::ops::Deref;

#[derive(Debug)]
struct NaiveDateWrapper(chrono::NaiveDate);

impl Default for NaiveDateWrapper {
    fn default() -> Self {
        Self(chrono::NaiveDate::from_ymd(2021, 1, 1))
    }
}

impl Deref for NaiveDateWrapper {
    type Target = chrono::NaiveDate;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[derive(Debug, Default)]
struct StructA {
  value1: i32,
  value2: i32,
  value19: i32,
  day: NaiveDateWrapper,
}

fn main() {
    let v = StructA::default();
    println!("{:?}", v.day.0);  
    println!("{:?}", *v.day);
}
like image 35
Alexey Larionov Avatar answered Oct 22 '22 05:10

Alexey Larionov