15 Default, the builder pattern, and Deref

This chapter covers

This chapter is a fun one. You’ll learn the builder pattern, which lets you declare variables by chaining method after method instead of writing all the parameters for a struct. It’s especially good for writing code that other people might use because you can control which parts they can touch and which they can’t. The Deref trait that you’ll learn later in the chapter lets you make your own types that hold all the methods of another type for free. This allows you to easily make types that hold someone else’s type inside, to which you can add your own methods on top.

15.1 Implementing Default

You can implement the Default trait to give values to a struct or enum that you think will be most common or represent the type’s base state. The builder pattern in the next section works nicely with this to let users easily make any changes after starting with default values.

Most frequently used types in the Rust standard library already implement Default. You can see which types implement Default in the documentation (http://mng.bz/yZgd) if you are curious. Default values are not surprising, such as 0, "" (empty strings), false, and so on, which makes sense (you wouldn’t want defaults to be something like "Smurf" for String or some random number like 576 for an i32!). We can see some default values in a quick example:

fn main() {
    let default_i8: i8 = Default::default();
    let default_str: String = Default::default();
    let default_bool: bool = Default::default();
 
    println!("'{default_i8}', '{default_str}', '{default_bool}'");
}

This prints '0', '', 'false'.

So Default is sort of like a new() method that can’t take any arguments. Let’s try it with our own type. First, we will make a struct that doesn’t implement Default yet. It has a new function, which we use to make a character named Billy with some stats:

struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}
 
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain
}
 
impl Character {
    fn new(name: String, age: u8, height: u32, weight: u32, alive: bool) ->
    Self {
        Self {
            name,
            age,
            height,
            weight,
            lifestate: if alive {
                LifeState::Alive
            } else {
                LifeState::Dead
            },
        }
    }
}
 
fn main() {
    let character_1 = Character::new("Billy".to_string(), 15, 170, 70, true);
}

But maybe in our world, we want most of the characters to be named Billy, age 15, height 170, weight 70, and alive. We can implement Default so that we can just write Character::default() and won’t need to enter any arguments. It looks like this:

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}
 
#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}
 
impl Default for Character {
    fn default() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
        }
    }
}
 
fn main() {
    let character_1 = Character::default();
 
    println!(
        "The character {:?} is {:?} years old.",
        character_1.name, character_1.age
    );
}

It prints The character "Billy" is 15 years old. Much easier!

But not having to enter arguments isn’t the main reason for implementing Default. After all, you could just come up with any other function that returns a Character with these parameters. So why implement Default instead of writing a new() or some other function? Here are a few good reasons why you might want to implement Default:

This last point is easiest to explain using an example. Consider this simple struct:

#[derive(Default)]
struct Size {
    height: f64,
    length: f64,
    width: f64,
}

Each of the struct’s parameters is f64, which implements Default, so we can easily use #[derive(Default)] for it, too. That lets us write Size::default() if we want each parameter to be 0.0, but it also lets us do something like this:

#[derive(Debug, Default)]
struct Size {
    height: f64,
    length: f64,
    width: f64,
}
 
fn main() {
    let only_height = Size {
        height: 1.0,            
        ..Default::default()    
    };
    println!("{only_height:?}");
}

Makes height 1.0

For the rest, uses their default values. Typing .. means “for each remaining parameter.”

The output is

Size { height: 1.0, length: 0.0, width: 0.0 }

You also see Default a lot in a pattern known as the builder pattern, which we will take a look at now.

15.2 The builder pattern

The builder pattern is an interesting way to build a type (usually a struct). Some people like this pattern because it is quite readable, as it lets you chain method after method for all the parameters you want to change. For example, if we used the builder pattern on the Size struct we just looked at, it might look something like this, which is quite readable:

let my_size = Size::default().height(1.0).width(5.0);

The readability comes from being pretty close to how you would explain this in regular conversation: “Make a struct Size called my_size with default values but change height to 1.0 and width to 5.0.”

But the builder pattern isn’t just for readable syntax: it also gives you more control over how other people use your types. Generally, the builder pattern makes the most sense when you have a type with a lot of fields, most of which are default values. A good example would be a database client with a lot of fields like username, password, connect_timeout, port_address, and so on. In most cases, a user will prefer default values, but the builder pattern allows some of these values to be changed when necessary.

