07 Rustlings Move Semantics Part 1 Solution
From the Rustlings ReadMe: These exercises are adapted from pnkfelix's Rust Tutorial -- Thank you Felix!!!
Further information
For this section, the book links are especially important.
Intro
Ownership, References, and Borrowing are essential components of Rust's unique characteristics. It's crucial to have a solid understanding of these concepts to harness the full potential of Rust. In this introduction, we'll provide a brief overview of each aspect, but you can find more detailed information in the links provided above.
What is Ownership?
Ownership in Rust is a set of rules that dictate how a program manages memory. All programs require a memory management system during execution. While some languages rely on garbage collection to automatically locate and clean up unused memory, and others require the programmer to allocate and free memory manually, Rust takes a different approach. Rust's memory management is handled through ownership rules that are checked at compile time. If any of these rules are violated, the code will not compile. The advantage of this system is that it doesn't slow down our program during runtime.
For many programmers, the ownership concept is a novel idea and may require some adjustment. However, once you have a firm grasp of ownership, you'll have a strong foundation on what sets Rust apart from other languages and makes it memory safe.
References and Borrowing
Due to Rust's strict ownership rules, the language provides tools to reference or borrow different parts of your code as a means of accessing or using data without "consuming" it. These tools introduce another set of rules that must be learned to fully leverage Rust's features. With a thorough understanding of References and Borrowing, you can work efficiently within Rust's ownership system, allowing for more reliable and performant programs.
move_semantics1.rs
// move_semantics1.rs
// Execute `rustlings hint move_semantics1` or use the `hint` watch subcommand for a hint.
// I AM NOT DONE
fn main() {
let vec0 = Vec::new();
let vec1 = fill_vec(vec0);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
vec1.push(88);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}
fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
let mut vec = vec;
vec.push(22);
vec.push(44);
vec.push(66);
vec
}
Here's our first exercise, which not much in the sense of instructions on how to solve our problem. As always let's take a look at what the Rust compiler is telling us.
move_semantics1.rs
errors
⚠️ Compiling of exercises/move_semantics/move_semantics1.rs failed! Please try again. Here is the output:
error[E0596]: cannot borrow `vec1` as mutable, as it is not declared as mutable
--> exercises/move_semantics/move_semantics1.rs:13:5
|
13 | vec1.push(88);
| ^^^^^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
9 | let mut vec1 = fill_vec(vec0);
| +++
error: aborting due to previous error
For more information about this error, try `rustc --explain E0596`.
We get a lot of help here from this output clearly telling us where and what the problem is.
- where
line 13
:vec1.push(88)
- what
cannot borrow as mutable
We even a very clear suggestion on what we should add on line 9
, so let's do this.
9 | let mut vec1 = fill_vec(vec0);
| +++
move_semantics1
solution
fn main() {
let vec0 = Vec::new();
let mut vec1 = fill_vec(vec0); // adding `mut`
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
vec1.push(88);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}
fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
let mut vec = vec;
vec.push(22);
vec.push(44);
vec.push(66);
vec
}
Doing exactly as the Rust compiler says gets us an easy win.
Output:
====================
vec1 has length 3 content `[22, 44, 66]`
vec1 has length 4 content `[22, 44, 66, 88]`
====================
Since this was such an easy exercise, let's play around with the code, if you look at the hint by typing hint
in terminal, you'll see it gives you another little challenge.
Also: Try accessing `vec0` after having called `fill_vec()`. See what happens!
See if you can get this to work, it should be pretty straight forward if you follow the compiler hints, I was able to get this to print.
Output:
====================
vec1 has length 3 content `[22, 44, 66]`
vec1 has length 4 content `[22, 44, 66, 88]`
vec0 has length 1 content `[1]`
====================
On to the next one!
move_semantics2.rs
// move_semantics2.rs
// Make me compile without changing line 13 or moving line 10!
// Execute `rustlings hint move_semantics2` or use the `hint` watch subcommand for a hint.
// I AM NOT DONE
fn main() {
let vec0 = Vec::new();
let mut vec1 = fill_vec(vec0);
// Do not change the following line!
println!("{} has length {} content `{:?}`", "vec0", vec0.len(), vec0);
vec1.push(88);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}
fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
let mut vec = vec;
vec.push(22);
vec.push(44);
vec.push(66);
vec
}
Here we get one line of instruction, make this code compile without changing line 13 or moving line 10. Now let's take a look at what the Rust compiler is telling us.
move_semantics2.rs
errors
⚠️ Compiling of exercises/move_semantics/move_semantics2.rs failed! Please try again. Here is the output:
error[E0382]: borrow of moved value: `vec0`
--> exercises/move_semantics/move_semantics2.rs:13:57
|
8 | let vec0 = Vec::new();
| ---- move occurs because `vec0` has type `Vec<i32>`, which does not implement the `Copy` trait
9 |
10 | let mut vec1 = fill_vec(vec0);
| ---- value moved here
...
13 | println!("{} has length {} content `{:?}`", "vec0", vec0.len(), vec0);
| ^^^^^^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `fill_vec` to borrow instead if owning the value is not necessary
--> exercises/move_semantics/move_semantics2.rs:20:18
|
20 | fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
| -------- ^^^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
10 | let mut vec1 = fill_vec(vec0.clone());
| ++++++++
error: aborting due to previous error
Alright, we have a lot to see here, the first error on line 8 tells us that vec0
is being moved to line 10 to the line let mut vec1 = fill_vec(0);
The note tells us to consider changing the parameter type, but that shouldn't be where we should be looking because in this case we do want to return a Vec<i32>
. If we continue reading our errors, we get the answer in the line help: consider cloning the value if the performanc cost is acceptable
. This is very helpful and clear answer. If you played around with the code on [[#move_semantics1.rs]] you might have already encountered this solution. So let's try this clone()
solution.
move_semantics2
solution
fn main() {
let vec0 = Vec::new();
let mut vec1 = fill_vec(vec0.clone()); // adding clone() here
// Do not change the following line!
println!("{} has length {} content `{:?}`", "vec0", vec0.len(), vec0);
vec1.push(88);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}
fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
let mut vec = vec;
vec.push(22);
vec.push(44);
vec.push(66);
vec
}
It works. Again, you shouldn't be too surprised if you played around with the previous code that we had since it's pretty much the same code. Our code compiles and this is our output.
Output:
====================
vec0 has length 0 content `[]`
vec1 has length 4 content `[22, 44, 66, 88]`
====================
If we look at the hint, we actually have a few different options to make this code compile. The compiler told us one solution with cloning along with the warning that we should be aware that there could be a performance cost if our data is big. In our case it works because it's not, but using clone()
is not always a good solution. So, let's look at alternate solutions from the Rustlings hint.
hint
So, vec0
is passed into the fill_vec
function as an argument. In Rust,
when an argument is passed to a function and it's not explicitly returned,
you can't use the original variable anymore. We call this "moving" a variable.
Variables that are moved into a function (or block scope) and aren't explicitly
returned get "dropped" at the end of that function. This is also what happens here.
There's a few ways to fix this, try them all if you want:
-
Make another, separate version of the data that's in
vec0
and pass that tofill_vec
instead. -
Make
fill_vec
borrow its argument instead of taking ownership of it, and then copy the data within the function in order to return an ownedVec<i32>
-
Make
fill_vec
mutably borrow a reference to its argument (which will need to be mutable), modify it directly, then not return anything. Then you can get rid ofvec1
entirely -- note that this will change what gets printed by the firstprintln!
Option 1, is what we already did by cloning the data inside of vec0
we created a separate version so let's look at the other 2 options.
Solution 2
Tells us that we should make fill_vec
borrow its arguments instead of owning them, then we copy the data within the function and return and owned Vec<i32>
. Let's see how we can implement this.
fn main() {
let vec0 = Vec::new();
let mut vec1 = fill_vec(&vec0); // ad the `&` to our `vec0`
// Do not change the following line!
println!("{} has length {} content `{:?}`", "vec0", vec0.len(), vec0);
vec1.push(88);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}
// change the paramter of our function here to be a reference with &Vec<i32>
fn fill_vec(vec: &Vec<i32>) -> Vec<i32> {
let mut vec = vec.clone(); // we use clone() here to copy the data
vec.push(22);
vec.push(44);
vec.push(66);
vec
}
So here's the second solution and although it handles the problem a little differently it still has to copy the data using clone()
. So it would still have similar performance hits if we had a huge vector of data. However, it's worth noting that the second solution is more idiomatic Rust. In Rust, it's common to use the clone method directly on the caller's side, making it clear that a clone is being created and ownership is being transferred.
Solution 3
fn main() {
let mut vec0 = Vec::new(); // Make vec0 mutable
fill_vec(&mut vec0); // Pass a mutable reference to vec0
// Do not change the following line!
println!("{} has length {} content `{:?}`", "vec0", vec0.len(), vec0);
vec0.push(88);
println!("{} has length {} content `{:?}`", "vec0", vec0.len(), vec0); // Update the label to "vec0"
}
// Change the function parameter to accept a mutable reference with &mut Vec<i32>
// Remove the return type, as the function doesn't need to return anything
fn fill_vec(vec: &mut Vec<i32>) {
vec.push(22);
vec.push(44);
vec.push(66);
}
Now let's took at our final solution that changes a bit more how the code functions. In this modified version, fill_vec
now takes a mutable reference to a Vec<i32>
(indicated by &mut Vec<i32>
), allowing it to modify the input vector directly. Since the input vector is modified in place, the function doesn't need to return anything. As a result, there is no need for a separate vec1
.
We also see that our output changes
Output:
====================
vec0 has length 3 content `[22, 44, 66]`
vec0 has length 4 content `[22, 44, 66, 88]`
====================
So which Solution is Best?
The best solution depends on your specific requirements:
- If you need to keep the original vector unchanged and create a modified version, choose either solution 1 or 2. Solution 2 is more idiomatic Rust.
- If you don't need the original vector in its initial state and want to modify it directly, solution 3 is the best choice, as it avoids creating a new vector and is more efficient.
In general, solution 3 is the most efficient because it directly modifies the original vector without creating a new one. However, this may not be suitable for every use case, as it changes the original vector's content. Let's move on to the next exercise.
move_semantics3
// move_semantics3.rs
// Make me compile without adding new lines-- just changing existing lines!
// (no lines with multiple semicolons necessary!)
// Execute `rustlings hint move_semantics3` or use the `hint` watch subcommand for a hint.
// I AM NOT DONE
fn main() {
let vec0 = Vec::new();
let mut vec1 = fill_vec(vec0);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
vec1.push(88);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}
fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
vec.push(22);
vec.push(44);
vec.push(66);
vec
}
This exercise says we can't add any additional lines of code, but we can change the lines. Let's take a look at what the compiler is saying.
move_semantics3
errors
⚠️ Compiling of exercises/move_semantics/move_semantics3.rs failed! Please try again. Here is the output:
error[E0596]: cannot borrow `vec` as mutable, as it is not declared as mutable
--> exercises/move_semantics/move_semantics3.rs:20:13
|
20 | fn fill_vec(vec: Vec<i32>) -> Vec<i32> {
| ^^^ not mutable
21 | vec.push(22);
| ------------ cannot borrow as mutable
22 | vec.push(44);
| ------------ cannot borrow as mutable
23 | vec.push(66);
| ------------ cannot borrow as mutable
|
help: consider changing this to be mutable
|
20 | fn fill_vec(mut vec: Vec<i32>) -> Vec<i32> {
| +++
error: aborting due to previous error
As always we get great help from the compiler. This seems pretty straight forward, let's do what the compiler suggests and see what happens.
move_semantics3
solution
fn main() {
let vec0 = Vec::new();
let mut vec1 = fill_vec(vec0);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
vec1.push(88);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}
// adding `mut` to the `vec` paramater
fn fill_vec(mut vec: Vec<i32>) -> Vec<i32> {
vec.push(22);
vec.push(44);
vec.push(66);
vec
}
Success!
Output:
====================
vec1 has length 3 content `[22, 44, 66]`
vec1 has length 4 content `[22, 44, 66, 88]`
====================
Following the Rust compiler's help is very easy in this case, if we notice the code is very similar to the previous exercise but this code is missing the let mut vec = vec;
line from the fn fill_vec
function.
Wrapping Up: The Rustlings Move Semantics Journey Continues!
There you have it! We've explored the first three move_semantics
exercises from the Rustlings course. To keep these blog posts digestible, we'll be diving into the next 3 move_semantics
challenges in a separate post, so keep an eye out for that!
Now, let's do a quick recap of what we've covered in this post: we guided you through the first 3 move_semantics
exercises, shedding light on how to tackle each problem by interpreting the error messages and hints provided. We also compared various solutions for each exercise, highlighting the trade-offs and stressing the importance of choosing the right method based on your code's specific needs. Our focus was on understanding the concepts of ownership, borrowing, and mutable borrowing in Rust – essential ingredients for writing efficient and safe code.
By working through these exercises and comprehending the solutions, you're now better prepared to handle ownership and borrowing situations in your Rust projects. Stay tuned for more Rustling adventures!