Deref and DerefMut
Deref
is the trait that lets you use *
to dereference something. We saw the word Deref
before when using a tuple struct to make a new type, and now it's time to learn it.
We know that a reference is not the same as a value:
// ⚠️ fn main() { let value = 7; // This is an i32 let reference = &7; // This is a &i32 println!("{}", value == reference); }
And Rust won't even give a false
because it won't even compare the two.
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src\main.rs:4:26
|
4 | println!("{}", value == reference);
| ^^ no implementation for `{integer} == &{integer}`
Of course, the solution here is *
. So this will print true
:
fn main() { let value = 7; let reference = &7; println!("{}", value == *reference); }
Now let's imagine a simple type that just holds a number. It will be like a Box
, and we have some ideas for some extra functions for it. But if we just give it a number, it won't be able to do much with it.
We can't use *
like we can with Box
:
// ⚠️ struct HoldsANumber(u8); fn main() { let my_number = HoldsANumber(20); println!("{}", *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 adding a separate u8
to the 20. It would be nice if we could just add them together. 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, has information about it, and can use its methods. Because 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
.
Interesting fact: String
is actually a smart pointer to &str
and Vec
is a smart pointer to array (or other types). So we have actually been using smart pointers since the beginning.
Implementing Deref
is not too hard and the examples in the standard library are easy. Here's the sample code from the standard library:
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); }
So we follow that and now our Deref
looks like this:
#![allow(unused)] fn main() { // 🚧 impl Deref for HoldsANumber { type Target = u8; // Remember, this is the "associated type": the type that goes together. // You have to use the right type Target = (the type you want to return) fn deref(&self) -> &Self::Target { // Rust calls .deref() when you use *. We just defined Target as a u8 so this is easy to understand &self.0 // We chose &self.0 because it's a tuple struct. In a named struct it would be something like "&self.number" } } }
So now we can do this with *
:
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); }
So that will print 40
and we didn't need to write my_number.0
. That means we get the methods of u8
and we can write our own methods for HoldsANumber
. We will add our own simple method 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 then it gives it to you inside Some
, and if it can't do it then it gives 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)); // This method comes from u8 my_number.prints_the_number_times_two(); // This is our own method }
This prints:
None
40
We can also implement DerefMut
so we can change the values through *
. It looks almost the same. You need Deref
before you can implement DerefMut
.
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 { // You don't need type Target = u8; here because it already knows thanks to Deref fn deref_mut(&mut self) -> &mut Self::Target { // Everything else is the same except it says mut everywhere &mut self.0 } } fn main() { let mut my_number = HoldsANumber(20); *my_number = 30; // DerefMut lets us do this println!("{:?}", my_number.checked_sub(100)); my_number.prints_the_number_times_two(); }
So you can see that Deref
gives your type a lot of power.
This is also why the standard library 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 complicated type. Let's imagine a really confusing example to understand what they mean. We'll start with Character
struct for a game. A new Character
needs some stats like intelligence and strength. So here is our first character:
struct Character { name: String, strength: u8, dexterity: u8, health: u8, intelligence: u8, wisdom: u8, charm: u8, hit_points: i8, alignment: Alignment, } impl Character { fn new( name: String, strength: u8, dexterity: u8, health: u8, intelligence: u8, wisdom: u8, charm: u8, hit_points: i8, alignment: Alignment, ) -> Self { Self { name, strength, dexterity, health, intelligence, wisdom, charm, hit_points, alignment, } } } enum Alignment { Good, Neutral, Evil, } fn main() { let billy = Character::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good); }
Now let's imagine that we want to keep character hit points in a big vec. Maybe we'll put monster data in there too, and keep it all together. Since hit_points
is an i8
, we implement Deref
so we can do all sorts of math on it. But look at how strange it looks in our main()
function now:
use std::ops::Deref; // All the other code is the same until after the enum Alignment struct Character { name: String, strength: u8, dexterity: u8, health: u8, intelligence: u8, wisdom: u8, charm: u8, hit_points: i8, alignment: Alignment, } impl Character { fn new( name: String, strength: u8, dexterity: u8, health: u8, intelligence: u8, wisdom: u8, charm: u8, hit_points: i8, alignment: Alignment, ) -> Self { Self { name, strength, dexterity, health, intelligence, wisdom, charm, hit_points, alignment, } } } enum Alignment { Good, Neutral, Evil, } impl Deref for Character { // impl Deref for Character. Now we can do any integer math we want! type Target = i8; fn deref(&self) -> &Self::Target { &self.hit_points } } fn main() { let billy = Character::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good); // Create two characters, billy and brandy let brandy = Character::new("Brandy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good); let mut hit_points_vec = vec![]; // Put our hit points data in here hit_points_vec.push(*billy); // Push *billy? hit_points_vec.push(*brandy); // Push *brandy? println!("{:?}", hit_points_vec); }
This just prints [5, 5]
. Our code is now very strange for someone to read. 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 code is 2000 lines long, and suddenly we have to figure 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)
, but it makes the code look very strange. Probably a simple .get_hp()
method would be much better, or another struct that holds the characters. Then you could iterate through and push the hit_points
for each one. Deref
gives a lot of power but it's good to make sure that the code is logical.