To keep our examples short, though, we’ll keep using the previous Character struct whose default name was Billy, so we will start with that as the default as we learn this pattern. As before, most of our characters will be named Billy, but we also want to give people the option to make some changes. Let’s learn how to do that.

15.2.1 Writing builder methods

Let’s imagine that we have a Character struct and would like to type .height() after declaring it to change the height. How would we do that? One way is to take the whole struct by value, change one value, and pass it back. In other words, each builder method will return Self. Here is what it would look like:

fn height(mut self, height: u32) -> Self {
    self.height = height;
    self
}

Notice that it takes a mut self, which is an owned self—not a mutable reference (&mut self). It takes ownership of Self, and with mut, it will be mutable, even if it wasn’t mutable before. That’s because .height() has full ownership, and nobody else can touch it, so it is safe to be mutable. Then the method just changes self.height and returns Self (which, in this case, is Character).

Let’s have three of these builder methods. They are exceptionally easy to write. Just take a mut self and a value, change a parameter to the value, and return self:

fn height(mut self, height: u32) -> Self {
    self.height = height;
    self
}
 
fn weight(mut self, weight: u32) -> Self {
    self.weight = weight;
    self
}
 
fn name(mut self, name: &str) -> Self {
    self.name = name.to_string();
    self
}

Because each of these methods gives a Self back, we can now chain methods to write something like this to make a character:

let character_1 = Character::default().height(180).weight(60).name("Bobby");

So far, our code looks like this:

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
}
 
#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}
 
impl Character {
    fn height(mut self, height: u32) -> Self {
        self.height = height;
        self
    }
 
    fn weight(mut self, weight: u32) -> Self {
        self.weight = weight;
        self
    }
 
    fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self
    }
}
 
impl Default for Character {
    fn default() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
        }
    }
}
 
fn main() {
    let character_1 = Character::default().height(180).weight(60).name("Bobby");
    println!("{character_1:?}");
}

This prints Character { name: "Bobby", age: 15, height: 180, weight: 60, lifestate: Alive }.

That’s the first part of the builder pattern, but what about the part about giving you greater control over how people use your types? At the moment, height is a u32, so nothing is stopping people from making a character with a height up to 4294967295 (the highest possible number for u32). Let’s think about how to keep people from doing that.

15.2.2 Adding a final check to the builder pattern

One last method to add in the builder pattern is usually called .build(). This method is a sort of final check. When you give a user a method like .height() you can make sure that they only put in a u32, but what if they enter 5000 for height? That might not be okay in the game you are making. For our final .build() method, we will have it return a Result. Inside the method, we will check whether the user input is okay, and if it is, we will return an Ok(Self).

This raises a question: How do we force a user to use this .build() method? Right now, a user can write let x = Character::new().height(76767); and get a Character. There are many ways to do this. First, let’s look at a quick and dirty method. We’ll add a can_use: bool value to Character:

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
    can_use: bool,          
}

Sets whether the user can use the character

Next, skipping over the code in between, the implementation for Default will now look like this:

impl Default for Character {
    fn default() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
            can_use: true,                 
        }
    }
}

Default::()default() always gives a good character, so it’s true.

For the other methods, like .height(), we will set can_use to false. Only .build() will set it to true again, so now the user has to do a final check with .build(). We will make sure that height is not above 200 and weight is not above 300. Also, in our game, there is a bad word called smurf that we don’t want characters to use.

Our .build() method looks like this:

fn build(mut self) -> Result<Character, String> {
    if self.height < 200
        && self.weight < 300
        && !self.name.to_lowercase().contains("smurf")
    {
        self.can_use = true;
        Ok(self)
    } else {
        Err("Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)"
            .to_string())
    }
}

Using !self.name.to_lowercase().contains("smurf") makes sure that the user doesn’t write something like "SMURF" or "IamSmurf". It makes the whole String lowercase (small letters) and checks for .contains() instead of ==.

If everything is okay, we set can_use to true and give the character to the user inside Ok.

Now that our code is done, we will create three characters that don’t work and one character that does work. The code now looks like this:

#[derive(Debug)]
struct Character {
    name: String,
    age: u8,
    height: u32,
    weight: u32,
    lifestate: LifeState,
    can_use: bool,
}
 
#[derive(Debug)]
enum LifeState {
    Alive,
    Dead,
    NeverAlive,
    Uncertain,
}
 
