Qu’est-ce que la possession ?
La possession (ownership) est un ensemble de règles qui regissent la façon dont un programme Rust gère la mémoire. Tous les programmes doivent gérer la façon dont ils utilisent la mémoire d’un ordinateur pendant leur exécution. Certains langages disposent d’un ramasse-miettes (garbage collector) qui recherche regulierement la mémoire inutilisee pendant l’exécution du programme ; dans d’autres langages, le programmeur doit explicitement allouer et libérer la mémoire. Rust utilise une troisième approche : la mémoire est gérée par un système de possession avec un ensemble de règles que le compilateur vérifie. Si l’une des règles est violee, le programme ne compilera pas. Aucune des fonctionnalités de la possession ne ralentira votre programme pendant son exécution.
Comme la possession est un concept nouveau pour de nombreux programmeurs, il faut un certain temps pour s’y habituer. La bonne nouvelle, c’est que plus vous acquerrez de l’experience avec Rust et les règles du système de possession, plus vous trouverez facile de développer naturellement du code sur et efficace. Perseverez !
Lorsque vous comprendrez la possession, vous disposerez d’une base solide pour comprendre les fonctionnalités qui rendent Rust unique. Dans ce chapitre, vous apprendrez la possession en travaillant sur des exemples qui se concentrent sur une structure de données très courante : les chaînes de caractères (strings).
La pile et le tas
De nombreux langages de programmation ne vous demandent pas de penser souvent à la pile (stack) et au tas (heap). Mais dans un langage de programmation système comme Rust, le fait qu’une valeur soit sur la pile ou sur le tas affecte le comportement du langage et explique pourquoi vous devez prendre certaines décisions. Des aspects de la possession seront décrits en relation avec la pile et le tas plus tard dans ce chapitre, voici donc une brève explication en guise de préparation.
La pile et le tas sont tous deux des parties de la mémoire disponibles pour votre code à l’exécution, mais ils sont structurés de manières différentes. La pile stocké les valeurs dans l’ordre où elle les reçoit et les retire dans l’ordre inverse. C’est ce qu’on appelle dernier entré, premier sorti (LIFO, last in, first out). Pensez à une pile d’assiettes : quand vous en ajoutez, vous les posez sur le dessus, et quand vous avez besoin d’une assiette, vous en prenez une sur le dessus. Ajouter ou retirer des assiettes du milieu ou du bas ne fonctionnerait pas aussi bien ! Ajouter des données s’appelle empiler sur la pile, et en retirer s’appelle dépiler de la pile. Toutes les données stockées sur la pile doivent avoir une taille fixe et connue. Les données de taille inconnue à la compilation ou de taille pouvant changer doivent être stockées sur le tas à la place.
Le tas est moins organisé : quand vous placez des données sur le tas, vous demandez une certaine quantité d’espace. L’allocateur de mémoire trouve un emplacement vide dans le tas qui est assez grand, le marque comme étant utilisé, et renvoie un pointeur, qui est l’adresse de cet emplacement. Ce processus s’appelle allouer sur le tas et est parfois abrégé en simplement allouer (empiler des valeurs sur la pile n’est pas considéré comme une allocation). Comme le pointeur vers le tas à une taille connue et fixe, vous pouvez stocker le pointeur sur la pile, mais quand vous voulez les données réelles, vous devez suivre le pointeur. Imaginez que vous arriviez dans un restaurant. À l’entrée, vous indiquez le nombre de personnes dans votre groupe, et le personnel trouve une table vide qui peut accueillir tout le monde et vous y mène. Si quelqu’un dans votre groupe arrive en retard, il peut demander où vous avez été assis pour vous trouver.
Empiler sur la pile est plus rapide qu’allouer sur le tas car l’allocateur n’a jamais besoin de chercher un endroit pour stocker de nouvelles données ; cet endroit est toujours au sommet de la pile. En comparaison, allouer de l’espace sur le tas demande plus de travail car l’allocateur doit d’abord trouver un espace assez grand pour contenir les données, puis effectuer de la comptabilité pour préparer la prochaine allocation.
Accéder aux données sur le tas est généralement plus lent qu’accéder aux données sur la pile car il faut suivre un pointeur pour y arriver. Les processeurs contemporains sont plus rapides s’ils effectuent moins de sauts en mémoire. Pour continuer l’analogie, imaginez un serveur dans un restaurant qui prend les commandes de plusieurs tables. Il est plus efficace de prendre toutes les commandes à une table avant de passer à la suivante. Prendre une commande à la table A, puis une à la table B, puis revenir à la table A, puis à la table B serait un processus bien plus lent. De la même façon, un processeur peut généralement mieux faire son travail s’il travaille sur des données proches les unes des autres (comme sur la pile) plutôt qu’éloignées (comme cela peut être le cas sur le tas).
Quand votre code appelle une fonction, les valeurs passées à la fonction (y compris, potentiellement, des pointeurs vers des données sur le tas) et les variables locales de la fonction sont empilées sur la pile. Quand la fonction se terminé, ces valeurs sont dépilées.
Garder la trace de quelles parties du code utilisent quelles données sur le tas, minimiser la quantité de données dupliquées sur le tas, et nettoyer les données inutilisées sur le tas pour ne pas manquer d’espace sont tous des problèmes que la possession résout. Une fois que vous comprendrez la possession, vous n’aurez plus besoin de penser souvent à la pile et au tas, mais savoir que le but principal de la possession est de gérer les données du tas peut aider à expliquer pourquoi elle fonctionne ainsi.
Les règles de la possession
Tout d’abord, jetons un coup d’oeil aux règles de la possession. Gardez ces règles à l’esprit pendant que nous travaillons sur les exemples qui les illustrent :
- Chaque valeur en Rust à un proprietaire (owner).
- Il ne peut y avoir qu’un seul propriétaire à la fois.
- Quand le propriétaire sort de la portée, la valeur est supprimee (dropped).
La portée des variables
Maintenant que nous avons depasse la syntaxe de base de Rust, nous n’inclurons pas tout le code fn main() { dans les exemples, donc si vous suivez, assurez- vous de placer les exemples suivants à l’intérieur d’une fonction main manuellement. En consequence, nos exemples seront un peu plus concis, ce qui nous permettra de nous concentrer sur les détails importants plutot que sur le code repetitif.
Comme premier exemple de possession, nous allons examiner la portée de certaines variables. Une portee (scope) est la zone au sein d’un programme dans laquelle un élément est valide. Prenons la variable suivante :
#![allow(unused)]
fn main() {
let s = "hello";
}
La variable s fait référence à un littéral de chaîne de caractères, dont la valeur est codee en dur dans le texte de notre programme. La variable est valide à partir du moment où elle est déclarée jusqu’à la fin de la portée courante. Le listing 4-1 montre un programme avec des commentaires indiquant ou la variable s serait valide.
fn main() {
{ // s is not valid here, since it's not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
}
En d’autres termes, il y a ici deux moments importants :
- Quand
sentre dans la portée, elle est valide. - Elle reste valide jusqu’à ce qu’elle sorte de la portée.
A ce stade, la relation entre les portées et la validite des variables est similaire a celle des autres langages de programmation. Nous allons maintenant approfondir cette comprehension en introduisant le type String.
Le type String
Pour illustrer les règles de la possession, nous avons besoin d’un type de données plus complexe que ceux que nous avons couverts dans la section [“Les types de données”][data-types] du chapitre 3. Les types couverts precedemment ont une taille connue, peuvent être stockés sur la pile et depiles quand leur portée est terminée, et peuvent être copies rapidement et trivialement pour créer une nouvelle instance indépendante si une autre partie du code a besoin d’utiliser la même valeur dans une portée différente. Mais nous voulons examiner des données stockées sur le tas et explorer comment Rust sait quand nettoyer ces données, et le type String en est un excellent exemple.
Nous nous concentrerons sur les aspects de String qui sont lies à la possession. Ces aspects s’appliquent également a d’autres types de données complexes, qu’ils soient fournis par la bibliothèque standard ou créés par vous. Nous aborderons les aspects de String non lies à la possession au [chapitre 8][ch8].
Nous avons déjà vu les littéraux de chaînes de caractères, ou une valeur de chaîne est codee en dur dans notre programme. Les littéraux de chaînes sont pratiques, mais ils ne conviennent pas à toutes les situations ou nous pourrions vouloir utiliser du texte. L’une des raisons est qu’ils sont immuables. Une autre est que toutes les valeurs de chaînes ne peuvent pas être connues au moment où nous écrivons notre code : par exemple, que faire si nous voulons prendre une saisie utilisateur et la stocker ? C’est pour ces situations que Rust dispose du type String. Ce type gère des données allouées sur le tas et est donc capable de stocker une quantite de texte qui nous est inconnue à la compilation. Vous pouvez créer une String à partir d’un littéral de chaîne en utilisant la fonction from, comme ceci :
#![allow(unused)]
fn main() {
let s = String::from("hello");
}
L’opérateur double deux-points :: nous permet d’espacer cette fonction from particuliere sous le type String plutot que d’utiliser un nom comme string_from. Nous discuterons de cette syntaxe plus en détail dans la section [“Les methodes”][methods] du chapitre 5, et quand nous parlerons de l’espacement de noms avec les modules dans [“Les chemins pour faire référence à un élément dans l’arborescence des modules”][paths-module-tree] au chapitre 7.
Ce type de chaîne peut être modifié : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-01-can-mutate-string/src/main.rs:here}}
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!`
}
Alors, quelle est la différence ici ? Pourquoi String peut-elle être modifiée mais pas les littéraux ? La différence reside dans la façon dont ces deux types gèrent la mémoire.
Mémoire et allocation
Dans le cas d’un littéral de chaîne, nous connaissons le contenu à la compilation, donc le texte est code en dur directement dans l’exécutable final. C’est pourquoi les littéraux de chaînes sont rapides et efficaces. Mais ces proprietes ne viennent que de l’immutabilite du littéral de chaîne. Malheureusement, nous ne pouvons pas placer un bloc de mémoire dans le binaire pour chaque morceau de texte dont la taille est inconnue à la compilation et dont la taille pourrait changer pendant l’exécution du programme.
Avec le type String, pour prendre en charge un morceau de texte modifiable et extensible, nous devons allouer une quantite de mémoire sur le tas, inconnue à la compilation, pour contenir le contenu. Cela signifie :
- La mémoire doit être demandee à l’allocateur de mémoire à l’exécution.
- Nous avons besoin d’un moyen de rendre cette mémoire à l’allocateur quand nous avons terminé avec notre
String.
La première partie est faite par nous : quand nous appelons String::from, son implémentation demande la mémoire dont elle a besoin. C’est a peu près universel dans les langages de programmation.
Cependant, la seconde partie est différente. Dans les langages avec un ramasse-miettes (GC), le GC garde la trace de la mémoire qui n’est plus utilisee et la nettoie, et nous n’avons pas besoin d’y penser. Dans la plupart des langages sans GC, c’est notre responsabilite d’identifier quand la mémoire n’est plus utilisee et d’appeler du code pour la libérer explicitement, tout comme nous l’avons fait pour la demander. Faire cela correctement a historiquement été un problème de programmation difficile. Si nous oublions, nous gaspillons de la mémoire. Si nous le faisons trop tot, nous aurons une variable invalide. Si nous le faisons deux fois, c’est aussi un bug. Nous devons associer exactement un allocate avec exactement un free.
Rust prend un chemin différent : la mémoire est automatiquement rendue une fois que la variable qui la possède sort de la portée. Voici une version de notre exemple de portée du listing 4-1 utilisant une String au lieu d’un littéral de chaîne : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-02-string-scope/src/main.rs:here}}
fn main() {
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no
// longer valid
}
Il y à un moment naturel auquel nous pouvons rendre la mémoire dont notre String a besoin à l’allocateur : quand s sort de la portée. Quand une variable sort de la portée, Rust appelle une fonction speciale pour nous. Cette fonction s’appelle drop, et c’est la que l’auteur de String peut placer le code pour rendre la mémoire. Rust appelle drop automatiquement à l’accolade fermante.
Note : En C++, ce patron de desallocation des ressources à la fin de la duree de vie d’un élément est parfois appelé Resource Acquisition Is Initialization (RAII). La fonction
dropen Rust vous sera familiere si vous avez utilise des patrons RAII.
Ce patron à un impact profond sur la façon dont le code Rust est écrit. Cela peut sembler simple pour l’instant, mais le comportement du code peut être inattendu dans des situations plus complexes quand nous voulons que plusieurs variables utilisent les données que nous avons allouées sur le tas. Explorons certaines de ces situations maintenant.
Interaction entre les variables et les données avec le deplacement (Move)
Plusieurs variables peuvent interagir avec les mêmes données de différentes façons en Rust. Le listing 4-2 montre un exemple utilisant un entier.
fn main() {
let x = 5;
let y = x;
}
x to yNous pouvons probablement deviner ce que cela fait : “Lier la valeur 5 a x ; puis, faire une copie de la valeur dans x et la lier a y.” Nous avons maintenant deux variables, x et y, et les deux valent 5. C’est en effet ce qui se passe, car les entiers sont des valeurs simples avec une taille connue et fixe, et ces deux valeurs 5 sont empilees sur la pile.
Maintenant, regardons la version avec String : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-03-string-move/src/main.rs:here}}
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}
Cela semble très similaire, donc nous pourrions supposer que le fonctionnement serait le même : c’est-a-dire que la deuxième ligne ferait une copie de la valeur dans s1 et la lierait a s2. Mais ce n’est pas tout a fait ce qui se passe.
Jetez un oeil à la figure 4-1 pour voir ce qui se passe sous le capot avec String. Une String est composee de trois parties, montrées a gauche : un pointeur vers la mémoire qui contient le contenu de la chaîne, une longueur, et une capacité. Ce groupe de données est stocké sur la pile. A droite se trouve la mémoire sur le tas qui contient le contenu.
La longueur est la quantite de mémoire, en octets, que le contenu de la String utilise actuellement. La capacité est la quantite totale de mémoire, en octets, que la String a recue de l’allocateur. La différence entre la longueur et la capacité est importante, mais pas dans ce contexte, donc pour l’instant, il est acceptable d’ignorer la capacité.
Quand nous assignons s1 a s2, les données de la String sont copiees, ce qui signifie que nous copions le pointeur, la longueur, et la capacité qui sont sur la pile. Nous ne copions pas les données sur le tas auxquelles le pointeur fait référence. En d’autres termes, la représentation des données en mémoire ressemble à la figure 4-2.
La représentation ne ressemble pas à la figure 4-3, qui est ce a quoi la mémoire ressemblerait si Rust copiait également les données du tas. Si Rust faisait cela, l’opération s2 = s1 pourrait être très couteuse en termes de performances à l’exécution si les données sur le tas étaient volumineuses.
Plus tot, nous avons dit que quand une variable sort de la portée, Rust appelle automatiquement la fonction drop et nettoie la mémoire du tas pour cette variable. Mais la figure 4-2 montre les deux pointeurs de données pointant vers le même emplacement. C’est un problème : quand s2 et s1 sortent de la portée, elles essaieront toutes les deux de libérer la même mémoire. C’est ce qu’on appelle une erreur de double liberation (double free) et c’est l’un des bugs de securite mémoire que nous avons mentionnes precedemment. Libérer la mémoire deux fois peut entrainer une corruption de la mémoire, ce qui peut potentiellement mener à des vulnerabilites de securite.
Pour garantir la securite de la mémoire, après la ligne let s2 = s1;, Rust considère s1 comme n’étant plus valide. Par consequent, Rust n’a pas besoin de libérer quoi que ce soit quand s1 sort de la portée. Regardez ce qui se passe quand vous essayez d’utiliser s1 après que s2 a été créée ; cela ne fonctionnera pas : rust,ignore,does_not_compile {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-04-cant-use-after-move/src/main.rs:here}}
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}
Vous obtiendrez une erreur comme celle-ci car Rust vous empeche d’utiliser la référence invalidee : console {{#include ../listings/ch04-understanding-ownership/no-listing-04-cant-use-after-move/output.txt}}
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:16
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{s1}, world!");
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Si vous avez entendu les termes copie superficielle (shallow copy) et copie profonde (deep copy) en travaillant avec d’autres langages, le concept de copier le pointeur, la longueur et la capacité sans copier les données ressemble probablement à une copie superficielle. Mais comme Rust invalide aussi la première variable, au lieu d’être appelée une copie superficielle, c’est connu sous le nom de deplacement (move). Dans cet exemple, nous dirions que s1 a été deplacee dans s2. Donc, ce qui se passe réellement est illustre dans la figure 4-4.
Cela resout notre problème ! Avec seulement s2 valide, quand elle sort de la portée, elle seule liberera la mémoire, et c’est terminé.
De plus, il y à un choix de conception qui est implique par cela : Rust ne créera jamais automatiquement de copies “profondes” de vos données. Par consequent, toute copie automatique peut être considérée comme peu couteuse en termes de performances à l’exécution.
Portée et assignation
L’inverse est également vrai pour la relation entre la portée, la possession et la liberation de la mémoire via la fonction drop. Quand vous assignez une valeur completement nouvelle à une variable existante, Rust appellera drop et liberera immediatement la mémoire de la valeur d’origine. Considérez ce code, par exemple : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-04b-replacement-drop/src/main.rs:here}}
fn main() {
let mut s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!");
}
Nous declarons d’abord une variable s et la lions à une String avec la valeur "hello". Ensuite, nous créons immediatement une nouvelle String avec la valeur "ahoy" et l’assignons a s. A ce stade, plus rien ne fait référence à la valeur originale sur le tas. La figure 4-5 illustre les données de la pile et du tas maintenant :
La chaîne originale sort donc immediatement de la portée. Rust exécutera la fonction drop sur celle-ci et sa mémoire sera libérée immediatement. Quand nous affichons la valeur à la fin, ce sera "ahoy, world!".
Interaction entre les variables et les données avec Clone
Si nous voulons vraiment copier en profondeur les données du tas de la String, et pas seulement les données de la pile, nous pouvons utiliser une methode courante appelée clone. Nous aborderons la syntaxe des methodes au chapitre 5, mais comme les methodes sont une fonctionnalité courante dans de nombreux langages de programmation, vous les avez probablement déjà vues.
Voici un exemple de la methode clone en action : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-05-clone/src/main.rs:here}}
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");
}
Cela fonctionne parfaitement et produit explicitement le comportement illustre dans la figure 4-3, ou les données du tas sont effectivement copiees.
Quand vous voyez un appel a clone, vous savez qu’un code arbitraire est exécute et que ce code peut être couteux. C’est un indicateur visuel que quelque chose de différent se passe.
Données uniquement sur la pile : Copy
Il y à une autre subtilite dont nous n’avons pas encore parle. Ce code utilisant des entiers – dont une partie a été montrée dans le listing 4-2 – fonctionne et est valide : rust {{#rustdoc_include ../listings/ch04-understanding-ownership/no-listing-06-copy/src/main.rs:here}}
fn main() {
let x = 5;
let y = x;
println!("x = {x}, y = {y}");
}
Mais ce code semble contredire ce que nous venons d’apprendre : nous n’avons pas d’appel a clone, mais x est toujours valide et n’a pas été deplace dans y.
La raison est que les types tels que les entiers qui ont une taille connue à la compilation sont entierement stockés sur la pile, donc les copies des valeurs réelles sont rapides a effectuer. Cela signifie qu’il n’y à aucune raison de vouloir empecher x d’être valide après avoir crée la variable y. En d’autres termes, il n’y a pas de différence entre copie profonde et copie superficielle ici, donc appeler clone ne ferait rien de différent de la copie superficielle habituelle, et nous pouvons l’omettre.
Rust à une annotation speciale appelée le trait Copy que nous pouvons placer sur les types stockés sur la pile, comme le sont les entiers (nous parlerons davantage des traits au [chapitre 10][traits]). Si un type implémenté le trait Copy, les variables qui l’utilisent ne sont pas deplacees, mais sont plutot copiees de maniere triviale, ce qui les rend toujours valides après l’assignation à une autre variable.
Rust ne nous laissera pas annoter un type avec Copy si le type, ou l’une de ses parties, a implémenté le trait Drop. Si le type nécessite qu’un traitement special se produise quand la valeur sort de la portée et que nous ajoutons l’annotation Copy à ce type, nous obtiendrons une erreur de compilation. Pour apprendre comment ajouter l’annotation Copy à votre type pour implémenter le trait, consultez [“Les traits derivables”][derivable-traits] dans l’annexe C.
Alors, quels types implementent le trait Copy ? Vous pouvez consulter la documentation du type en question pour en être sur, mais en règle generale, tout groupe de valeurs scalaires simples peut implémenter Copy, et rien qui nécessite une allocation ou qui constitue une forme de ressource ne peut implémenter Copy. Voici quelques-uns des types qui implementent Copy :
- Tous les types d’entiers, comme
u32. - Le type booleen,
bool, avec les valeurstrueetfalse. - Tous les types a virgule flottante, comme
f64. - Le type caractère,
char. - Les tuples, s’ils ne contiennent que des types qui implementent également
Copy. Par exemple,(i32, i32)implémentéCopy, mais(i32, String)ne l’implémenté pas.
La possession et les fonctions
Le mecanisme de passage d’une valeur à une fonction est similaire a celui de l’assignation d’une valeur à une variable. Passer une variable à une fonction la deplacera ou la copiera, tout comme l’assignation. Le listing 4-3 contient un exemple avec des annotations montrant ou les variables entrent et sortent de la portée.
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); // Because i32 implements the Copy trait,
// x does NOT move into the function,
// so it's okay to use x afterward.
} // Here, x goes out of scope, then s. However, 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.
Si nous essayions d’utiliser s après l’appel a takes_ownership, Rust lancerait une erreur de compilation. Ces vérifications statiques nous protegent des erreurs. Essayez d’ajouter du code a main qui utilise s et x pour voir ou vous pouvez les utiliser et ou les règles de la possession vous en empechent.
Les valeurs de retour et la portée
Renvoyer des valeurs peut également transferer la possession. Le listing 4-4 montre un exemple de fonction qui renvoie une valeur, avec des annotations similaires a celles du listing 4-3.
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 a String.
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
}
La possession d’une variable suit le même schema à chaque fois : assigner une valeur à une autre variable la deplace. Quand une variable qui inclut des données sur le tas sort de la portée, la valeur sera nettoyee par drop a moins que la possession des données n’ait été transferee à une autre variable.
Bien que cela fonctionne, prendre la possession puis la rendre avec chaque fonction est un peu fastidieux. Que faire si nous voulons laisser une fonction utiliser une valeur sans en prendre la possession ? C’est assez agacant que tout ce que nous passons doive aussi être renvoye si nous voulons le reutiliser, en plus de toutes les données resultant du corps de la fonction que nous pourrions également vouloir renvoyer.
Rust nous permet de renvoyer plusieurs valeurs en utilisant un tuple, comme le montre le listing 4-5.
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{s2}' is {len}.");
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}
Mais c’est beaucoup trop de ceremonie et de travail pour un concept qui devrait être courant. Heureusement pour nous, Rust dispose d’une fonctionnalité pour utiliser une valeur sans transferer la possession : les références.