Possession
Mécanisme de possession (ownership)
En Rust, la mémoire est gérée avec un mécanisme de possession (ownership) qui est vérifié au moment de la compilation. Les règles de l'ownership sont les suivantes:
- chaque valeur en Rust a un propriétaire (owner)
- il ne peut y avoir qu'un seul propriétaire de cette valeur à un instant t
- quand le propriétaire est hors de portée, la valeur est détruite
Pour illustrer ce concept, nous utiliserons le type String
qui est stocké sur
le tas (heap). Nous avons déjà utilisé des chaînes de caractères mais ces
dernières, de type &str
, ont un contenu immutable:
fn print_type_of<T>(_: &T) { println!("{}", std::any::type_name::<T>()) } fn main() { let mut x = "hello"; x = "world"; print_type_of(&x); println!("{}", x); }
La fonction from
de String
permet de créer une chaîne de caractère dont on
va pouvoir modifier le contenu:
fn print_type_of<T>(_: &T) { println!("{}", std::any::type_name::<T>()) } fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str() appends a literal to a String println!("{}", s); // This will print `hello, world!` print_type_of(&s); }
Examinons la relation entre une variable s1
et la mémoire dans le cas de la
déclaration suivante:
#![allow(unused)] fn main() { let s1 = String::from("hello"); }
Dans le cas d'un type String
, la mémoire utilisée est de deux types: une
structure, stockée sur la pile, contenant ta taille maximale de la chaîne de
caractère, le nombre de caractères actuels, ainsi qu'un pointeur vers une zone
mémoire sur le tas (heap) contenant la chaîne elle-même:
Si l'on écrit le code suivant, regardons ce qui se passe au niveau de la mémoire:
let s1 = String::from("hello"); let s2 = s1;
En mémoire, la structure contenant les informations sur la chaîne est dupliquée, à la différence de la chaîne elle-même se trouvant sur le tas:
Si l'on fait un parallèle avec d'autres langages comme C++, on parlera de shallow copy: une partie de la mémoire est dupliquée, à la différence d'une deep copy.
En Rust, la mémoire avec laquelle la variable est liée est
automatiquement détruire lorsque la variable devient hors de portée: Rust
appelle la méthode drop
qui rend (au système d'exploitation) la mémoire allouée sur le
tas. Sur le schéma ci-dessus, la question de pose de savoir qui de s1
et s2
doit posséder la zone mémoire et doit donc être responsable de sa destruction,
afin d'éviter les double free.
Pour résoudre ce problème, Rust considère qu'après la ligne:
#![allow(unused)] fn main() { let s2 = s1; }
la variable s1
n'est plus valable. En effet, pour toutes les données possédant
une méthode drop
, une affectation correspond à un déplacement (move)
plutôt qu'à une copie (copy): l'emplacement mémoire de départ est donc
invalidé.
fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 n'est plus valide println!("{}", s1); println!("{}", s2); }
Le schéma de la mémoire après l'affectation de s1
à s2
est donc le suivant:
Rust, par défaut, ne fera jamais de copie profonde des données stockées sur le tas. Cette copie doit être faite de manière explicite avec clone():
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("{}", s1); println!("{}", s2); }
Le code ci-dessus correspondant à l'organisation de la mémoire suivante:
Implémentation des traits Copy
et Drop
Revenons au code exemple que nous avions vu précédemment:
fn main() { let x = 5; let y = x; println!("x = {}, y = {}", x, y); }
Ici, il n'est pas nécessaire d'utiliser clone()
car pour les types de données
de base (comme les entiers), la taille est connue au moment de la compilation.
Ces types de données donnent donc lieu à une allocation statique effectuée sur
la pile, les notions de copie profonde et de shallow copy ont ici le même
comportement: les données, dans le cas d'une affectation sont bien copiées.
Ces types de données implémentent le
trait Copy
, régissant
le comportement de l'affectation. Les données de type String
implémentent
quant à elles le trait Drop
: ces données impliquent une affectation de type
déplacement plutôt qu'une copie: elles seront donc régies par le mécanisme de
possession. Rust ne permet pas qu'un type implémentant Drop
implémente le
type Copy
.
Parmi les types implémentant le trait Copy, on trouve:
- Tous les types entiers, tel que
u32
- Le type booléen
bool
- Tous les types flottants, comme
f64
- Le type caractère
char
- Les tuples, seulement si tous leurs membres implémentent
Copy
. Par exemple,(i32, i32)
implémenteCopy
, mais pas(i32, String)
Possession et fonctions
Le passage d'un paramètre à une fonction utilise le même mécanisme que
l'affectation. Dans l'exemple ci-dessous, le fait de passer s
en paramètre à
la fonction takes_ownership
transfère la propriété de s
au paramètre
some_string
de la fonction. Lorsque l'on sort de la fonction, comme
some_string
est hors de portée, la mémoire associée est rendue. Pour une
variable entière le comportement est différent puisque le passage au paramètre
some_integer
de la fonction makes_copy
se fait en effectuant une copie.
fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // x would move into the function, // but i32 is Copy, so it's okay to still // use x afterward } // Here, x goes out of scope, then s. But because s's value was moved, nothing // special happens. fn takes_ownership(some_string: String) { // some_string comes into scope println!("{}", some_string); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{}", some_integer); } // Here, some_integer goes out of scope. Nothing special happens.
Il est tout à fait possible de transférer la possession à une fonction et de la récupérer ensuite. Il suffit de retourner les données:
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String::from("yours"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns one fn takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function }
Nous avons donc trois possibilités: une fonction peut prendre la possession de données, la donner, ou la prendre puis la rendre. Notons un dernier mécanisme: une fonction peut tout à fait retourner un tuple, permettant ainsi de retourner plusieurs valeurs.
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{}' is {}.", s2, len); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String (s, length) }