impl Default for Character {
    fn default() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
            height: 170,
            weight: 70,
            lifestate: LifeState::Alive,
            can_use: true,
        }
    }
}
 
impl Character {
 
    fn height(mut self, height: u32) -> Self {
        self.height = height;
        self.can_use = false;                               
        self
    }
 
    fn weight(mut self, weight: u32) -> Self {
        self.weight = weight;
        self.can_use = false;
        self
    }
 
    fn name(mut self, name: &str) -> Self {
        self.name = name.to_string();
        self.can_use = false;
        self
    }
 
    fn build(mut self) -> Result<Character, String> {
  if self.height < 200
        && self.weight < 300
        && !self.name.to_lowercase().contains("smurf")
            self.can_use = true;                            
            Ok(self)
        } else {
            Err("Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)"
                .to_string())
        }
    }
}
 
    let character_with_smurf = Character::default()
        .name("Lol I am Smurf!!").build();                  
    let character_too_tall = Character::default()           
        .height(400)
        .build();
    let character_too_heavy = Character::default()          
        .weight(500)
        .build();
    let okay_character = Character::default()
        .name("Billybrobby")
        .height(180)
        .weight(100)
        .build();                                           
 
    let character_vec = vec![                               
        character_with_smurf,
        character_too_tall,
        character_too_heavy,
        okay_character,
    ];
 
    for character in character_vec {
        match character {
            Ok(character) => println!("{character:?}\n"),
            Err(err_info) => println!("{err_info}\n"),
        }
    }
}

Set this to false every time a parameter changes.

At this point, everything is okay, so set it to true and return the character.

This one contains "smurf"—not okay.

Too tall—not okay

Too heavy—not okay

This character is okay. Name is fine; height and weight are fine.

Each of these is a Result<Character, String>. Let’s put them in a Vec so we can see them.

This will print

Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)
 
Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)
 
Could not create character. Characters must have:
1) Height below 200
2) Weight below 300
3) A name that is not Smurf (that is a bad word)
 
Character { name: "Billybrobby", age: 15, height: 180, weight: 100,
lifestate: Alive, can_use: true }

So that’s not bad as long as our code checks whether can_use is true or not. But what if we are writing a library for other people to use? We can’t force them to check can_use, so we can’t keep them from making a Character that is wrong. Is there a way to not even generate a Character struct in the first place if it shouldn’t be built? Let’s look at that pattern now.

15.2.3 Making the builder pattern more rigorous

The main way to make sure nobody can generate a Character struct on their own is to start with a different type. This type will look similar, but can’t be used anywhere—it can only be used to turn into a Character if the parameters are okay. We’ll call it CharacterBuilder. Any functions that take a Character require a Character and nothing else, so even though CharacterBuilder has the same properties, it’s not the same type. And to turn a CharacterBuilder into a Character, we’ll make a method called .try_build().

To make this last example more readable, let’s simplify the Character struct. It might look something like this:

#[derive(Debug)]                                                   
pub struct Character {                                             
    name: String,                                                  
    age: u8,                                                       
}
impl Default for Character {
    fn default() -> Self {
        Self {
            name: "Billy".to_string(),
            age: 15,
        }
    }
}
#[derive(Debug)]
pub struct CharacterBuilder {
    pub name: String,
    pub age: u8,
}
 
impl CharacterBuilder {
    fn new(name: String, age: u8) -> Self {                         
        Self { name, age }
    }
 
    fn try_build(self) -> Result<Character, &'static str> {         
        if !self.name.to_lowercase().contains("smurf") {
            Ok(Character {
                name: self.name,
                age: self.age,
            })
        } else {
            Err("Can't make a character with the word 'smurf' inside it!")
        }
    }
}
 
fn do_something_with_character(character: &Character) {}            
 
fn main() {
    let default_character = Character::default();
    do_something_with_character(&default_character);
    let second_character = CharacterBuilder::new("Bobby".to_string(), 27)
        .try_build()
        .unwrap();
    do_something_with_character(&second_character);
    let bad_character = CharacterBuilder::new("Smurfysmurf".to_string(), 40)
    .try_build();
    println!("{bad_character:?}");
    // do_something_with_character(&bad_character);                  
}

This is fine because we control both name and age. We know that these two parameters are acceptable.

