Qu'est-ce que la possession ?
La possession est un jeu de règles qui gouvernent la gestion de la mémoire par un programme Rust. Tous les programmes doivent gérer la façon dont ils utilisent la mémoire lorsqu'ils s'exécutent. Certains langages ont un ramasse-miettes qui scrute constamment la mémoire qui n'est plus utilisée pendant qu'il s'exécute ; dans d'autres langages, le développeur doit explicitement allouer et libérer la mémoire. Rust adopte une troisième approche : la mémoire est gérée avec un système de possession qui repose sur un jeu de règles que le compilateur vérifie au moment de la compilation. Si une de ces règles a été enfreinte, le programme ne sera pas compilé. Aucune des fonctionnalités de la possession ne ralentit votre programme à l'exécution.
Comme la possession est un nouveau principe pour de nombreux développeurs, cela prend un certain temps pour s'y familiariser. La bonne nouvelle est que plus vous devenez expérimenté avec Rust et ses règles de possession, plus vous développerez naturellement et facilement du code sûr et efficace. Gardez bien cela à l'esprit !
Lorsque vous comprendrez la possession, vous aurez des bases solides pour comprendre les fonctionnalités qui font la particularité de Rust. Dans ce chapitre, vous allez apprendre la possession en pratiquant avec plusieurs exemples qui se concentrent sur une structure de données très courante : les chaînes de caractères.
La pile et le tas
De nombreux langages ne nécessitent pas de se préoccuper de la pile (stack) et du tas (heap). Mais dans un langage de programmation système comme Rust, le fait qu'une donnée soit sur la pile ou sur le tas a une influence sur le comportement du langage et explique pourquoi nous devons faire certains choix. Nous décrirons plus loin dans ce chapitre comment la possession fonctionne vis-à-vis de la pile et du tas, voici donc une brève explication au préalable.
La pile et le tas sont tous les deux des emplacements de la mémoire à disposition de votre code lors de son exécution, mais sont organisés de façon différente. La pile enregistre les valeurs dans l'ordre qu'elle les reçoit et enlève les valeurs dans l'autre sens. C'est ce que l'on appelle le principe de dernier entré, premier sorti. C'est comme une pile d'assiettes : quand vous ajoutez des nouvelles assiettes, vous les déposez sur le dessus de la pile, et quand vous avez besoin d'une assiette, vous en prenez une sur le dessus. Ajouter ou enlever des assiettes au milieu ou en bas ne serait pas aussi efficace ! Ajouter une donnée sur la pile se dit empiler et en retirer une se dit dépiler. Toutes donnée stockée dans la pile doit avoir une taille connue et fixe. Les données avec une taille inconnue au moment de la compilation ou une taille qui peut changer doivent plutôt être stockées sur le tas.
Le tas est moins bien organisé : lorsque vous ajoutez des données sur le tas, vous demandez une certaine quantité d'espace mémoire. Le gestionnaire de mémoire va trouver un emplacement dans le tas qui est suffisamment grand, va le marquer comme étant en cours d'utilisation, et va retourner un pointeur, qui est l'adresse de cet emplacement. Cette procédure est appelée allocation sur le tas, ce qu'on abrège parfois en allocation tout court. L'ajout de valeurs sur la pile n'est pas considéré comme une allocation. Comme le pointeur vers le tas a une taille connue et fixe, on peut stocker ce pointeur sur la pile, mais quand on veut la vraie donnée, il faut suivre le pointeur.
C'est comme si vous vouliez manger au restaurant. Quand vous entrez, vous indiquez le nombre de personnes dans votre groupe, et le personnel trouve une table vide qui peut recevoir tout le monde, et vous y conduit. Si quelqu'un dans votre groupe arrive en retard, il peut leur demander où vous êtes assis pour vous rejoindre.
Empiler sur la pile est plus rapide qu'allouer sur le tas car le gestionnaire ne va jamais avoir besoin de chercher un emplacement pour y stocker les nouvelles données ; il le fait toujours au sommet de la pile. En comparaison, allouer de la place sur le tas demande plus de travail, car le gestionnaire doit d'abord trouver un espace assez grand pour stocker les données et mettre à jour son suivi pour préparer la prochaine allocation.
Accéder à des données dans le tas est plus lent que d'accéder aux données sur la pile car nous devons suivre un pointeur pour les obtenir. Les processeurs modernes sont plus rapides s'ils se déplacent moins dans la mémoire. Pour continuer avec notre analogie, imaginez un serveur dans un restaurant qui prend les commandes de nombreuses tables. C'est plus efficace de récupérer toutes les commandes à une seule table avant de passer à la table suivante. Prendre une commande à la table A, puis prendre une commande à la table B, puis ensuite une autre à la table A, puis une autre à la table B serait un processus bien plus lent. De la même manière, un processeur sera plus efficace dans sa tâche s'il travaille sur des données qui sont proches les unes des autres (comme c'est le cas sur la pile) plutôt que si elles sont plus éloignées (comme cela peut être le cas sur le tas). Allouer une grande quantité de mémoire sur le tas peut aussi prendre beaucoup de temps.
Quand notre code utilise une fonction, les valeurs passées à la fonction (incluant, potentiellement, des pointeurs de données sur le tas) et les variables locales à la fonction sont déposées sur la pile. Quand l'utilisation de la fonction est terminée, ces données sont retirées de la pile.
La possession nous aide à ne pas nous préoccuper de faire attention à quelles parties du code utilisent quelles données sur le tas, de minimiser la quantité de données en double sur le tas, ou encore de veiller à libérer les données inutilisées sur le tas pour que nous ne soyons pas à court d'espace. Quand vous aurez compris la possession, vous n'aurez plus besoin de vous préoccuper de la pile et du tas très souvent, mais savoir que le but principal de la possession est de gérer les données du tas peut vous aider à comprendre pourquoi elle fonctionne de cette manière.
Les règles de la possession
Tout d'abord, définissons les règles de la possession. Gardez à l'esprit ces règles pendant que nous travaillons sur des exemples qui les illustrent :
- Chaque valeur en Rust a une variable qui s'appelle son propriétaire.
- Il ne peut y avoir qu'un seul propriétaire à la fois.
- Quand le propriétaire sortira de la portée, la valeur sera supprimée.
Portée de la variable
Maintenant
que nous avons vu la syntaxe Rust de base, nous n'allons plus ajouter tout le
code du style fn main() {
dans les exemples, donc si vous voulez reproduire
les exemples, assurez-vous de les placer manuellement dans une fonction main
. Par
conséquent, nos exemples seront plus concis, nous permettant de nous concentrer
sur les détails de la situation plutôt que sur du code normalisé.
Pour le premier exemple de possession, nous allons analyser la portée de certaines variables. Une portée est une zone dans un programme dans laquelle un élément est en vigueur. Admettons 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, où la
valeur de la chaîne est codée en dur dans notre programme. La variable est en
vigueur à partir du moment où elle est déclarée jusqu'à la fin de la portée
actuelle. L'encart 4-1 nous présente un programme avec des commentaires pour
indiquer quand la variable s
est en vigueur :
fn main() { { // s n'est pas en vigueur ici, elle n'est pas encore déclarée let s = "hello"; // s est en vigueur à partir de ce point // on fait des choses avec s ici } // cette portée est maintenant terminée, et s n'est plus en vigueur }
Autrement dit, il y a ici deux étapes importantes :
- Quand
s
rentre dans la portée, elle est en vigueur. - Cela reste ainsi jusqu'à ce qu'elle sorte de la portée.
Pour le moment, la relation entre les portées et les conditions pour lesquelles
les variables sont en vigueur sont similaires à d'autres langages de
programmation. Maintenant, nous allons aller plus loin en y ajoutant le type
String
.
Le type String
Pour illustrer les règles de la possession, nous avons besoin d'un type de
donnée qui est plus complexe que ceux que nous avons rencontrés dans la section
“Types de données” du chapitre 3. Les types que
nous avons vus précédemment ont tous une taille connue et peuvent être stockés
sur la pile ainsi que retirés de la pile lorsque la portée n'en a plus besoin,
et peuvent aussi être rapidement et facilement copiés afin de constituer 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 expérimenter le stockage
de données sur le tas et découvrir comment Rust sait quand il doit nettoyer ces
données, et le type String
est un bon exemple.
Nous allons nous concentrer sur les caractéristiques de String
qui sont liées
à la possession. Ces aspects s'appliquent également à d'autres types de données
complexes, qu'ils soient fournis par la bibliothèque standard ou qu'ils soient
créés par vous. Nous verrons String
plus en détail dans le chapitre
8.
Nous avons déjà vu les littéraux de chaînes de caractères, quand une valeur de
chaîne est codée en dur dans notre programme. Les littéraux de chaînes sont
pratiques, mais ils ne conviennent pas toujours à tous les cas où on veut
utiliser du texte. Une des raisons est qu'ils sont immuables. Une autre raison
est qu'on ne connaît pas forcément le contenu des chaînes de caractères quand
nous écrivons notre code : par exemple, comment faire si nous voulons récupérer
du texte saisi par l'utilisateur et l'enregistrer ? Pour ces cas-ci, Rust a un
second type de chaîne de caractères, String
. Ce type gère ses données sur le
tas et est ainsi capable de stocker une quantité de texte qui nous est inconnue
au moment de la compilation. Vous pouvez créer une String
à partir d'un
littéral de chaîne de caractères 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'appeler cette fonction
spécifique dans l'espace de nom du type String
plutôt que d'utiliser un nom
comme string_from
. Nous verrons cette syntaxe plus en détail dans la section
“Syntaxe de méthode” du chapitre 5 et lorsque
nous aborderons les espaces de noms dans la section “Les chemins pour désigner
un élément dans l'arborescence de module” du
chapitre 7.
Ce type de chaîne de caractères peut être mutable :
fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str() ajoute un littéral de chaîne dans une String println!("{}", s); // Cela va afficher `hello, world!` }
Donc, quelle est la différence ici ? Pourquoi String
peut être mutable, mais
pourquoi les littéraux de chaînes ne peuvent pas l'être ? La différence
se trouve dans la façon dont ces deux types travaillent avec la mémoire.
Mémoire et allocation
Dans le cas d'un littéral de chaîne de caractères, nous connaissons le contenu au moment de la compilation donc le texte est codé en dur directement dans l'exécutable final. Voilà pourquoi ces littéraux de chaînes de caractères sont performants et rapides. Mais ces caractéristiques viennent de leur immuabilité. Malheureusement, on ne peut pas accorder une grosse région de mémoire dans le binaire pour chaque morceau de texte qui n'a pas de taille connue au moment de la compilation et dont la taille pourrait changer pendant l'exécution de ce programme.
Avec le type String
, pour nous permettre d'avoir un texte mutable et qui peut
s'agrandir, nous devons allouer une quantité de mémoire sur le tas, inconnue
au moment de la compilation, pour stocker le contenu. Cela signifie que :
- La mémoire doit être demandée auprès du gestionnaire de mémoire lors de l'exécution.
- Nous avons besoin d'un moyen de rendre cette mémoire au gestionnaire lorsque
nous aurons fini d'utiliser notre
String
.
Nous nous occupons de ce premier point : quand nous appelons String::from
, son
implémentation demande la mémoire dont elle a besoin. C'est pratiquement
toujours ainsi dans la majorité des langages de programmation.
Cependant, le deuxième point est différent. Dans des langages avec un
ramasse-miettes, le ramasse-miettes surveille et nettoie la mémoire qui n'est
plus utilisée, sans que nous n'ayons à nous en préoccuper. Dans la pluspart des
langages sans ramasse-miettes, c'est de notre responsabilité d'identifier quand
cette mémoire n'est plus utilisée et d'appeler du code pour explicitement la
libérer, comme nous l'avons fait pour la demander auparavant. Historiquement,
faire ceci correctement a toujours été une difficulté pour les développeurs. Si
nous oublions de le faire, nous allons gaspiller de la mémoire. Si nous le
faisons trop tôt, nous allons avoir une variable invalide. Si nous le faisons
deux fois, cela produit aussi un bogue. Nous devons associer exactement un
allocate
avec exactement un free
.
Rust prend un chemin différent : la mémoire est automatiquement libérée dès
que la variable qui la possède sort de la portée. Voici une version de notre
exemple de portée de l'encart 4-1 qui utilise une String
plutôt qu'un littéral
de chaîne de caractères :
fn main() { { let s = String::from("hello"); // s est en vigueur à partir de ce point // on fait des choses avec s ici } // cette portée est désormais terminée, et s // n'est plus en vigueur maintenant }
Il y a un moment naturel où nous devons rendre la mémoire de notre
String
au gestionnaire : quand s
sort de la portée. Quand une variable sort
de la portée, Rust appelle une fonction spéciale pour nous. Cette fonction
s'appelle drop
, et c'est dans celle-ci que l'auteur de
String
a pu mettre le code pour libérer la mémoire. Rust appelle
automatiquement drop
à l'accolade fermante }
.
Remarque : en C++, cette façon de libérer des ressources à la fin de la durée de vie d'un élément est parfois appelée l'acquisition d'une ressource est une initialisation (RAII). La fonction
drop
de Rust vous sera familière si vous avez déjà utilisé des techniques de RAII.
Cette façon de faire a un impact profond sur la façon dont le code Rust est écrit. Cela peut sembler simple dans notre cas, mais le comportement du code peut être surprenant dans des situations plus compliquées où nous voulons avoir plusieurs variables utilisant des données que nous avons affectées sur le tas. Examinons une de ces situations dès à présent.
Les interactions entre les variables et les données : le déplacement
Plusieurs variables peuvent interagir avec les mêmes données de différentes manières en Rust. Regardons un exemple avec un entier dans l'encart 4-2 :
fn main() { let x = 5; let y = x; }
Nous pouvons probablement deviner ce que ce code fait : “Assigner la valeur 5
à x
; ensuite faire une copie de cette valeur de x
et l'assigner à y
.”
Nous avons maintenant deux variables, x
et y
, et chacune vaut 5
. C'est
effectivement ce qui se passe, car les entiers sont des valeurs simples avec une
taille connue et fixée, et ces deux valeurs 5
sont stockées sur la pile.
Maintenant, essayons une nouvelle version avec String
:
fn main() { let s1 = String::from("hello"); let s2 = s1; }
Cela ressemble beaucoup, donc nous allons supposer que cela fonctionne pareil
que précédemment : ainsi, la seconde ligne va faire une copie de la valeur de
s1
et l'assigner à s2
. Mais ce n'est pas tout à fait ce qu'il se passe.
Regardons l'illustration 4-1 pour découvrir ce qui arrive à String
sous le
capot. Une String
est constituée de trois éléments, présents sur la gauche :
un pointeur vers la mémoire qui contient le contenu de la chaîne de caractères,
une taille, et une capacité. Ce groupe de données est stocké sur la pile. À
droite, nous avons la mémoire sur le tas qui contient les données.
La taille est la quantité de mémoire, en octets, que le contenu de la String
utilise actuellement. La capacité est la quantité totale de mémoire, en octets,
que la String
a reçue du gestionnaire. La différence entre la taille et la
capacité est importante, mais pas pour notre exemple, donc pour l'instant, ce
n'est pas grave d'ignorer la capacité.
Quand nous assignons s1
à s2
, les données de la String
sont copiées, ce
qui veut dire que nous copions le pointeur, la taille et la capacité qui sont
stockés sur la pile. Nous ne copions pas les données stockées sur le tas
auxquelles le pointeur se réfère. Autrement dit, la représentation des données
dans la mémoire ressemble à l'illustration 4-2.
Cette représentation n'est pas comme l'illustration 4-3, qui représenterait la
mémoire si Rust avait aussi copié les données sur le tas. Si Rust faisait ceci,
l'opération s2 = s1
pourrait potentiellement être très coûteuse en termes de
performances d'exécution si les données sur le tas étaient volumineuses.
Précédemment, nous avons dit que quand une variable sortait de la portée, Rust
appelait automatiquement la fonction drop
et nettoyait la mémoire sur le tas
allouée pour cette variable. Mais l'illustration 4-2 montre que les deux
pointeurs de données pointeraient au même endroit. C'est un problème : quand
s2
et s1
sortent de la portée, elles vont essayer toutes les deux de
libérer la même mémoire. C'est ce qu'on appelle une erreur de double
libération et c'est un des bogues de sécurité de mémoire que nous avons
mentionnés précédemment. Libérer la mémoire deux fois peut mener à des
corruptions de mémoire, ce qui peut potentiellement mener à des vulnérabilités
de sécurité.
Pour garantir la sécurité de la mémoire, après la ligne let s2 = s1
, Rust
considère que s1
n'est plus en vigueur. Par conséquent, Rust n'a pas besoin
de libérer quoi que ce soit lorsque s1
sort de la portée. Regardez ce qu'il
se passe quand vous essayez d'utiliser s1
après que s2
est créé, cela ne va
pas fonctionner :
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
}
Vous allez avoir une erreur comme celle-ci, car Rust vous défend d'utiliser la référence qui n'est plus en vigueur :
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
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!("{}, world!", s1);
| ^^ value borrowed here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error
Si vous avez déjà entendu parler de copie superficielle et de copie
profonde en utilisant d'autres langages, l'idée de copier le pointeur, la
taille et la capacité sans copier les données peut vous faire penser à de la
copie superficielle. Mais comme Rust neutralise aussi la première variable, au
lieu d'appeler cela une copie superficielle, on appelle cela un déplacement.
Ici, nous pourrions dire que s1
a été déplacé dans s2
. Donc ce qui se
passe réellement est décrit par l'illustration 4-4.
Cela résout notre problème ! Avec seulement s2
en vigueur, quand elle
sortira de la portée, elle seule va libérer la mémoire, et c'est tout.
De plus, cela signifie qu'il y a eu un choix de conception : Rust ne va jamais créer automatiquement de copie “profonde” de vos données. Par conséquent, toute copie automatique peut être considérée comme peu coûteuse en termes de performances d'exécution.
Les interactions entre les variables et les données : le clonage
Si nous voulons faire une copie profonde des données sur le tas d'une
String
, et pas seulement des données sur la pile, nous pouvons utiliser une
méthode commune qui s'appelle clone
. Nous aborderons la syntaxe des méthodes
au chapitre 5, mais comme les méthodes sont des outils courants dans de
nombreux langages, vous les avez probablement utilisées auparavant.
Voici un exemple d'utilisation de la méthode clone
:
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); }
Cela fonctionne très bien et c'est ainsi que vous pouvez reproduire le comportement décrit dans l'illustration 4-3, où les données du tas sont copiées.
Quand vous voyez un appel à clone
, vous savez que du code arbitraire est
exécuté et que ce code peut être coûteux. C'est un indicateur visuel qu'il se
passe quelque chose de différent.
Données uniquement sur la pile : la copie
Il y a un autre détail dont on n'a pas encore parlé. Le code suivant utilise des entiers - on en a vu une partie dans l'encart 4-2 - il fonctionne et est correct :
fn main() { let x = 5; let y = x; println!("x = {}, y = {}", x, y); }
Mais ce code semble contredire ce que nous venons d'apprendre : nous n'avons
pas appelé clone
, mais x
est toujours en vigueur et n'a pas été déplacé
dans y
.
La raison est que les types comme les entiers ont une taille connue au moment de
la compilation et sont entièrement stockés sur la pile, donc la copie des
vraies valeurs est rapide à faire. Cela signifie qu'il n'y a pas de raison que
nous voudrions neutraliser x
après avoir créé la variable y
. En d'autres
termes, il n'y a pas ici de différence entre la copie superficielle et profonde,
donc appeler clone
ne ferait rien d'autre qu'une copie superficielle classique
et on peut s'en passer.
Rust a une annotation spéciale appelée le trait Copy
que nous pouvons utiliser
sur des types comme les entiers qui sont stockés sur la pile (nous verrons les
traits dans le chapitre 10). Si un type implémente le trait Copy
, une
variable sera toujours en vigueur après avoir été affectée à une autre
variable. Rust ne nous autorisera pas à annoter un type avec le trait Copy
si
ce type, ou un de ses éléments, a implémenté le trait Drop
. Si ce type a
besoin que quelque chose de spécial se produise quand la valeur sort de la
portée et que nous ajoutons l'annotation Copy
sur ce type, nous aurons une
erreur au moment de la compilation. Pour savoir comment ajouter l'annotation
Copy
sur votre type pour implémenter le trait, référez-vous à l'annexe
C sur les traits dérivables.
Donc, quels sont les types qui implémentent le trait Copy
? Vous pouvez
regarder dans la documentation pour un type donné pour vous en assurer, mais de
manière générale, tout groupe de valeur scalaire peut implémenter Copy
, et
tout ce qui ne nécessite pas d'allocation de mémoire ou tout autre forme de
ressource qui implémente Copy
. Voici quelques types qui implémentent Copy
:
- Tous les types d'entiers, comme
u32
. - Le type booléen,
bool
, avec les valeurstrue
etfalse
. - Tous les types de flottants, comme
f64
. - Le type de caractère,
char
. - Les tuples, mais uniquement s'ils contiennent des types qui implémentent
aussi
Copy
. Par exemple, le(i32, i32)
implémenteCopy
, mais pas(i32, String)
.
La possession et les fonctions
La syntaxe pour passer une valeur à une fonction est similaire à celle pour assigner une valeur à une variable. Passer une variable à une fonction va la déplacer ou la copier, comme l'assignation. L'encart 4-3 est un exemple avec quelques commentaires qui montrent où les variables rentrent et sortent de la portée :
Fichier : src/main.rs
fn main() { let s = String::from("hello"); // s rentre dans la portée. prendre_possession(s); // La valeur de s est déplacée dans la fonction… // … et n'est plus en vigueur à partir d'ici let x = 5; // x rentre dans la portée. creer_copie(x); // x va être déplacée dans la fonction, // mais i32 est Copy, donc on peut // utiliser x ensuite. } // Ici, x sort de la portée, puis ensuite s. Mais puisque la valeur de s a // été déplacée, il ne se passe rien de spécial. fn prendre_possession(texte: String) { // texte rentre dans la portée. println!("{}", texte); } // Ici, texte sort de la portée et `drop` est appelé. La mémoire est libérée. fn creer_copie(entier: i32) { // entier rentre dans la portée. println!("{}", entier); } // Ici, entier sort de la portée. Il ne se passe rien de spécial.
Si on essayait d'utiliser s
après l'appel à prendre_possession
, Rust
déclencherait une erreur à la compilation. Ces vérifications statiques
nous protègent des erreurs. Essayez d'ajouter du code au main
qui utilise s
et x
pour découvrir lorsque vous pouvez les utiliser et lorsque les règles de
la possession vous empêchent de le faire.
Les valeurs de retour et les portées
Retourner des valeurs peut aussi transférer leur possession. L'encart 4-4 montre un exemple d'une fonction qui retourne une valeur, avec des annotations similaires à celles de l'encart 4-3 :
Fichier : src/main.rs
fn main() { let s1 = donne_possession(); // donne_possession déplace sa valeur de // retour dans s1 let s2 = String::from("hello"); // s2 rentre dans la portée let s3 = prend_et_rend(s2); // s2 est déplacée dans // prend_et_rend, qui elle aussi // déplace sa valeur de retour dans s3. } // Ici, s3 sort de la portée et est éliminée. s2 a été déplacée donc il ne se // passe rien. s1 sort aussi de la portée et est éliminée. fn donne_possession() -> String { // donne_possession va déplacer sa // valeur de retour dans la // fonction qui l'appelle. let texte = String::from("yours"); // texte rentre dans la portée. texte // texte est retournée et // est déplacée vers le code qui // l'appelle. } // Cette fonction va prendre une String et en retourne aussi une. fn prend_et_rend(texte: String) -> String { // texte rentre dans la portée. texte // texte est retournée et déplacée vers le code qui l'appelle. }
La possession d'une variable suit toujours le même schéma à chaque fois :
assigner une valeur à une autre variable la déplace. Quand une variable qui
contient des données sur le tas sort de la portée, la valeur sera nettoyée avec
drop
à moins que la possession de cette donnée soit donnée à une autre
variable.
Même si cela fonctionne, il est un peu fastidieux de prendre la possession puis ensuite de retourner la possession à chaque fonction. Et qu'est-ce qu'il se passe si nous voulons qu'une fonction utilise une valeur, mais n'en prenne pas possession ? C'est assez pénible que tout ce que nous passons doit être retourné si nous voulons l'utiliser à nouveau, en plus de toutes les données qui découlent du corps de la fonction que nous voulons aussi récupérer.
Rust nous permet de retourner plusieurs valeurs à l'aide d'un tuple, comme ceci :
Fichier : src/main.rs
fn main() { let s1 = String::from("hello"); let (s2, taille) = calculer_taille(s1); println!("La taille de '{}' est {}.", s2, taille); } fn calculer_taille(s: String) -> (String, usize) { let taille = s.len(); // len() retourne la taille d'une String. (s, taille) }
Mais c'est trop laborieux et beaucoup de travail pour un principe qui devrait être banal. Heureusement pour nous, Rust a une fonctionnalité pour utiliser une valeur sans avoir à transférer la possession, avec ce qu'on appelle les références.