It’s now time to look at the main ways to build your own types in Rust: structs and enums. You’ll also learn how to implement functions attached to and called on these types, called methods. These methods use the keyword self
a lot, so get ready to see it!
Structs and enums have a similar syntax and are easiest to learn together, so that’s what we’ll do here. They also work together because structs can contain enums, and enums can contain structs. Because they look similar, sometimes users new to Rust confuse them. But here’s a rule of thumb to start: if you have a lot of things to group together, that’s a struct, but if you have a lot of choices and need to select one, that’s an enum.
If this book, Learn Rust in a Month of Lunches, were a struct, it would have its own properties, too. It would have a title
(that’s a String
), an author_name
(also a String
), and a year_of_publication
(maybe an i32
). But it also has more than one way to buy it: you can choose to buy it either as a printed book or as an eBook. That’s an enum! So keep that simple example in mind as we learn how structs and enums work.
With structs, you can create your own type. You can probably guess that the name is short for structure; you construct your own type with them. You will use structs all the time in Rust because they are so convenient. Structs are created with the keyword struct
, followed by its name. The name of a struct should be in UpperCamelCase (capital letter for each word with no spaces). The code will still work if you write a struct in all lowercase, but the compiler will give a warning recommending that you change its name to UpperCamelCase.
There are three types of structs. One is a unit struct. Unit means “doesn’t have anything” (like the unit type). For a unit struct, you simply write the name and a semicolon:
struct FileDirectory;
The next type is a tuple struct, or an unnamed struct. It is “unnamed” because you only need to write the types inside the tuple, not the field names. Tuple structs are good when you need a simple struct and don’t need to remember names. You access their items in the same way as other tuples: .0, .1
, and so on:
struct ColorRgb(u8, u8, u8);
fn main() {
let my_color = ColorRgb(50, 0, 50); ①
println!("The second part of the color is: {}", my_color.1);
}
① Makes a color out of red, green, and blue
This prints The second part of the color is: 0
.
The third type is the named struct, which is the most common struct. In this struct, you declare field names and types inside a {}
code block. Note that you don’t write a semicolon after a named struct because there is a whole code block after it:
struct ColorRgb(u8, u8, u8); ① struct SizeAndColor { size: u32, color: ColorRgb, ② } fn main() { let my_color = ColorRgb(50, 0, 50); let size_and_color = SizeAndColor { size: 150, color: my_color }; }
① Declares the same Color tuple struct
② Puts it in our new named struct
You separate fields by commas in a named struct, too. For the last field, you can add a comma or not—it’s up to you. SizeAndColor
had a comma after color
:
struct ColorRgb(u8, u8, u8);
struct SizeAndColor {
size: u32,
color: ColorRgb, ①
}
But you don’t need it to compile the program. It can be a good idea to always put a comma because sometimes you will change the order of the fields:
struct ColorRgb(u8, u8, u8);
struct SizeAndColor {
size: u32,
color: ColorRgb ①
}
Then we cut and paste to change the order of the parameters:
struct SizeAndColor {
colour: ColorRgb ①
size: u32,
}
① Whoops! Now this doesn’t have a comma.
But it is not very important either way.
Now, let’s create a Country
struct as our first concrete example. The Country
struct has the fields population, capital
, and leader_name
. To declare a Country
, we simply give it all the values it needs. Rust won’t instantiate (start) a Country
for us unless we give it a value for each of its three parameters:
struct Country { population: u32, capital: String, leader_name: String } fn main() { let population = 500_000; let capital = String::from("Elista"); let leader_name = String::from("Batu Khasikov"); let kalmykia = Country { population: population, capital: capital, leader_name: leader_name, }; }
Did you notice that we wrote the same thing twice? We wrote population: population, capital: capital
, and leader_name: leader_name
. In fact, you don’t need to do that. One nice convenience in Rust is that if the field name and variable name are the same, you don’t have to write both. Let’s give that a try:
struct Country { population: u32, capital: String, leader_name: String } fn main() { let population = 500_000; let capital = String::from("Elista"); let leader_name = String::from("Batu Khasikov"); let kalmykia = Country { population, capital, leader_name, }; }
And, of course, you can just put a struct together without making variables first:
struct Country { population: u32, capital: String, leader_name: String } fn main() { let kalmykia = Country { population: 500_000, capital: String::from("Elista"), leader_name: String::from("Batu Khasikov") }; }
Now, let’s say you wanted to add a climate
(weather) property to Country
. You would use it to pick a climate type for each country: Tropical, Dry, Temperate, Continental
, and Polar
(those are the main climate types). You would write let kalmkia = Country
and eventually get to climate
and would write something to choose one of the five. That’s what an enum is for! Let’s learn them now.
An enum
is short for enumerations (we’ll find out soon why they are called that). They look very similar to structs but are different:
So structs are for many things together, while enums are for many possible choices.
To declare an enum, write enum
and use a code block with the options separated by commas. Just like a struct, the last part can have a comma or not. To make a choice when using an enum, use the enum name, followed by two ::
(colons), and then the name of the variant (the choice). That means you can choose by typing Climate::Tropical, Climate::Dry
, and so on.
Here is our Climate
enum, which the Country
struct now holds:
enum Climate { Tropical, Dry, Temperate, Continental, Polar, } struct Country { population: u32, capital: String, leader_name: String, climate: Climate, ① } fn main() { let kalmykia = Country { population: 500_000, capital: String::from("Elista"), leader_name: String::from("Batu Khasikov"), climate: Climate::Continental, ② }; }
① As noted before, a struct can hold an enum, and an enum can hold a struct. This is one example of that.
② This is the important part: you use :: to make a choice inside an enum.
Now let’s change examples and create a simple enum called ThingsInTheSky
:
enum ThingsInTheSky { Sun, Stars, }
This, too, is an enum because you can either see the sun or the stars: you have to choose one.
Now let’s create some functions related to the enum so that we can work with it a bit:
enum ThingsInTheSky { Sun, Stars, } fn create_skystate(time: i32) -> ThingsInTheSky { ① match time { 6..=18 => ThingsInTheSky::Sun, _ => ThingsInTheSky::Stars, } } fn check_skystate(state: &ThingsInTheSky) { ② match state { ThingsInTheSky::Sun => println!("I can see the sun!"), ThingsInTheSky::Stars => println!("I can see the stars!") } } fn main() { let time = 8; ③ let skystate = create_skystate(time); ④ check_skystate(&skystate); }
① This function is pretty simple: it takes a number to represent the hour of the day and returns a ThingsInTheSky based on that. You can see the Sun between 6 and 18 o’clock; otherwise, you can see Stars.
② This second function takes a reference to a ThingsInTheSky and prints a message depending on which variant of ThingsInTheSky it is.
This prints I can see the sun!
But what makes Rust’s enums special is that they don’t just contain choices; they can hold data. A struct can hold an enum, an enum can hold a struct, and an enum can hold other types of data, too. Let’s give ThingsInTheSky
some data:
enum ThingsInTheSky { Sun(String), Stars(String), } fn create_skystate(time: i32) -> ThingsInTheSky { match time { ① 6..=18 => ThingsInTheSky::Sun(String::from("I can see the sun!")), _ => ThingsInTheSky::Stars(String::from("I can see the stars!")), } } fn check_skystate(state: &ThingsInTheSky) { match state { ThingsInTheSky::Sun(description) => println!("{description}"), ② ThingsInTheSky::Stars(n) => println!("{n}"), } } fn main() { let time = 8; let skystate = create_skystate(time); check_skystate(&skystate); }
① Now that the enum variants hold a String, you have to provide a String, too, when creating ThingsInTheSky.
② Now, when we match on our reference to ThingsInTheSky, we have access to the data inside (in this case, a String). Note that we can give the inner String any name we want here: description, n, or anything else.
This prints the same thing: I can see the sun!
With the use
keyword, you can also “import” an enum, so you don’t have to type so much. Here’s an example with a Mood
enum where we have to type Mood::
every time we match on it:
enum Mood { Happy, Sleepy, NotBad, Angry, } fn match_mood(mood: &Mood) -> i32 { let happiness_level = match mood { Mood::Happy => 10, ① Mood::Sleepy => 6, ① Mood::NotBad => 7, ① Mood::Angry => 2, ① }; happiness_level } fn main() { let my_mood = Mood::NotBad; let happiness_level = match_mood(&my_mood); println!("Out of 1 to 10, my happiness is {happiness_level}); }
① Here we type Mood:: every time.
It prints Out of 1 to 10, my happiness is 7
. Let’s try the use
keyword to import this enum’s variants so that we can type less. To import everything, write *
:
enum Mood {
Happy,
Sleepy,
NotBad,
Angry,
}
fn match_mood(mood: &Mood) -> i32 {
use Mood::*; ①
let happiness_level = match mood {
Happy => 10,
Sleepy => 6,
NotBad => 7,
Angry => 2,
};
happiness_level
}
fn main() {
let my_mood = Mood::Happy;
let happiness_level = match_mood(&my_mood);
println!("Out of 1 to 10, my happiness is {happiness_level}");
}
① This imports every variant inside the Mood enum. Using * is the same as writing use Mood::Happy; then use Mood::Sleepy; and so on for each variant.
This use
keyword isn’t just for enums, by the way: it’s used any time you use :: too much and want to type less. Do you remember this example from chapter 2 where we used a function called std::mem::size_of_val()
to check the size of two names? That was a lot of typing:
fn main() {
let size_of_jaurim = std::mem::size_of_val("자우림");
let size_of_adrian = std::mem::size_of_val("Adrian Fahrenheit Țepeș");
println!("{size_of_jaurim}, {size_of_adrian}");
}
This prints their size in bytes: 9 and 25 bytes
. But we could have gone with use
to import the function so that we only have to write size_of_val
every time we use it:
use std::mem::size_of_val; ① fn main() { let size_of_jaurim = size_of_val("자우림"); let size_of_adrian = size_of_val("Adrian Fahrenheit Țepeș"); println!("{size_of_jaurim}, {size_of_adrian}"); }
① The use keyword can be used inside main or inside or outside another function. If you use it inside a smaller scope, like a separate function, then it will only apply inside that scope.
If an enum doesn’t contain any data, then its variants can be cast into an integer. That’s because Rust gives each variant of these simple enums a number that starts with 0 for its own use. (That’s where the name enum
comes from: the num
in enum
is the same as the num
in number.)
enum Season {
Spring, ①
Summer,
Autumn,
Winter,
}
fn main() {
use Season::*;
let four_seasons = vec![Spring, Summer, Autumn, Winter];
for season in four_seasons {
println!("{}", season as u32);
}
}
① If this was Spring(String) or something it wouldn’t work.
0 1 2 3
However, you can also choose a different number if you like. The compiler doesn’t care and can use it in the same way, as long as two variants aren’t using the same number. To do this, add an =
and your number to the variant that you want to have a number. You don’t have to give all of them a number. But if you don’t, Rust will add 1 from the variant before to give it a number:
enum Star { BrownDwarf = 10, RedDwarf = 50, YellowStar = 100, RedGiant = 1000, DeadStar, ① } fn main() { use Star::*; let starvec = vec![BrownDwarf, RedDwarf, YellowStar, RedGiant, DeadStar]; for star in starvec { match star as u32 { size if size <= 80 => println!("Not the biggest star."), size if size >= 80 && size <= 200 => ➥println!("This is a good-sized star."), other_size => ➥println!("That star is pretty big! It's {other_size}"), ② } } }
① Think about this one. What number will it have?
② We need to have this final arm of the match so that Rust can decide what to do if the u32 it gets is some other value that’s not smaller than 80, or in between 80 and 200. We called the variable other_size here, but we could have called it size or anything else.
Not the biggest star. Not the biggest star. This is a good-sized star. That star is pretty big! It's 1000 That star is pretty big! It's 1001
If we hadn’t chosen our own numbers, then Rust would have started with 0 for each variant. Thus BrownDwarf
would have been a 0 instead of a 10, DeadStar
would have been 4 instead of 1001, and so on.
We learned in the last chapter that items in a Vec
, array, etc., all need the same type and that only tuples are different. However, enums give us a bit of flexibility here because they can carry data, and that means that you can use an enum to hold different types inside a collection.
Imagine we want to have a Vec
that holds either u32
s or i32
s. Rust will let us create a Vec<u32>
or a Vec<i32>
, but it won’t let us make a Vec<u32 or i32>
. However, we can make an enum (let’s call it Number
) and then put it inside a Vec
. That will give us a type Vec<Number>
. The Number
enum can have two variants, one of which holds a u32
and another that holds an i32
. Here is what it would look like:
enum Number { U32(u32), I32(i32), }
So there are two variants: the U32
variant with a u32
inside and the I32
variant with i32
inside. U32
and I32
are simply names we made. They could have been UThirtyTwo
and IThirtyTwo
or anything else.
The compiler doesn’t mind that a Vec<Number>
can hold either a u32
or i32
because they are all inside a single type called Number
. And because it’s an enum, you have to pick one, which is what we want. We will use the .is_positive()
method to pick. If it’s true
, we will choose U32
, and if it’s false
, we will choose I32
. Now the code looks like this:
enum Number { U32(u32), I32(i32), } fn get_number(input: i32) -> Number { let number = match input.is_positive() { true => Number::U32(input as u32), ① false => Number::I32(input), ② }; number } fn main() { let my_vec = vec![get_number(-800), get_number(8)]; for item in my_vec { match item { Number::U32(number) => println!("A u32 with the value {number}"), Number::I32(number) => println!("An i32 with the value {number}"), } } }
① Changes the number to a u32 if it’s positive
② Otherwise, keeps the number as an i32 because a u32 can’t be made from a negative number
This prints what we wanted to see:
An i32 with the value -800 A u32 with the value 8
We used a few functions in our previous samples to match on enums and print out differently depending on which variant the function received. But wouldn’t it be nice if we could make functions that are a part of the structs and enums themselves? Indeed, we can: this is called implementing.
This is where you can start to give your structs and enums some real power. To write functions for a struct or an enum, use the impl
keyword and then a scope with {}
to write the functions (this is called an impl block). These functions are called methods. There are two kinds of methods in an impl
block:
Methods—These take self
in some form (&self
or &mut self
or self
). Regular methods use a .
(a period). .clone()
is an example of a regular method.
Associated functions (known as static methods in some languages)—These do not take self
. Associated means “related to.” Associated functions are called differently, by typing ::
in between the type name and the function name. String::from()
is an associated function, and so is Vec::new()
. You see associated functions most often used to create new variables.
This simple example shows why associated functions don’t use a period:
fn main() { let mut my_string = String::from("I feel excited"); ① my_string.push('!'); ② }
① The variable my_string doesn’t exist yet, so you can’t call my_string.some_method_name(). Instead, we use String::from to create a String.
② But now the variable my_string exists, so we can use . to call a method on it. One method that we already know is .push(). my_string now holds the value "I feel excited!"
NOTE Actually, you can call all methods using ::
if you want, but methods that take self
use .
for convenience. There is sometimes a good reason to use ::
for a method that takes self, but we will look at that later. It’s not very important to know just yet.
One more thing to know before we get to creating an impl
block: a struct or enum needs to have Debug
if you want to use {:?}
to print it. Rust has a convenient way to do this: if you write #[derive(Debug)]
above the struct or enum, you can print it with {:?}
. These messages with #[]
are called attributes. You can sometimes use them to tell the compiler to give your struct an ability like Debug
. There are many attributes, and we will learn about them later. But derive
is probably the most common, and you see it a lot above structs and enums, so it’s good to learn now.
Okay, let’s make an enum block now. In the next example, we are going to create animals and print them:
#[derive(Debug)] enum AnimalType { Cat, Dog, } #[derive(Debug)] struct Animal { age: u8, animal_type: AnimalType, } impl Animal { fn new_cat() -> Self { ① Self { ② age: 10, animal_type: AnimalType::Cat, } } fn check_type(&self) { match self.animal_type { AnimalType::Dog => println!("The animal is a dog"), AnimalType::Cat => println!("The animal is a cat"), } } fn change_to_dog(&mut self) { ③ self.animal_type = AnimalType::Dog; println!("Changed animal to dog! Now it's {self:?}"); } fn change_to_cat(&mut self) { self.animal_type = AnimalType::Cat; println!("Changed animal to cat! Now it's {self:?}"); } } fn main() { let mut new_animal = Animal::new_cat(); ④ new_animal.check_type(); new_animal.change_to_dog(); new_animal.check_type(); new_animal.change_to_cat(); new_animal.check_type(); }
① Here, Self means Animal. You can also write Animal instead of Self. To the compiler, it is the same thing.
② When we write Animal::new(), we always get a cat that is 10 years old.
③ Because we are inside impl Animal, &mut self means &mut Animal. Use .change_to_dog() to change the cat to a dog. Taking &mut self lets us change it.
④ This associated function will create a new Animal for us: a cat, 10 years old
The animal is a cat Changed animal to dog! Now it's Animal { age: 10, animal_type: Dog } The animal is a dog Changed animal to cat! Now it's Animal { age: 10, animal_type: Cat } The animal is a cat
Remember that Self
means the type Self
, and self
means the variable called self
that refers to the object itself. So, in our code, Self
means the type Animal
. Also, fn change_to_dog(&mut self)
means fn change_to_dog(&mut Animal)
.
Here is one more short example. This time, we will use impl
on an enum
:
enum Mood { Good, Bad, Sleepy, } impl Mood { fn check(&self) { match self { Mood::Good => println!("Feeling good!"), Mood::Bad => println!("Eh, not feeling so good"), Mood::Sleepy => println!("Need sleep NOW"), } } } fn main() { let my_mood = Mood::Sleepy; my_mood.check(); }
You could take these two examples and develop them a bit if you like. How would you write a function that lets you create a new Animal
that is an AnimalType::Dog
? How about letting the user of the function choose an age instead of always generating a Cat
that is 10 years old? Or how about giving the enum Mood
to the Animal
struct, too?
Using structs, enums, and impl
blocks is one of the most common things you’ll do in Rust, so you’ll quickly get into the habit of putting them together. In the next section, you’ll learn to do the complete opposite! Because if you have a fully constructed struct or other type, you can also destructure it in the same way that we have learned to destructure tuples. Let’s take a look at that now.
Let’s look at some more destructuring. You can get the values from a struct or enum by using let
backward. We learned in the last chapter that this is destructuring because it creates variables that are not part of a structure. We’ll start with a simple example. You’ll recognize the following character if you’ve seen the movie 8 Mile before:
struct Person { ① name: String, real_name: String, height: u8, happiness: bool } fn main() { let papa_doc = Person { ② name: "Papa Doc".to_string(), real_name: "Clarence".to_string(), height: 170, happiness: false }; let Person { ③ name, real_name, height, happiness, } = papa_doc; println!("They call him {name} but his real name is {real_name}. He is {height} cm tall and is he happy? {happiness}"); }
This prints They call him Papa Doc but his real name is Clarence. He is 170 cm tall and is he happy? false
You can see that destructuring works backward:
You can also rename variables as you destructure. The following code is the same as the previous code, except that we chose the name fake_name
for the name
parameter and cm
for the height
parameter:
struct Person { name: String, real_name: String, height: u8, happiness: bool } fn main() { let papa_doc = Person { name: "Papa Doc".to_string(), real_name: "Clarence".to_string(), height: 170, happiness: false }; let Person { name: fake_name, ① real_name, height: cm, ② happiness } = papa_doc; println!("They call him {fake_name} but his real name is {real_name}. ➥He is {cm} cm tall and is he happy? {happiness}"); }
① Here, we choose to call the variable fake_name.
② And here, we choose to call the variable cm.
Now, let’s look at a bigger example. In this example, we have a City
struct. We give it a new
function to make it. Then we have a process_city_values
function to do things with the values. In the function, we just create a Vec
, but you can imagine that we can do much more after we destructure it:
struct City {
name: String,
name_before: String,
population: u32,
date_founded: u32,
}
impl City {
fn new(
name: &str,
name_before: &str,
population: u32,
date_founded: u32,
) -> Self {
Self {
name: String::from(name),
name_before: String::from(name_before),
population,
date_founded,
}
} ①
fn print_names(&self) {
let City {
name,
name_before,
population,
date_founded,
} = self;
println!("The city {name} used to be called {name_before}.");
}
}
fn main() {
let tallinn = City::new("Tallinn", "Reval", 426_538, 1219);
tallinn.print_names();
}
① Now, we have the values to use separately.
This prints The city Tallinn used to be called Reval
.
You’ll notice that the compiler tells us that we didn’t use the variables population
and date_founded
. We can fix that! If you don’t want to use all the properties of a struct, just type ..
after you finish the properties you want to use. The print_names()
method in the following code will now only destructure with the name
and name_ before
parameters:
struct City {
name: String,
name_before: String,
population: u32,
date_founded: u32,
}
impl City {
fn new(
name: &str,
name_before: &str,
population: u32,
date_founded: u32
) -> Self {
Self {
name: String::from(name),
name_before: String::from(name_before),
population,
date_founded,
}
}
fn print_names(&self) {
let City {
name,
name_before,
.. ①
} = self;
println!("The city {name} used to be called {name_before}.");
}
}
fn main() {
let tallinn = City::new("Tallinn", "Reval", 426_538, 1219);
tallinn.print_names();
}
① These two dots tell Rust not to care about the other parameters inside City.
Interestingly, you can even destructure inside the signature of a function. Let’s give this a try with the same sample with Papa Doc:
struct Person { ① name: String, real_name: String, height: u8, happiness: bool, } fn check_if_happy(person: &Person) { ② println!("Is {} happy? {}", person.name, person.happiness); } fn check_if_happy_destructured(Person { name, happiness, .. }: &Person) { ③ println!("Is {name} happy? {happiness}"); } fn main() { let papa_doc = Person { name: "Papa Doc".to_string(), real_name: "Clarence".to_string(), height: 170, happiness: false, }; check_if_happy(&papa_doc); check_if_happy_destructured(&papa_doc); }
① This is the exact same struct as the previous sample—no changes here.
② Next is a function that takes a &Person and checks whether the person is happy.
③ And, finally, a function that does the same thing, except that it destructures the Person struct. This gives direct access to the name and happiness parameters and uses .. to ignore the rest of the struct’s parameters.
Is Papa Doc happy? false Is Papa Doc happy? false
So that finishes up the basics of structs and enums. For the final section in this chapter, we’ll learn an interesting fact about the .
operator: the “dot operator.” It has a certain magic to it that keeps syntax clean when using methods for your types.
We learned in chapter 2 that when you have a reference, you need to use *
to get to the value. A reference is a different type, so the following won’t work:
fn main() { let my_number = 9; let reference = &my_number; println!("{}", my_number == reference); }
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src\main.rs:5:30
|
5 | println!("{}", my_number == reference);
| ^^ no implementation for
➥`{integer} == &{integer}`
So we changed line 5 to println!("{}", my_number == *reference);
and now it prints true
because it’s now i32 == i32
, not i32 == &i32
. This is called dereferencing.
Now let’s look at something interesting. First, let’s make a simple String
. We’ll see if it’s empty:
fn main() { let my_name = "Billy".to_string(); println!("{}", my_name.is_empty()); }
Easy, right? It just says false
.
And, just like before, you can’t compare a reference to something that’s not a reference. So if we try to compare a String
to a &String
, we will get an error:
fn main() {
let my_name = "Billy".to_string();
let other_name = "Billy".to_string();
println!("{}", my_name == &other_name);
// println!("{}", &my_name == &&other_name); ①
}
① You can’t compare a &String with a &&String. Uncommenting this will generate an error, too.
But take a look at this example. Do you think it will compile?
fn main() { let my_name = "Billy".to_string(); let double_ref = &&my_name; println!("{}", double_ref.is_empty()); }
It does! The method .is_empty()
is for the String
type, but we called it on a &&String
. That’s because when you use a method, Rust will dereference for you until it reaches the original type. The . in a method is called the dot operator, and it does dereferencing for free. Without it, you would have to write this:
fn main() { let my_name = "Billy".to_string(); let double_ref = &&my_name; println!("{}", (&**double_ref).is_empty()); }
And that compiles, too! That’s one *
to get to the type itself and then an &
to take a reference to it (because .is_empty()
takes a &self
). But the dot operator will dereference as much as needed, so you don’t have to write *
and &
everywhere just to use the methods for a type. This works just fine, too:
fn main() { let my_name = "Billy".to_string(); let my_ref = &my_name; println!("{}", &&&&&my_ref.is_empty()); }
That was a lot to think about, but, fortunately, the conclusion is easy: when you use the dot operator, you don’t need to worry about *
.
As a Rust programmer, you are going to use structs and enums everywhere. You’ll soon get into the habit of making one, starting an impl
block, and then adding methods. It’s also nice that you can already see that some of the types you’ve learned are structs and enums. A String
is, in fact, a struct String
, a Vec
is a struct Vec
, and there are impl String
and impl Vec
blocks, too. There’s nothing magic about them, and you’re already starting to see how they work. We haven’t learned any types in the standard library that are enums yet, but we will in the next chapter! Two of Rust’s most famous types are structs and enums, and now you’re ready to learn how they work.
Structs are a little bit like tuples with names. They can hold all sorts of different types inside.
Usually, after making a struct or enum, you’ll start an impl
block and give it some methods. Most of the time, they’ll take &self
or &mut self
if you need to change it.
Not all methods inside an impl
block need self
: if you want one to start a new struct or enum, it will create a Self
and return it. You might even want one without self
that returns something else. The compiler doesn’t care whether you have self
inside an impl
block.
To get data from inside an enum, you’ll usually use match
or something similar. An enum is about having only one choice, so you have to check which one was chosen!
Enums are a good way to get around Rust’s strict rules. Make one enum and put in as many types as you need!
Destructuring can look strange at first, but it’ll work every time if you take a normal let
statement and turn the code around.