This returns a CharacterBuilder, so we can give the user full control over the parameters. A CharacterBuilder on its own is useless except to try to turn into a Character.

A proper error type would be nice here, but we’ll keep it simple for now and return a &’static str for the Err case.

This function does nothing yet; it only accepts a Character, not a CharacterBuilder.

This bad_character variable is a Result::Err. It failed to turn into a Character, so it can’t be used in this function.

In this case, everything works out except bad_character. We didn’t unwrap it, but it looks like this: Err("Can't make a character with the word 'smurf' inside it!").

By now, we should have a pretty good idea of how to use the builder pattern. What’s most interesting about this pattern is not that you can use slick names like .name() but that it makes you think about how others will use your types. Starting with Default and then adding these small methods makes it really easy to predict how people will use your types because you have complete control over them.

Up next, we will learn the Deref trait, which lets you make your own types that you control and also have quick access to the methods in other people’s types.

15.3 Deref and DerefMut

Way back in chapter 7, we saw the word Deref when learning the newtype pattern. Here is the tuple struct we used to make a new type:

struct File(String);
 
fn main() {
    let my_file = File(String::from("I am file contents"));
    let my_string = String::from("I am file contents");
}

We noted that the File struct holds a String, but it can’t use any of String’s methods. If you are just writing a bit of code in a single file, then you can, of course, use .0 to access the String inside. But if File is inside another mod and isn’t written struct File(pub String);, you won’t be able to use .0 to access String, and you won’t be able to use any of String’s methods. This is where the Deref trait comes in, so let’s take a look at how that works.

15.3.1 Deref basics

Deref is the trait that lets you use * to dereference something, which we learned pretty early on in the book. For example, we know that a reference is not the same as a value:

fn main() {
    let value = 7;         
    let reference = &7;    
    println!("{}", value == reference);
}

This is an i32.

This is a &i32.

This code doesn’t even return false because Rust refuses to even compare the two—they are different types:

error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src\main.rs:4:26
  |
4 |     println!("{}", value == reference);
  |                          ^^ no implementation for `{integer} ==
  &{integer}`

As we saw before, the solution here is to use * to dereference. Now, this will print true:

fn main() {
    let value = 7;
    let reference = &7;
    println!("{}", value == *reference);
}

Now, let’s imagine a simple type that only holds a number. It would be nice if we could use it like a Box by using * to dereference, and we have some ideas for some extra functions for it. But there isn’t much we can do yet with a struct that only holds a number.

For example, we can’t use * as we could with Box:

struct HoldsANumber(u8);
 
fn main() {
    let boxed_number = Box::new(20);
    println!("This works fine: {}", *boxed_number);
    let my_number = HoldsANumber(20);
    println!("This fails though: {}", *my_number + 20);
}

The error is

error[E0614]: type `HoldsANumber` cannot be dereferenced
  --> src\main.rs:24:22
   |
24 |     println!("{:?}", *my_number + 20);

We can, of course, do this: println!("{:?}", my number.0 + 20);. But then we are just manually adding the u8 to the 20. Plus, it is likely that we don’t want to make the u8 inside it pub when other people use our code. It would be nice if we could add them together somehow.

The message cannot be dereferenced gives us a clue: we need to implement Deref. Something simple that implements Deref is sometimes called a “smart pointer.” A smart pointer can point to its item, might have information about it (metadata; one example of metadata in a smart pointer is Vec, which holds information on its length), and can use its methods. Right now, we can add my_number.0, which is a u8, but we can’t do much else with a HoldsANumber—all it has so far is Debug.

Interestingly, String is a smart pointer to &str, and Vec is a smart pointer to array (or other types). Box, Rc, RefCell, and so on are smart pointers too. So, we have actually been using smart pointers all this time. Let’s implement Deref now and make our HoldsANumber struct into a smart pointer, too.

15.3.2 Implementing Deref

