Enums and structs
are how we define the data that makes
up custom types.
Methods are how we define behavior
on custom types.
Methods are similar to functions, which
we covered in module 5.
Methods are defined either in the context
of an enum that we talked about in module
7
or a struct from module 8.
Review these modules to get ready for this one.
In this module, we'll take a look at when
methods are useful.
We'll cover how to define methods
and then show how to call methods.
We'll take a brief aside to mention associated
functions and how they're different from regular
functions and methods.
We'll wrap up by talking about the difference
between methods that only need to read
data from the type they're defined on
versus methods that need to be able to modify
the data too.
Let's start with when methods are useful.
Let's take a look at the HockeyPlayer struct
we defined in module 8.
With just this definition,
we can create an instance, read from
its fields, change the values
in the fields if the instance is mutable,
but that's about it.
We'd like to model what happens when a player
shoots the puck at the goal.
So, let's start by defining a function named
shoot_puck
and we'll work towards making the function into
a method. The shoot_puck
function will take a HockeyPlayer instance,
which is the player that will be shooting the puck.
Eventually, this function could take in a lot of
data about the current state of the game.
For now, we'll start our modeling by taking
into account the number of seconds left
in the current period.
The actual implementation of this function
isn't important for the moment.
Feel free to add an implementation of your choosing
that looks at the player's stats,
and the seconds left,
and prints out whether the puck went in the goal
or not, according to how you want your hockey
simulator to work.
We've decided that, in our very
unrealistic simulation,
all players always make their goals
unless there's under 5 minutes left,
in which case, only players at the center
position have the composure to make shots,
and players in other positions miss.
This code works fine
and prints Goal!.
However, we're not communicating
the capabilities of our program
the best way we could. In
our hockey simulation program,
the shoot_puck function only makes
sense in the context of a HockeyPlayer
instance.
Other types, like coach, fan,
or mascot can't legally
shoot the puck.
We're defining behavior of the hockey player
that every hockey player should have,
and the code would be more organized
if the behavior was clearly tied to the
struct.
That's where methods come in.
How do we define a method?
Let's find out.
Methods are defined within a block
that starts with the impl keyword,
which is short for implementation.
Then, we specify what we're implementing
methods on, which, in this case,
is the HockeyPlayer struct.
Note that this code would look similar if
we were implementing a method on an enum
rather than a struct.
We'd just put the name of the enum where
we currently have the name of the struct.
Next comes curly brackets to start the body
of the block. Within
the block, we use the fn keyword
and the method name,
just like with functions.
A big difference between functions
and methods is that the first parameter
of a method is always a form
of self, which refers
to the type that we're defining the method on -
in this case, a HockeyPlayer instance.
The rest of the parameters of the method
are the same as the rest of the parameters of the function
we had.
And whatever implementation you have would be the
same as well, except wherever
you used the HockeyPlayer parameter,
you'd use self
instead.
Now that we have a method defined, what
does it look like to call it?
Instead of having the function name
and passing the HockeyPlayer instance
as the first argument,
we can specify the HockeyPlayer instance,
then a dot, then the name
of the method we want to call,
and the remaining arguments that aren't self.
Now this code works just like the code
where shoot_puck was a function,
but we've more clearly communicated that
shooting a puck is behavior that a hockey
player instance has.
Related to methods are associated
functions. Let's take
a look at associated functions for a minute
and see how they compare
with regular functions
and with methods.
Associated functions are defined
within an impl block on a struct
or an enum, the same as methods.
There's still behavior that's related to the type,
so we want to organize that behavior
so that it's associated
with the type. Unlike
methods, they don't take self
as a parameter. Associated
functions are commonly used to create
instances of type, so
they don't have a self instance to operate
on.
For example, here's an associated
function named new
that creates an instance of a HockeyPlayer
struct from the given name,
number, and position.
This function always starts the goals_ytd
field at 0.
Note that new isn't a special
function name in Rust like it is
in languages like Ruby,
but by convention, many Rust types
define an associated function named
new
that creates new instances of that type.
To call an associated function,
specify the type name, then two
colons, then the function name.
The associated function is namespaced
beneath the type name.
This code will function similarly to the code
we had before in that we have
a new HockeyPlayer instance.
The difference is the goals_ytd
will be 0 instead of 7.
Returning to methods,
let's talk about the different forms of self
that a method can take, which
has to do with whether the method only needs to
read data
or whether it needs to write data too.
We actually glossed over a problem our code
currently has: a player
instance can only shoot the puck once.
If we try to have the same player shoot the puck
again,
we get an error saying use
of moved value: 'player'.
This has to do with Rust's system
of ownership
and the next unit is all about ownership,
so don't worry about understanding why this error
happens right this second.
A better way to define the shoot_puck
method in this context
is for it to borrow self instead
of taking ownership, which we
indicate with an &.
Again, we're
going to go into a lot more detail on this
in the next unit.
Making this change fixes the error
and we can now call shoot_puck more
than once.
&self is for methods
that don't change anything about self's
values. In this
implementation, we're only reading
from the position field of the HockeyPlayer
instance, not changing anything.
Let's say we did want to change self
to increase the count of the goals_ytd
when shooting the puck
and the player gets a goal.
If we make this change
and try to compile the code,
we get an error that we cannot assign to the
field, goals_ytd.
Just like we have to explicitly annotate
variables that can be mutated,
we have to annotate methods that can mutate
self.
We do this by making the first parameter of
the method &mut self.
This fixes the error,
and we can see that the method has modified
the player instance.
In this module,
we covered how methods
let us add behavior to data types,
how to define and call methods,
associated functions to find in impl
blocks that are associated
with a type
but not with an instance of the type,
and the various forms of self that
methods take depending on whether they read
or write data.
Those are the basics of methods
and the end of the syntax introduction for this
course.
Next unit,
we'll find out all about the concepts of ownership
and borrowing.
We'll get comfortable with when we should be adding ampersands.