In the last module, we had a working solution
to the pluralize exercise
that cloned the string passed to the pluralize
function.
This allowed the pluralize function to take
ownership of the cloned value
while the original value could still be used
in the main function.
However, having the pluralize function
take an owned string value isn't the
most idiomatic solution.
The most idiomatic solution uses the Rust
feature called borrowing.
In this module,
we'll talk about what borrowing is
and reasons your code might want to borrow
instead of taking ownership.
Borrowing a value may feel similar to using
a pointer if you're familiar
with languages that use pointers.
But there are important differences in Rust
that we'll go over.
Finally, we'll update the pluralize function
to borrow instead of taking ownership,
and we'll talk about why it's more idiomatic.
So, what is borrowing?
Borrowing is a way to allow some code
to use a value without moving ownership.
It's like letting someone borrow one of your toys
for a little while.
Even though someone else is using your toy,
you still own it,
and it's still your responsibility to clean
it up when everyone's done.
Let's look at an example in code.
We have a Person struct that has a name field.
We also have a function called congratulate,
which takes a person as a parameter.
Since the congratulate function doesn't need
to have ownership of the person,
it borrows the person, which is indicated
by the ampersand before the type name.
In main,
we first create an instance of a Person,
and then we call the congratulate function
and pass it a reference to the Person
instance as an argument.
We create the reference by again using an
ampersand.
p retains ownership of the Person instance;
we can still use it in main,
we didn't have to clone it, and it gets
cleaned up at the end of main
when p goes out of scope - not in
the congratulate function.
This code compiles without any errors
and prints both the congratulations
and the message at the end of main.
So, why would we want to borrow?
The reason we want the ability to borrow is
for performance.
If a function doesn't need ownership of
a value that has allocated memory,
instead of cloning the value and giving that to the
function,
we can give the function a reference to the original
value.
The function can borrow the value
and no extra allocation is needed.
For example, pretend that the Person struct
had 10 fields and the congratulate
function only needed to use one of them.
If we had to clone the whole Person instance
in order to pass it to a function that only uses
part of it,
we would be wasting the memory space needed to
store the cloned instance, and we'd
be wasting time making the clone.
With borrowing,
no cloning of anything is necessary.
Borrowing is also a signal of intent.
When you look at a function signature in the documentation
and see that it borrows its parameters,
you know it wants nothing to do
with the allocation
or deallocation of that data.
Borrowing and references may remind you
of pointers in languages like C
and C++. References
in Rust are similar -
there is one big, important difference.
In safe Rust, the borrow checker
ensures, at compile time,
that you'll never have an invalid reference -
a reference that points to nothing
or to invalid memory.
This prevents a lot of bugs.
For example, here are some bugs you may have
seen in other languages.
If you've ever gotten a seg fault in C
or C++,
or errors like undefined method for nil
class in Ruby,
or undefined is not a function in JavaScript,
you've experienced a runtime bug caused
by an invalid reference
that's a compile time error in Rust.
These runtime errors happen only in certain
circumstances
and can be very hard to track down.
Rust catches these errors every time
and won't let you try to even run code
with this problem.
Let's see what happens when we try to create
an invalid reference.
You may think that this code should work.
If so, don't feel bad -
lots of people think that this code should work.
The function name doesn't take any arguments.
It allocates a new String and attempts
to return a reference to that String.
When we try to compile this, we get an error saying
missing lifetime specifier.
We're going to talk all about lifetimes in unit
4. But for now, trust
me that adding a lifetime specifier here won't
fix the problem.
The help text gives more of a hint:
this function's return type contains
a borrowed value,
but there is no value for it to be borrowed from.
Looking at the code again,
the problem is that the variable n is
the owner of the allocated String.
But n goes out of scope at the end of the name
function. Rust
doesn't let us return a reference to n
from this function because there's nothing for
that reference to point to
once the function is over.
If we were able to do this, the
variable my_name in main
would contain an invalid reference.
In general, you can't return a reference
from a function that points to something that
was created within that function -
n will go out of scope
and get cleaned up at the end of the function.
To fix this code,
change the name function to return an
owned String instead of a borrowed
string slice
and return the String directly.
This way, ownership is returned
out of the function
and the String doesn't get cleaned up.
Now, let's take this knowledge about borrowing
and apply it to the pluralize exercise.
Here's the first solution we went over in the previous
module, where we cloned s
and passed the cloned value to the pluralize
function.
Instead of cloning s in main,
we're going to pass a borrowed string slice
of s to pluralize.
This means we have to change the signature
of pluralize to expect a string
slice as the type of the singular argument.
We'll talk about why this is &str,
a string slice,
rather than &String, in
the next module.
If we compile at this point, we'll get an error
because we need to fix the body of pluralize.
The error message tells us we can't use the
plus operator to concatenate
two string slices.
The help text explains more about why.
The plus operator puts the result of
the concatenation into the string on
the left. So, the string on the left
needs to be able to be reallocated to
make more space for the result.
String slices are borrowed
and can't reallocate.
The help text
also tells us how to fix this
by adding to_owned()
to create a String.
This does allocate memory.
Let's make that change
and call to_owned on singular.
Now the solution compiles
and runs, producing the output we expect.
In this case, changing to borrowing
isn't about saving an allocation,
as we're still needing to make that allocation
for the new String created from the concate
nation.
We still think that borrowing is a more idiomatic
solution than the solution where we cloned
s in main.
Here, the allocation is an implementation
detail encapsulated in the pluralize
function rather than making that
a concern of the code calling
pluralize.
We've shown that borrowing is a way to lend
out a value,
instead of transferring ownership to allow
other code to use the value.
Rust code uses borrowing to reduce
allocations, which can improve the runtime
performance and memory usage of our code.
Borrowing in Rust is less error-prone
than using pointers in other languages
because Rust makes sure that the references are
always valid. In
general, having functions borrow
values rather than take ownership is
more idiomatic.
These are the basics about borrowing.
There are more details that we're going to get into
in the next module.