Implementing Deref is not too hard, and the examples in the standard library are easy. Let’s take a look at the sample code from the standard library (http://mng.bz/M94B):

use std::ops::Deref;
 
struct DerefExample<T> {
    value: T
}
 
impl<T> Deref for DerefExample<T> {
    type Target = T;
 
    fn deref(&self) -> &Self::Target {
        &self.value
    }
}
 
fn main() {
    let x = DerefExample { value: 'a' };
    assert_eq!('a', *x);
}

We can follow this code and change it to fit our HoldsANumber type. With Deref, it now looks like this:

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

Remember, this is the associated type—a type that goes together with a trait. The return value is Self::Target, which we decided will be a u8.

Rust calls .deref() when you use * or use the dot operator when using a method. We just defined Target as a u8, so this &Self::Target is easy to understand: it’s a reference to a u8. If Self::Target is a u8, then &Self::Target is a &u8.

We chose &self.0 because it’s a tuple struct. In a named struct, it would be something like &self.number.

With these changes, we can now use the * operator:

use std::ops::Deref;
#[derive(Debug)]
struct HoldsANumber(u8);
 
impl Deref for HoldsANumber {
    type Target = u8;
 
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
 
fn main() {
    let my_number = HoldsANumber(20);
    println!("{:?}", *my_number + 20);
}

That will print 40 without us needing to write my_number.0.

And here’s the interesting part: Deref gives us access to the methods of u8, and on top of that, we can write our own methods for HoldsANumber. Let’s write our own simple method for HoldsANumber and use another method we get from u8 called .checked_sub(). The .checked_sub() method is a safe subtraction that returns an Option. If it can do the subtraction within the bounds of a number, it returns the value inside Some, and if it can’t do it, it returns a None. Remember, a u8 can’t be negative, so it’s safer to do .checked_sub() so we don’t panic:

use std::ops::Deref;
 
struct HoldsANumber(u8);
 
impl HoldsANumber {
    fn prints_the_number_times_two(&self) {
        println!("{}", self.0 * 2);
    }
}
 
impl Deref for HoldsANumber {
    type Target = u8;
 
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
 
fn main() {
    let my_number = HoldsANumber(20);
    println!("{:?}", my_number.checked_sub(100));   
    my_number.prints_the_number_times_two();        
}

A method from u8

Our own method

This prints

None
40

Deref alone doesn’t give mutable access to the inner type, though, so this won’t work:

use std::ops::Deref;
 
struct HoldsANumber(u8);
 
impl Deref for HoldsANumber {
    type Target = u8;
 
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
 
fn main() {
    let mut my_number = HoldsANumber(20);
    *my_number = 30;
}

Here, we try to dereference and turn the number inside from 20 to 30, but the compiler won’t let us:

error[E0594]: cannot assign to data in dereference of `HoldsANumber`
  --> src/main.rs:21:5
   |
21 |     *my_number = 30;
   |     ^^^^^^^^^^^^^^^ cannot assign
   |
   = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `HoldsANumber`

But no problem! Implementing DerefMut after we’ve already implemented Deref is incredibly easy. Let’s do that now.

15.3.3 Implementing DerefMut

We can also implement DerefMut if we need mutable access, but you need Deref before you can implement DerefMut, as the signature shows:

pub trait DerefMut: Deref

The signatures for Deref and DerefMut are very similar, so let’s compare the two and see which parts are different. First, Deref:

pub trait Deref {
    type Target: ?Sized;
 
    fn deref(&self) -> &Self::Target;
}

Then DerefMut:

pub trait DerefMut: Deref {
    fn deref_mut(&mut self) -> &mut Self::Target;
}

Here are some items to note:

In other words, to implement DerefMut after Deref, you copy and paste the Deref implementation, delete the first line, and add a bunch of muts everywhere.

Knowing this, we can now implement both Deref and DerefMut for our HoldsANumber:

use std::ops::{Deref, DerefMut};
 
struct HoldsANumber(u8);
 
impl HoldsANumber {
    fn prints_the_number_times_two(&self) {
        println!("{}", self.0 * 2);
    }
}
 
impl Deref for HoldsANumber {
    type Target = u8;
 
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
 
impl DerefMut for HoldsANumber {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}
 
fn main() {
    let mut my_number = HoldsANumber(20);
    *my_number = 30;                               
    println!("{:?}", my_number.checked_sub(100));
    my_number.prints_the_number_times_two();
}

DerefMut lets us do this.

You can see that Deref gives your type a lot of power. Just implement Deref, and you get all the methods for the type inside!

Probably the most common use for Deref in everyday code is when you want type safety. Let’s say you have an Email type that is an Email(String) or a Quantity type that is a Quantity(u32). If you implement Deref, you get the methods of the type inside. But at the same time, nobody can just use a String and a u32 where your function calls for an Email or a Quantity because they are not the same type.

After reading this, Deref might now be your favorite new trait. It’s best to use Deref only when it makes sense, though. Let’s see why.

15.3.4 Using Deref the wrong way

The standard library has a strong recommendation on how Deref should be used, which says, Deref should only be implemented for smart pointers to avoid confusion. That’s because you can do some strange things with Deref for a type that doesn’t really have any relation with what it dereferences to. (Well, the compiler won’t consider it strange, but anyone reading the code will!)

Let’s try to imagine the worst possible way to use Deref to understand what they mean. We’ll start with Character struct for a game. A new Character needs some stats like intelligence and strength. Here is our first character:

struct Character {
    name: String,
    strength: u8,
    dexterity: u8,
    intelligence: u8,
    hit_points: i8,
}
 
impl Character {
    fn new(
        name: String,
        strength: u8,
        dexterity: u8,
        intelligence: u8,
        hit_points: i8,
    ) -> Self {
        Self {
            name,
            strength,
            dexterity,
            intelligence,
            hit_points,
        }
    }
}
 
fn main() {
    let billy = Character::new("Billy".to_string(), 9, 12, 7, 10);
}

Now, let’s imagine that we’d like to modify the character’s hit points when they get hit (the hit points will go down) or when they heal (like when they drink a potion; the hit points will go up). And maybe we’d like to keep character hit points in a big Vec. Maybe we’ll put monster data in there, too, and keep it all together and do some calculations later. Since hit_points is an i8, we implement Deref so we can do all sorts of math on it. And to change the hit points, we’ll implement DerefMut, too. But look at how strange it looks in our main() function now:

use std::ops::{Deref, DerefMut};
 
struct Character {
    name: String,
    strength: u8,
    dexterity: u8,
    intelligence: u8,
    hit_points: i8,
}
 
impl Character {
    fn new(
        name: String,
        strength: u8,
        dexterity: u8,
        intelligence: u8,
        hit_points: i8,
    ) -> Self {
        Self {
            name,
            strength,
            dexterity,
            intelligence,
            hit_points,
        }
    }
}
 
impl Deref for Character {               
    type Target = i8;
 
    fn deref(&self) -> &Self::Target {
        &self.hit_points
    }
}
 
impl DerefMut for Character {
 
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.hit_points
    }
}
 
fn main() {                              
    let mut billy = Character::new("Billy".to_string(), 9, 12, 7, 10);
    let mut brandy = Character::new("Brandy".to_string(), 10, 8, 9, 10);
    
    *billy -= 10;                        
    *brandy += 1;
 
    let mut hit_points_vec = vec![];     
    hit_points_vec.push(*billy);
    hit_points_vec.push(*brandy);
}

With impl Deref for Character, we can do any integer math we want on their hit points! And with DerefMut, we can change their hit points, too.

We’ll start main() by creating two characters.

Changes their hit points. It’s starting to look weird.

Starts our hit points analysis. We push *billy and *brandy into the Vec. Or, rather, we push their hit points in.

Our code is now very strange for someone to read. Can a reader of the code understand what happens when you -= 10 on a Character? And how could anyone know that using .push() on a Character struct is pushing an i8? You’d have to go to the Deref implementation to see what’s going on.

We can read Deref just above main() and figure out that *billy means i8, but what if there was a lot of code? Maybe our program is 2,000 lines long, and we have to do a lot of searching through the code to find out why we are .push()ing *billy. Character is certainly more than just a smart pointer for i8.

Of course, it is not illegal to write hit_points_vec.push(*billy), and the compiler is happy to run this code, but it makes the code look weird. A simple .get_hp() or .change_hp() method would be much better. Deref gives a lot of power, but it’s good to make sure that the code is logical.

Hopefully, this chapter has given you a lot of ideas for how to put your types together. Rust’s rich type system and traits like Default and Deref give you a lot of options and a great deal of control. The builder pattern we learned is not a built-in Rust type or trait but is commonly used for all the good reasons we saw in this chapter. In the next chapter, we will learn the final type of Rust generics called const generics and begin looking at external crates (code written by others for us to use). We will also look at unsafe Rust, a type of Rust that you may never need to use but that exists for very good reasons. Unsafe Rust also serves as a good reminder of why Rust was created in the first place.

Summary