07 Rustlings Move Semantics Part 2 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.
Move Semantics Part 2
We've already covered the first three exercises of Move Semantics in part one, in this episode we tackle the next three. Let's get started!
move_semantics4.rs
// move_semantics4.rs
// Refactor this code so that instead of passing `vec0` into the `fill_vec` function,
// the Vector gets created in the function itself and passed back to the main
// function.
// Execute `rustlings hint move_semantics4` 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);
}
// `fill_vec()` no longer takes `vec: Vec<i32>` as argument
fn fill_vec() -> Vec<i32> {
let mut vec = vec;
vec.push(22);
vec.push(44);
vec.push(66);
vec
}
Our instructions here are to refactor our code so that instead of passing vec0
into the fill_vec
function, the Vector gets created in the function itself and passed back to the main function.
move_semantics4.rs
errors
⚠️ Compiling of exercises/move_semantics/move_semantics4.rs failed! Please try again. Here is the output:
error[E0423]: expected value, found macro `vec`
--> exercises/move_semantics/move_semantics4.rs:23:19
|
23 | let mut vec = vec;
| ^^^ not a value
error[E0061]: this function takes 0 arguments but 1 argument was supplied
--> exercises/move_semantics/move_semantics4.rs:12:20
|
12 | let mut vec1 = fill_vec(vec0);
| ^^^^^^^^ ---- argument of type `Vec<_>` unexpected
|
note: function defined here
--> exercises/move_semantics/move_semantics4.rs:22:4
|
22 | fn fill_vec() -> Vec<i32> {
| ^^^^^^^^
help: remove the extra argument
|
12 | let mut vec1 = fill_vec();
| ~~
error: aborting due to 2 previous errors
Error's are telling us that on line 23, vec
is not a value in addition it tells us that the function fill_vec()
takes 0 arguments but one is being supplied. So let's remove the argument on line 12 from the fill_vec(vec0)
.
After we do that we still get some errors, but they're different now.
⚠️ Compiling of exercises/move_semantics/move_semantics4.rs failed! Please try again. Here is the output:
error[E0423]: expected value, found macro `vec`
--> exercises/move_semantics/move_semantics4.rs:23:19
|
23 | let mut vec = vec;
| ^^^ not a value
error[E0282]: type annotations needed for `Vec<T>`
--> exercises/move_semantics/move_semantics4.rs:10:9
|
10 | let vec0 = Vec::new();
| ^^^^
|
help: consider giving `vec0` an explicit type, where the type for type parameter `T` is specified
|
10 | let vec0: Vec<T> = Vec::new();
| ++++++++
error: aborting due to 2 previous errors
The error's start point us to using generics with Vec<T>
but our solution should be must simpler than that, since our instructions are to refactor our code to not create vec0
but to create a new vector in our fill_vec()
function.
Let's see what happens if we simple remove the let vec0 = Vec::new();
line from our code but and add the let mut vec = Vec::new();
to our fill_vec()
function. Hey guess what? It works. Below is the update code solution.
move_semantics4
solution
fn main() {
// removed previous vector creation here
let mut vec1 = fill_vec();
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
vec1.push(88);
println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}
fn fill_vec() -> Vec<i32> {
let mut vec = Vec::new(); // added vector creation within the function
vec.push(22);
vec.push(44);
vec.push(66);
vec
}
This is our output:
Output:
====================
vec1 has length 3 content `[22, 44, 66]`
vec1 has length 4 content `[22, 44, 66, 88]`
====================
move_semantics5.rs
// move_semantics5.rs
// Make me compile only by reordering the lines in `main()`, but without
// adding, changing or removing any of them.
// Execute `rustlings hint move_semantics5` or use the `hint` watch subcommand for a hint.
// I AM NOT DONE
fn main() {
let mut x = 100;
let y = &mut x;
let z = &mut x;
*y += 100;
*z += 1000;
assert_eq!(x, 1200);
}
This looks like an easy one, no need to write anything we just need to re-order the sequence of the lines. Let's look at the errors and see if we can get any hints on what we need to do.
move_semantics5.rs
errors
⚠️ Compiling of exercises/move_semantics/move_semantics5.rs failed! Please try again. Here is the output:
error[E0499]: cannot borrow `x` as mutable more than once at a time
--> exercises/move_semantics/move_semantics5.rs:11:13
|
10 | let y = &mut x;
| ------ first mutable borrow occurs here
11 | let z = &mut x;
| ^^^^^^ second mutable borrow occurs here
12 | *y += 100;
| --------- first borrow later used here
The Rust compiler tells us that we are borrowing x
mutably too many times, so let's go step by step and see what is happening on each line.
- We declare a mutable variable
x
and assign it the value100
. - We create a mutable reference
y
that borrowsx
using&mut x
. - Then, we create another mutable reference
z
that also borrowsx
using&mut x
. This is where the problem arises.- The rules of borrowing state that you can have either one mutable reference or any number of immutable references to a value at a given time.
- In this case, we already have
y
as a mutable reference tox
, so we can't create another mutable referencez
.
- The code tries to dereference
y
using*y
and add100
to the value ofx
. This is invalid becausey
is still in scope and holds a mutable reference tox
, and at this point,z
also exists. - Similarly, when the code tries to dereference
z
using*z
and add1000
to the value ofx
, it violates the borrowing rules.
So how do we solve this? Let's try by dereferencing y
before we try to borrow it again with z
by moving the line *y += 100;
above the 2nd attempt mutable borrow which is let z = &mut x;
. Doing so should allow us to compile.
move_semantics5.rs
solution
fn main() {
let mut x = 100;
let y = &mut x;
*y += 100;
let z = &mut x;
*z += 1000;
assert_eq!(x, 1200);
}
It compiles! We don't get an output because there is not println!
statement instead we have an assert_eq!
.
The code compiles successfully because it follows the borrowing rules in Rust. Here's a step-by-step explanation:
- We start by declaring a mutable variable
x
and assigning it the value100
. - We then create a mutable reference
y
that borrowsx
using&mut x
. This allows us to modifyx
throughy
. - We dereference
y
using*y
and add100
to the value ofx
. This modifiesx
to200
. - Next, we create another mutable reference
z
that also borrowsx
using&mut x
. This is allowed because there are no other references tox
at this point. - We dereference
z
using*z
and add1000
to the value ofx
. This modifiesx
to1200
. - Finally, we use
assert_eq!
to check ifx
is equal to1200
. Since the value ofx
is indeed1200
, the assertion passes.
Let's move on to our final move_semantics
exercise.
move_semantics6.rs
// move_semantics6.rs
// Execute `rustlings hint move_semantics6` or use the `hint` watch subcommand for a hint.
// You can't change anything except adding or removing references.
// I AM NOT DONE
fn main() {
let data = "Rust is great!".to_string();
get_char(data);
string_uppercase(&data);
}
// Should not take ownership
fn get_char(data: String) -> char {
data.chars().last().unwrap()
}
// Should take ownership
fn string_uppercase(mut data: &String) {
data = &data.to_uppercase();
println!("{}", data);
}
Our instructions are to not change anything but the references, so we'll look at the errors to get a better understanding as to where we are having issues.
move_semantics6.rs
errors
⚠️ Compiling of exercises/move_semantics/move_semantics6.rs failed! Please try again. Here is the output:
error[E0382]: borrow of moved value: `data`
--> exercises/move_semantics/move_semantics6.rs:12:22
|
8 | let data = "Rust is great!".to_string();
| ---- move occurs because `data` has type `String`, which does not implement the `Copy` trait
9 |
10 | get_char(data);
| ---- value moved here
11 |
12 | string_uppercase(&data);
| ^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `get_char` to borrow instead if owning the value is not necessary
--> exercises/move_semantics/move_semantics6.rs:16:19
|
16 | fn get_char(data: String) -> char {
| -------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
10 | get_char(data.clone());
| ++++++++
error[E0716]: temporary value dropped while borrowed
--> exercises/move_semantics/move_semantics6.rs:22:13
|
21 | fn string_uppercase(mut data: &String) {
| - let us call the lifetime of this reference `'1`
22 | data = &data.to_uppercase();
| --------^^^^^^^^^^^^^^^^^^^- temporary value is freed at the end of this statement
| | |
| | creates a temporary value which is freed while still in use
| assignment requires that borrow lasts for `'1`
error: aborting due to 2 previous errors
Alright let's go down the list understanding what there errors are telling us and see how we can fix them.
8 | let data = "Rust is great!".to_string();
| ---- move occurs because `data` has type `String`, which does not implement the `Copy` trait
9 |
10 | get_char(data);
| ---- value moved here
11 |
12 | string_uppercase(&data);
| ^^^^^ value borrowed here after move
note: consider changing this parameter type in function `get_char` to borrow instead if owning the value is not necessary
--> exercises/move_semantics/move_semantics6.rs:16:19
here the compiler is telling us clearly where to look data
does not implement the copy trait so we when we pass it through as a parameter in get_char(data),
it becomes owned by get_char()
In the next batch of errors we get a suggestion about cloning, but we know that we can't change any of the code other than changing the references, so this is not the path we want to take.
16 | fn get_char(data: String) -> char {
| -------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
10 | get_char(data.clone());
| ++++++++
error[E0716]: temporary value dropped while borrowed
|
21 | fn string_uppercase(mut data: &String) {
| - let us call the lifetime of this reference `'1'`
22 | data = &data.to_uppercase();
| --------^^^^^^^^^^^^^^^^^^^- temporary value is freed at the end of this statement
| | |
| | creates a temporary value which is freed while still in use
| assignment requires that borrow lasts for `'1`
We now get a message about the lifetime of a reference which we haven't covered yet so let's just keep this in mind for now, but again our task is to essentially just change how the functions handle ownership. So let's go back and look at the functions in the code:
// Should not take ownership
fn get_char(data: String) -> char {
data.chars().last().unwrap()
}
// Should take ownership
fn string_uppercase(mut data: &String) {
data = &data.to_uppercase();
println!("{}", data);
}
Looking at these two code blocks it looks straightforward, it's clear that we must change where the &
symbol is being used and essentially swap positions in each function to this:
// Should not take ownership
fn get_char(data: &String) -> char {
data.chars().last().unwrap()
}
// Should take ownership
fn string_uppercase(mut data: String) {
data = data.to_uppercase();
println!("{}", data);
}
Once we've done this we get new error's but it should be pretty clear what we need to do in the fn main()
⚠️ Compiling of exercises/move_semantics/move_semantics6.rs failed! Please try again. Here is the output:
error[E0308]: mismatched types
--> exercises/move_semantics/move_semantics6.rs:10:14
|
10 | get_char(data);
| -------- ^^^^
| | |
| | expected `&String`, found struct `String`
| | help: consider borrowing here: `&data`
| arguments to this function are incorrect
|
note: function defined here
--> exercises/move_semantics/move_semantics6.rs:16:4
|
16 | fn get_char(data: &String) -> char {
| ^^^^^^^^ -------------
error[E0308]: mismatched types
--> exercises/move_semantics/move_semantics6.rs:12:22
|
12 | string_uppercase(&data);
| ---------------- ^^^^^ expected struct `String`, found `&String`
| |
| arguments to this function are incorrect
|
note: function defined here
--> exercises/move_semantics/move_semantics6.rs:21:4
|
21 | fn string_uppercase(mut data: String) {
| ^^^^^^^^^^^^^^^^ ----------------
help: consider removing the borrow
|
12 - string_uppercase(&data);
12 + string_uppercase(data);
|
error: aborting due to 2 previous errors
The compiler gives us great information on what we should do literally showing us what we can do to make the code compile. So let's try it.
move_semantics6.rs
solution
fn main() {
let data = "Rust is great!".to_string();
get_char(&data);
string_uppercase(data);
}
// Should not take ownership
fn get_char(data: &String) -> char {
data.chars().last().unwrap()
}
// Should take ownership
fn string_uppercase(mut data: String) {
data = data.to_uppercase();
println!("{}", data);
}
There we have it our solution is to again swap the &
symbol's position to match that of the function's signature to make sure that we are borrowing and taking ownership as the function expects. With that we get our print out:
Output:
====================
RUST IS GREAT!
====================
Conclusion
Rust's move semantics are important for understanding memory management and ownership in the language. By leveraging references, borrowing, and ownership, Rust ensures memory safety and eliminates many common programming errors like null pointer dereferences and dangling references.
In this blog post, we explored three exercises related to move semantics. We refactored code, re-ordered lines, and adjusted ownership to solve the problems. Through these exercises, we gained a better understanding of how move semantics work in Rust and how to manipulate ownership and references effectively.
Move semantics play a crucial role in Rust's design philosophy, enabling high-performance and safe code without sacrificing expressiveness. By embracing move semantics and mastering the intricacies of ownership, borrowing, and references, Rust developers can write robust and efficient code.
Remember, practice is key to mastering move semantics and other advanced features of Rust. Keep exploring, experimenting, and building projects to deepen your understanding and become a proficient Rust programmer. Happy coding!