We're going to create a catalog that can store books, movies and audiobooks.
We have options on how to represent something:
Enums in Rust are a little different than enums in other languages as we will see.
#[derive(Debug)] enum Media { Book { title: String, author: String }, Movie { title: String, director: String }, Audiobook { title: String }, } // ...
We can imagine that the above code is like creating three structs for us (PLEASE NOTE: This is not what it's doing, but a way to think about it).
We can define functions that accept values of type "Media":
#[derive(Debug)] enum Media { Book { title: String, author: String }, Movie { title: String, director: String }, Audiobook { title: String }, } fn print_media(media: Media) { println!("{:?}", media); } fn main() { let audiobook = Media::Audiobook { title: String::from("The Great Gatsby"), }; print_media(audiobook); }
The above works just fine!
I had a few questions:
impl Media { ... }
to define the print_media
function?Book
and use that as part of the enum?The answers:
impl Media { ... }
to define the print_media
function as a method of the Media
enum. Here's how you can do it:impl Media { fn print(&self) { println!("{:?}", self); } }
Now you can call it like this: media.print()
instead of print_media(media)
.
Enums and structs in Rust serve different purposes:
The main difference is that an enum instance can only be one of its variants at a time, while a struct always has all of its fields.
Book
and use it as part of the enum. Here's an example:#[derive(Debug)] struct Book { title: String, author: String, } #[derive(Debug)] enum Media { Book(Book), Movie { title: String, director: String }, Audiobook { title: String }, }
This approach can be useful if you want to reuse the Book
struct elsewhere in your code or if you want to add methods specific to Book
.
In our main function, we can declare them like so:
fn main() { let audiobook = Media::Audiobook { title: String::from("The Great Gatsby"), }; let good_movie = Media::Movie { title: String::from("The Dark Knight"), director: String::from("Christopher Nolan"), }; let bad_book = Media::Book { title: String::from("50 Shades of Grey"), author: String::from("Some author"), }; print_media(audiobook); print_media(good_movie); print_media(bad_book); }
We can use impl
on enums.
For adding our description method, we can use some conditional logic.
impl Media { fn description(&self) -> String { if let Media::Book {title, author} = self { format!("Book {} {}", title, author) } else if let Media::Movie {title, director} = self { format!("Movie {} {}", title, director) } else if let Media::Audiobook {title} = self { format!("Audiobook: {}", title); } else { String::from("Media description") } } }
This is a very verbose approach, so alternative we can use match
:
impl Media { fn description(&self) -> String { match self { Media::Book { title, author } => format!("Book {} {}", title, author), Media::Movie { title, director } => format!("Movie {} {}", title, director), Media::Audiobook { title } => format!("Audiobook: {}", title), } } }
It's worth calling out that the "downside" to using implementations on enums is that we need to match on self to gets things done.
In many cases, you can use either! So ask these questions:
In our case, our media types will only ever have one, same method (description
), so it fits the use of enums very well.
A side-note is that if the elements have a lot properties, then you might want to opt for structs instead for destructuring purposes.
Below I've pulled out the Book
enum type to be a struct and passed that as the enum type.
I then give an example of using both a Book method as well as implementing a Media method for the Book:
#[derive(Debug)] struct Book { title: String, author: String, } impl Book { fn new(title: String, author: String) -> Self { Book { title, author } } fn read(&self) { println!("Reading {} by {}", self.title, self.author); } } #[derive(Debug)] enum Media { Book(Book), Movie { title: String, director: String }, Audiobook { title: String }, } impl Media { fn description(&self) -> String { match self { Media::Book(book) => format!("Book: {} by {}", book.title, book.author), Media::Movie { title, director } => format!("Movie {} {}", title, director), Media::Audiobook { title } => format!("Audiobook: {}", title), } } } fn print_media(media: &Media) { println!("{:?}", media); } fn main() { let audiobook = Media::Audiobook { title: String::from("The Great Gatsby"), }; let good_movie = Media::Movie { title: String::from("The Dark Knight"), director: String::from("Christopher Nolan"), }; let bad_book = Media::Book(Book::new( String::from("50 Shades of Grey"), String::from("Some author"), )); println!("{}", audiobook.description()); print_media(&audiobook); print_media(&good_movie); print_media(&bad_book); // Using if let if let Media::Book(book) = &bad_book { book.read(); } else { println!("This media is not a book and cannot be read"); } }
We can create a new struct to hold the media in a catalog:
#[derive(Debug)] struct Catalog { items: Vec<Media>, } impl Catalog { fn new() -> Self { Catalog { items: vec![] } } fn add(&mut self, media: Media) { self.items.push(media); } }
Note: we are not using a reference to the media
argument in the add
method, meaning that we want the catalog to take ownership.
In main:
fn main() { let audiobook = Media::Audiobook { title: String::from("The Great Gatsby"), }; let good_movie = Media::Movie { title: String::from("The Dark Knight"), director: String::from("Christopher Nolan"), }; let bad_book = Media::Book(Book::new( String::from("50 Shades of Grey"), String::from("Some author"), )); println!("{}", audiobook.description()); print_media(&audiobook); print_media(&good_movie); print_media(&bad_book); // Using if let if let Media::Book(book) = &bad_book { book.read(); } else { println!("This media is not a book and cannot be read"); } let mut catalog = Catalog::new(); catalog.add(audiobook); catalog.add(good_movie); catalog.add(bad_book); println!("{:#?}", catalog); }
Running this will yield:
Audiobook: The Great Gatsby Audiobook { title: "The Great Gatsby" } Movie { title: "The Dark Knight", director: "Christopher Nolan" } Book(Book { title: "50 Shades of Grey", author: "Some author" }) Reading 50 Shades of Grey by Some author Catalog { items: [ Audiobook { title: "The Great Gatsby", }, Movie { title: "The Dark Knight", director: "Christopher Nolan", }, Book( Book { title: "50 Shades of Grey", author: "Some author", }, ), ], }
Right now we have three variants of Media
. Let's add two more Podcast
and Placeholder
:
enum Media { Book(Book), Movie { title: String, director: String }, Audiobook { title: String }, Podcast(u32), // Not very clear in this case Placeholder, }
For Podcast
, it's not so easy for other devs to know what the u32
field represents, but this is to show what you can do.
So how do we work with variants that has raw data assigned to it as well as no data assigned at all?
First, update description:
impl Media { fn description(&self) -> String { match self { Media::Book(book) => format!("Book: {} by {}", book.title, book.author), Media::Movie { title, director } => format!("Movie {} {}", title, director), Media::Audiobook { title } => format!("Audiobook: {}", title), Media::Podcast(episode_number) => format!("Podcast episode {}", episode_number), Media::Placeholder => String::from("Placeholder"), } } }
Then for creating them:
let podcast = Media::Podcast(42); let placeholder = Media::Placeholder;
How can we get items from a catalog? We can use the .get
method.
If we use catalog.items.get(n)
where n
is an index, we get two possible results:
This is because .get(n)
returns a Result enum that wraps a value that comes out.
Rust doesn't have null
, nil
or undefined
. Instead, we get a built-in enum called Option
with the two variants mentioned above.
If you want to work with Option you have to use pattern matching. This forces you to handle the case in which you have a value and the case in which you do not.
match catalog.items.get(0) { Option::Some(value) => println!("{}", value.description()), Option::None => println!("No item found"), }
We'll create a custom version of the .get
method.
#[derive(Debug)] struct Catalog { items: Vec<Media>, } impl Catalog { fn new() -> Self { Catalog { items: vec![] } } fn add(&mut self, media: Media) { self.items.push(media); } fn get_by_index(&self, index: usize) -> CustomOption { if self.items.len() > index { CustomOption::Some(&self.items[index]) } else { CustomOption::None } } } enum CustomOption<'a> { Some(&'a Media), None, } fn main() { // ... omitted match catalog.get_by_index(0) { CustomOption::Some(value) => println!("{:#?}", value), CustomOption::None => println!("No item found"), } }
Here we are using a thing we haven't seen yet 'a
which is called a lifetime annotation. We'll get back to this later.
Also, we could access the value with a if let
statement:
if let CustomOption::Some(value) = catalog.get_by_index(8) { println!("{:#?}", value); } else { println!("No item found"); }
The one other different with our implementation is that Option<T>
is a generic where T
is a type you supply. For us, that is Option<Media>
.
Match is the ideal way to figure out if we have Some
or None
.
There are more compact ways, but could result in crashes (less safe).
For (1):
For (2):
For (3):
Again, stick to match in real world scenarios unless you have a specific purpose.
The final result:
// TODO: // 1) Safely access the first account in the 'accounts' vector using the // .first_mut() method. // 2) '.first_mut()' returns an Option whose Some variant is a mutable ref to // an Account. Use a 'match' statement to figure out if // you have a Some or a None // 3) In the Some case, set the balance of the account to 30, then print the account // 4) In the None case, print the message "No account found" // Hint: You might have to add in the 'mut' keyword somewhere... #[derive(Debug)] struct Account { balance: i32 } fn main() { let mut accounts: Vec<Account> = vec![ Account { balance: 0 }, Account { balance: 10 } ]; // Add code here: match accounts.first_mut() { Some(account) => { account.balance = 30; println!("{:#?}", account); } None => println!("No account found") } }