Box around traits

pull/47/head
Dhghomon 4 years ago committed by GitHub
parent b86d019b42
commit c0cf28ba3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7918,6 +7918,200 @@ fn main() {
Even without data it is a bit complicated, and Rust does not use this type of pattern very much. This is because Rust has strict rules on borrowing and ownership, as you know. But if you want to start a list like this (a linked list), `Box` can help.
## Box around traits
`Box` is very useful for returning traits. You know that you can write traits in generic functions like in this example:
```rust
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 a value. This example shows that a trait can be used on something of any size:
```rust
#![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:
```rust
// ⚠️
fn returns_just_a_trait() -> JustATrait {
let some_enum = EnumOfNumbers::I8(8);
some_enum
}
```
It says:
```text
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:
```rust
// 🚧
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:
```rust
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:
```text
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:
```rust
// ⚠️
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:
```text
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.
## Default and the builder pattern
You can implement a trait called `Default` that will give values to a `struct` or `enum` that you think will be most common. The builder pattern works nicely with this to let users easily make changes when they want. First let's look at `Default`. Actually, most general types in Rust already have `Default`, and they are not surprising: 0, empty strings, `false`, etc.

Loading…
Cancel
Save