Structs, short for structures,
are custom types that group related data together,
such as all of the attributes of a hockey player. Recall the primitive data types from module 4 -
we'll be building structs by putting primitive data types
together.
We'll be comparing and contrasting
structs with enums,
which we covered in module 7. In this module, we'll cover when structs are useful;
we'll
look at the syntax for defining and using a struct;
we'll also discuss some other forms of structs,
which are tuple structs and unit structs.
We'll also briefly circle back to enums to show how you can define enum variants to have named
fields like structs do.
Let's get started with when structs are useful.
Remember the HockeyPosition
enum
we created in the previous module?
Let's say we now want to create a data type that models a
hockey player -
that would be a good fit for a struct.
Every hockey player has a bunch of attributes that describe that player.
They each have a name,
a number,
a position, and some stats like how many goals they've scored this year.
This set of attributes is the same across all players,
but each player has different values for these attributes.
Putting values for these attributes together describes a particular hockey player.
Describing a hockey player with a struct is a better fit than describing a hockey player with an enum
because we're not choosing between different kinds of hockey players.
An enum is for when you want to describe a choice between a set of values,
such as choosing banana,
apple, or orange from a set of fruits.
A struct is for when you want to have the same attributes for all of the values of that type,
such as multiple recipes that all have a list of ingredients and a list of directions.
Let's look at how we define a hockey player struct in code.
A struct definition starts with the struct keyword,
then the name we want to use for the struct.
Here, we've picked HockeyPlayer. Struct names, by convention, should be camel case.
Next comes curly brackets, and, within them,
a comma-separated list of the field names and types that make up the struct.
Field names should be in snake case.
For hockey player, we've got a name field that's a String, a number field that's a u8, a position field that has the type of our HockeyPosition enum,
and a goals year-to-date field that is also u8.
Now it's time to put this struct definition to use.
Struct definitions list the set of attributes,
and we can then fill in a struct with values to represent a particular instance. To instatiate
a particular hockey player and store the data in a variable,
we set the variable equal to the struct name,
then open curly brackets,
then list each field and the value we want each field to have.
Remember to finish the assignment statement with a semicolon.
It's required to initialize all the fields of the struct with the value.
Once we have a struct instance,
we can access the value in a particular field of the struct by using the variable name, then a dot, then the field we want to access.
We can also change the values
in an instance's
fields as long as the instance is mutable.
This code adds one to the goals year-to-date field,
and the program now prints Bryan Rust has scored 8 goals this season.
Having named fields is what most structs look like,
but there are also tuple structs and unit structs.
Let's look at those briefly. Tuple
structs are structs that have a name for the whole type but don't name their fields.
They behave similarly to tuples.
You access their fields with a dot and an index,
starting with zero.
For example,
here's a tuple struct named Triangle,
defined to store the length of each of the three sides of a triangle.
Tuple
structs are useful to create types that are considered different from a plain tuple or other tuple structs
as far as Rust's type checker is concerned.
We can't pass a plain tuple to this function that only takes a triangle.
If we try,
we get a type error.
Giving the tuple a name by using a tuple struct has made this a new type incompatible with other tuples.
One common use of a tuple struct is called the newtype pattern,
where an existing type is wrapped in a tuple struct with one element to add meaning.
An example of this is wrapping
a u8 in a struct named Meters,
which now guarantees that, in places that use values of type Meters,
we can't use a plain
u8.
If we try to compile this code that attempts to call a function that takes Meters with a plain u8,
we'll get a type error.
This can prevent bugs caused by accidentally using values that are in the wrong units.
Structs actually don't need to have any fields. Structs without
fields are called unit
structs.
Unit
structs don't seem very useful yet,
but you can define methods on them.
We're going to talk about methods in the next module.
One last thing:
now that we've seen what structs with named fields look like,
let's look at
enum variants that can have named fields like structs.
Remember the clock example from the last module?
Each variant held values representing hours,
minutes, or seconds.
But it was hard to tell which field meant which value - you just had to know.
We can make this better by making these variants more like a struct and adding field names.
We also have to change the parentheses to curly brackets.
In this module, we covered
when to use structs versus enums,
how to declare and use structs, tuple and unit
structs, and enum variants
that look like structs.
That's how structs work in Rust.
Go forth and organize your data into new types.
Next,
we'll talk about adding behavior to structs and enums by defining methods on them.