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, si 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 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
}

Encart 4-1 : Une variable et la portée dans laquelle elle est 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 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;
}

Encart 4-2 : Assigner l'entier de la variable x à y

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.

Une string en mémoire

Illustration 4-1 : Représentation en mémoire d'une String qui contient la valeur "hello" assignée à s1.

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.

s1 et s2 qui pointent vers la même valeur

Illustration 4-2 : Représentation en mémoire de la variable s2 qui a une copie du pointeur, de la taille et de la capacité de s1

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.

s1 et s2 à deux endroits

Illustration 4-3 : Une autre possibilité de ce que pourrait faire s2 = s1 si Rust copiait aussi les données du tas

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.

s1 déplacé dans s2

Illustration 4-4 : Représentation de la mémoire après que s1 a été neutralisée

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 valeurs true et false.
  • 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émente Copy, 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.

Encart 4-3 : Les fonctions avec les possessions et les portées qui sont commentées

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.
}

Encart 4-4 : Transferts de possession des valeurs de retour

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)
}

Encart 4-5 : Retourner la possession des paramètres

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.