In Rust, the first basic principle of memory is ownership: a value belongs to its variable. In this section we will look at what happens when:
We clone a value.
We pass a value to another variable or a function.
A function returns a value.
19.1.1Cloning¶
The Clone trait (see Traits chapter), when implemented
by a type, allows the explicit copy of an object through the use of
the clone() method.
Depending on the implementation of the clone() method, the copy may be
done:
Using a raw copy of the memory. The memory zone where data is stored is copied exactly bit by bit, without any knowledge of the data’s structure.
Using a clever copy of the data. The knowledge of the data’s structure is used to achieve an elaborated copy that takes into account:
Allocated heap memory.
Opened resources (file handles, sockets, …).
Complex structures: lists, trees, graphs, …
Resetting of specific fields that must not be copied.
Here is an example in which we clone a String object (the String type
allocates memory on the heap for storing a character string):
let mut s1 = String::from("hello");
let s2 = s1.clone();
s1.push_str(", world!");
println!("s1 = {s1}, s2 = {s2}");Couldn't automatically determine type of variable `s2`.
Please give it an explicit type.19.1.2Passing a value¶
When a value is passed from a variable to another variable or a function, one of two things happen:
Either it is copied.
Or it is moved.
Both behaviours are implicitly selected by the compiler, and the
choice between them depends on the implementation or not of the Copy
trait by the value’s type.
If the value’s type implements the Copy trait, the value is copied,
otherwise it is moved.
With either copy or move the destination gets the ownership of the
data:
A new value (copied) in case of a copy.
The original value in case of a move.
19.1.2.1Copying¶
The Copy trait specializes the Clone trait (i.e.: it implements the
clone() method) by making a raw copy of the memory (memory bytes copy).
The copy has thus no knowledge of the data structure.
To implement the Copy trait, a type must NOT:
Use heap allocation.
Require any form of resource management (like file handles or locks).
Implement the
Droptrait, as it is used for cleanup when values go out of scope.
The built-in types that implement Copy are:
All integer types:
i8,i16,i32,i64,i128,isize,u8,u16,u32,u64,u128,usize.All floating-point types:
f32,f64.The boolean type:
bool.The character type:
char.Tuples, if all their fields implement
Copy(e.g.:(i32, bool))Arrays, if their element type implements
Copy(e.g.:[i32; 5])
In the following example, the table a is implicitly copied into b:
let a = [12, -7, 6];
let b = a;
(a, b)19.1.2.2Moving¶
When its value is moved, a variable loses the ownership and cannot be used anymore.
In the following example, s cannot be used anymore once its value is
passed to t:
let s = String::from("abc");
let t = s;
(s, t)19.1.3Passing to a function¶
The same copy/move mechanism applies when passing values to functions.
Here is an example with a String value that is passed to a function:
fn foo(s: String) {
println!("{}, world!", s);
}
let s1 = String::from("hello");
foo(s1);
let s2 = s1;
s2What happens is the following:
Ownership of
s1is passed tofoo()(i.e.: the value is moved) ass1does not have theCopytrait.s1is dropped at the end offoo().s1is no more valid inside the scope after the call tofoo().
If s1 is needed after the call to foo(), one solution that we have
seen above is to clone the value:
fn foo(s: String) {
println!("{}, world!", s);
}
let s1 = String::from("hello");
foo(s1.clone());
let s2 = s1;
s219.1.4Getting a variable from a function¶
The return value of a function is moved or copied in the same way we have seen.
In the following example the value returned by foo() is moved into
s1:
fn foo() -> String {
String::from("Hello, world!")
}
let s1 = foo();
s119.1.5Modifying a value with a function¶
Using what we have seen above, we are able to write a function that computes a new value and returns it. We can use this function to overwrite (i.e.: we use the variable shadowing feature, see Shadowing a variable) our variable with a new value:
fn inc(n: i16) -> i16 {
n + 1
}
let a = 5;
let a = inc(a);
aWhat we have done in the previous example is:
Pass a value to function
inc().Compute a new value inside the function.
Define a new variable
a, shadowing the previousavariable, and initialize it with the returned value ofinc().
It looks like we have modified a, while all we have done is redefining
a new variable of the same name. Both version of the variable a are
immutable.
Making the variable mutable, we can reset its value:
let mut b = 7;
b = inc(b);
bWe can do the same with a type that allocates memory on the heap:
fn foo(s: String) -> String {
format!("{s}def")
}
let mut s = String::from("abc");
s = foo(s);
sSince the ownership of the value is passed to foo(), it is also
possible to declare the foo’s parameter as mutable so we can modify it
in-place:
fn foo(mut s: String) -> String {
s.push_str("def");
s
}
let mut s = String::from("abc");
s = foo(s);
s