Box around traits
Box
is very useful for returning traits. You know that you can write traits in generic functions like in this example:
use std::fmt::Display; struct DoesntImplementDisplay {} fn displays_it<T: Display>(input: T) { println!("{}", input); } fn main() {}
This only takes something with Display
, so it can't accept our struct DoesntImplementDisplay
. But it can take in a lot of others like String
.
You also saw that we can use impl Trait
to return other traits, or closures. Box
can be used in a similar way. You can use a Box
because otherwise the compiler won't know the size of the value. This example shows that a trait can be used on something of any size:
#![allow(dead_code)] // Tell the compiler to be quiet use std::mem::size_of; // This gives the size of a type trait JustATrait {} // We will implement this on everything enum EnumOfNumbers { I8(i8), AnotherI8(i8), OneMoreI8(i8), } impl JustATrait for EnumOfNumbers {} struct StructOfNumbers { an_i8: i8, another_i8: i8, one_more_i8: i8, } impl JustATrait for StructOfNumbers {} enum EnumOfOtherTypes { I8(i8), AnotherI8(i8), Collection(Vec<String>), } impl JustATrait for EnumOfOtherTypes {} struct StructOfOtherTypes { an_i8: i8, another_i8: i8, a_collection: Vec<String>, } impl JustATrait for StructOfOtherTypes {} struct ArrayAndI8 { array: [i8; 1000], // This one will be very large an_i8: i8, in_u8: u8, } impl JustATrait for ArrayAndI8 {} fn main() { println!( "{}, {}, {}, {}, {}", size_of::<EnumOfNumbers>(), size_of::<StructOfNumbers>(), size_of::<EnumOfOtherTypes>(), size_of::<StructOfOtherTypes>(), size_of::<ArrayAndI8>(), ); }
When we print the size of these, we get 2, 3, 32, 32, 1002
. So if you were to do this, it would give an error:
#![allow(unused)] fn main() { // ⚠️ fn returns_just_a_trait() -> JustATrait { let some_enum = EnumOfNumbers::I8(8); some_enum } }
It says:
error[E0746]: return type cannot have an unboxed trait object
--> src\main.rs:53:30
|
53 | fn returns_just_a_trait() -> JustATrait {
| ^^^^^^^^^^ doesn't have a size known at compile-time
And this is true, because the size could be 2, 3, 32, 1002, or anything else. So we put it in a Box
instead. Here we also add the keyword dyn
. dyn
is a word that shows you that you are talking about a trait, not a struct or anything else.
So you can change the function to this:
#![allow(unused)] fn main() { // 🚧 fn returns_just_a_trait() -> Box<dyn JustATrait> { let some_enum = EnumOfNumbers::I8(8); Box::new(some_enum) } }
And now it works, because on the stack is just a Box
and we know the size of Box
.
You see this a lot in the form Box<dyn Error>
, because sometimes you can have more than one possible error.
We can quickly create two error types to show this. To make an official error type, you have to implement std::error::Error
for it. That part is easy: just write impl std::error::Error {}
. But errors also need Debug
and Display
so they can give information on the problem. Debug
is easy with #[derive(Debug)]
but Display
needs the .fmt()
method. We did this once before.
The code looks like this:
use std::error::Error; use std::fmt; #[derive(Debug)] struct ErrorOne; impl Error for ErrorOne {} // Now it is an error type with Debug. Time for Display: impl fmt::Display for ErrorOne { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "You got the first error!") // All it does is write this message } } #[derive(Debug)] // Do the same thing with ErrorTwo struct ErrorTwo; impl Error for ErrorTwo {} impl fmt::Display for ErrorTwo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "You got the second error!") } } // Make a function that just returns a String or an error fn returns_errors(input: u8) -> Result<String, Box<dyn Error>> { // With Box<dyn Error> you can return anything that has the Error trait match input { 0 => Err(Box::new(ErrorOne)), // Don't forget to put it in a box 1 => Err(Box::new(ErrorTwo)), _ => Ok("Looks fine to me".to_string()), // This is the success type } } fn main() { let vec_of_u8s = vec![0_u8, 1, 80]; // Three numbers to try out for number in vec_of_u8s { match returns_errors(number) { Ok(input) => println!("{}", input), Err(message) => println!("{}", message), } } }
This will print:
You got the first error!
You got the second error!
Looks fine to me
If we didn't have a Box<dyn Error>
and wrote this, we would have a problem:
#![allow(unused)] fn main() { // ⚠️ fn returns_errors(input: u8) -> Result<String, Error> { match input { 0 => Err(ErrorOne), 1 => Err(ErrorTwo), _ => Ok("Looks fine to me".to_string()), } } }
It will tell you:
21 | fn returns_errors(input: u8) -> Result<String, Error> {
| ^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
This is not surprising, because we know that a trait can work on many things, and they each have different sizes.