Default
traitDeref
and DerefMut
to steal the methods of other types to use in your ownThis 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.
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}'"); }
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
:
Default
is a trait, so if you implement Default
, you can pass your type into anything that requires it. Sometimes, you will come across functions or traits that require Default
to be implemented, such as the .unwrap_or_default()
method.
Your type might need to be a parameter in another struct or enum that wants to implement Default
. To implement Default
using #[derive(Default)]
, all of a type’s parameters need to implement it, too.
Having Default
gives users of your types a general idea of how to use them. For example, you might want to have a method called new()
or create()
to make a type with lots of customization. But you could also implement Default
so the user can just create one without thinking about all the settings.
Default is really convenient when working with parameters in a struct.
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:?}"); }
② For the rest, uses their default values. Typing ..
means “for each remaining parameter.”
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.
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.
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.
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.
⑥ 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.
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.
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.
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.
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 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); }
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.
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(); ② }
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.
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; }
pub trait DerefMut: Deref { fn deref_mut(&mut self) -> &mut Self::Target; }
Deref
has an associated type. DerefMut
looks like it doesn’t involve an associated type, but note that it says DerefMut: Deref
. That means that you need Deref
to implement DerefMut
, so anything that implements DerefMut
will have the associated type Self::Target
. So you don’t need to declare the associated type again for DerefMut
; it’s already there.
That’s why you see &mut Self::Target
as the output for the deref_mut()
method. If you see an associated type in a signature without an associated type in the trait, check to see whether another required trait made the associated type.
The function signatures are exactly the same, except they are mutable versions. We have &mut self
instead of &self, deref_mut()
instead of deref()
, and &mut Self::Target
instead of &Self::Target
.
In other words, to implement DerefMut
after Deref
, you copy and paste the Deref
implementation, delete the first line, and add a bunch of mut
s 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();
}
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.
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.
Implementing Default
for your types has some nice benefits. Among other benefits, it makes your code cleaner and lets your type be used wherever there is a Default
trait bound.
The builder pattern has a lot of flexibility. You can use it just because you like the syntax, or you can use it to give a lot of control over how your types are used.
Making a separate type that can only be used as a builder to turn into another type is a great way to make sure that your types don’t get misused.
With Deref
and DerefMut
, you can make your own types that have access to the methods of other types they hold.
Implementing DerefMut
after Deref
is easy: simply copy and paste the code, remove the line with the associated type, and add the word mut
everywhere.
Deref
is best used for simple types like smart pointers. Using it for more complex types can make your code difficult to understand.