The aim of the exercise is simple. Start with an empty vector of unsigned integers - as it does not implement the Copy
Trait - and create different types of functions and closures to push a new number to the original vector. Now that the goal is set, let’s create and instantiate an empty vector:
let mut v : Vec<u8> = Vec::new();
println!("Address: {:p}, Content: {:?}", &v, &v);
// Address: 0xb62dfffb10, Content: []
We will keep track of the address of the v
variable to ensure the same instance is used throughout. Let’s introduce the first function that serves the purpose.
Function on a mutable variable
fn push1(mut x : Vec<u8>) -> Vec<u8> {
x.push(1);
x
}
v = push1(v);
println!("Address: {:p}, Content: {:?}", &v, &v);
// Address: 0xb62dfffb10, Content: [1]
How does push1
work exactly? Well, we can say based on its signature that:
-
It takes ownership of the passed variable
x
(by definition) -
It is allowed to modify the passed variable
x
(because of themut
keyword)
Calling push1(x)
means that x
moves to the function’s scope, and gets destroyed right after the function has finished executing. So we need to return the modified variable and reassign it if we want to be able to use it in the rest of the program. As the printed output shows, the address has been preserved proving that we worked on the same vector instance.
Function on a mutable reference
fn push2(x : &mut Vec<u8>) {
x.push(2);
}
push2(&mut v);
println!("Address: {:p}, Content: {:?}", &v, &v);
// Address: 0xb62dfffb10, Content: [1,2]
The function push2
takes a mutable reference as argument. Its signature tells us that:
-
It borrows the passed reference
x
(by defintion) -
It is allowed to modify the passed reference
x
(because of themut
keyword)
Since the variable v
is of type Vec<u8>
, we need to cast it into a mutable reference during the function call by passing &mut v
as argument. The vector address remains unchanged based on the very definition of variable referencing. Let’s now take a look at closures.
Closure using a mutable variable
let mut push3 = || v.push(3);
push3();
println!("Address: {:p}, Content: {:?}", &v, &v);
// Address: 0xb62dfffb10, Content: [1,2,3]
As opposed to functions, closures have access to variables defined outside their scope. The closure push3
is defined as mutable which means that:
-
It has direct access to the original variable
v
(same as borrowing in effect) -
It is allowed to modify the original variable
v
(thanks to themut
keyword)
Since the closure works directly on the instance of variable v
, the address is indeed the same post execution.
Closure with “move” keyword
let push4 = move || {
v.push(4);
v
};
v = push4();
println!("Address: {:p}, Content: {:?}", &v, &v);
// Address: 0xb62dfffb10, Content: [1,2,3,4]
The move
keyword creates a closure that takes ownership of the variables defined outside its scope, if they are being used within the closure. We can then say about the closure that:
-
It takes ownership of the variable
v
(because of themove
keyword) -
It is allowed to modify the variable
v
(becausev
is mutable)
Similarly to the push1
function, because it took ownership of the variable v
, the closure needs to return the modified vector so it can be reassigned and used in the rest of the program.
If you like this post, follow me on Twitter and get notified on the next posts.