Utiliser Box<T> pour les données stockées sur le tas

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans Utiliser Box pour pointer vers des données sur le tas. Ce laboratoire est une partie du Livre Rust. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons apprendre à utiliser les pointeurs intelligents Box pour stocker des données sur le tas plutôt que sur la pile, dans des situations où la taille du type est inconnue à la compilation, lors du transfert de la propriété de grandes quantités de données pour éviter la copie, ou lorsqu'on possède une valeur qui implémente un trait particulier.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100431{{"Utiliser Box pour les données stockées sur le tas"}} rust/integer_types -.-> lab-100431{{"Utiliser Box pour les données stockées sur le tas"}} rust/function_syntax -.-> lab-100431{{"Utiliser Box pour les données stockées sur le tas"}} rust/expressions_statements -.-> lab-100431{{"Utiliser Box pour les données stockées sur le tas"}} rust/method_syntax -.-> lab-100431{{"Utiliser Box pour les données stockées sur le tas"}} rust/operator_overloading -.-> lab-100431{{"Utiliser Box pour les données stockées sur le tas"}} end

Utiliser Box<T>{=html} pour pointer vers des données sur le tas

Le pointeur intelligent le plus simple est une boîte, dont le type est écrit Box<T>. Les boîtes vous permettent de stocker des données sur le tas plutôt que sur la pile. Ce qui reste sur la pile est le pointeur vers les données du tas. Consultez le Chapitre 4 pour revoir la différence entre la pile et le tas.

Les boîtes n'ont pas de surcoût de performance, autre que le fait de stocker leurs données sur le tas au lieu de sur la pile. Mais elles n'ont pas non plus de nombreuses capacités supplémentaires. Vous les utiliserez le plus souvent dans les situations suivantes :

  • Lorsque vous avez un type dont la taille ne peut pas être connue à la compilation et que vous voulez utiliser une valeur de ce type dans un contexte qui nécessite une taille exacte
  • Lorsque vous avez une grande quantité de données et que vous voulez transférer la propriété tout en vous assurant que les données ne seront pas copiées lors de ce transfert
  • Lorsque vous voulez posséder une valeur et que vous vous souciez seulement du fait qu'il s'agit d'un type qui implémente un trait particulier plutôt que d'un type spécifique

Nous démontrerons la première situation dans "Autoriser les types récursifs avec des boîtes". Dans le second cas, le transfert de la propriété d'une grande quantité de données peut prendre beaucoup de temps car les données sont copiées sur la pile. Pour améliorer les performances dans cette situation, nous pouvons stocker la grande quantité de données sur le tas dans une boîte. Ensuite, seule la petite quantité de données de pointeur est copiée sur la pile, tandis que les données qu'elle référence restent au même endroit sur le tas. Le troisième cas est connu sous le nom d'objet de trait, et "Utiliser des objets de trait qui autorisent des valeurs de différents types" est consacré à ce sujet. Donc, ce que vous apprenez ici, vous le réappliquerez dans cette section!

Utiliser Box<T>{=html} pour stocker des données sur le tas

Avant de discuter du cas d'utilisation de stockage sur le tas pour Box<T>, nous allons aborder la syntaxe et la manière d'interagir avec les valeurs stockées dans un Box<T>.

Le Listing 15-1 montre comment utiliser une boîte pour stocker une valeur i32 sur le tas.

Nom de fichier : src/main.rs

fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}

Listing 15-1 : Stockage d'une valeur i32 sur le tas à l'aide d'une boîte

Nous définissons la variable b pour avoir la valeur d'une Box qui pointe vers la valeur 5, qui est allouée sur le tas. Ce programme affichera b = 5 ; dans ce cas, nous pouvons accéder aux données dans la boîte de la même manière que si ces données étaient sur la pile. Tout comme toute valeur possédée, lorsqu'une boîte sort de portée, comme b le fait à la fin de main, elle sera désallouée. La désallocation se produit à la fois pour la boîte (stockée sur la pile) et les données qu'elle pointe (stockées sur le tas).

Mettre une seule valeur sur le tas n'est pas très utile, donc vous n'utiliserez pas souvent les boîtes seules de cette manière. Avoir des valeurs comme un simple i32 sur la pile, où elles sont stockées par défaut, est plus approprié dans la majorité des situations. Regardons un cas où les boîtes nous permettent de définir des types que nous ne serions pas autorisés à définir si nous n'avions pas de boîtes.

Autoriser les types récursifs avec des boîtes

Une valeur d'un type récursif peut avoir une autre valeur du même type comme partie d'elle-même. Les types récursifs posent un problème car au moment de la compilation, Rust doit savoir combien d'espace occupe un type. Cependant, le couplage des valeurs de types récursifs pourrait théoriquement continuer indéfiniment, donc Rust ne peut pas savoir combien d'espace la valeur nécessite. Parce que les boîtes ont une taille connue, nous pouvons autoriser les types récursifs en insérant une boîte dans la définition du type récursif.

En tant qu'exemple de type récursif, explorons la liste cons. Il s'agit d'un type de données couramment trouvé dans les langages de programmation fonctionnels. Le type de liste cons que nous allons définir est simple, sauf pour la récursion ; par conséquent, les concepts de l'exemple avec lequel nous allons travailler seront utiles chaque fois que vous vous retrouverez dans des situations plus complexes impliquant des types récursifs.

Plus d'informations sur la liste cons

Une liste cons est une structure de données issue du langage de programmation Lisp et de ses dialectes, composée de paires imbriquées, et est la version Lisp d'une liste chaînée. Son nom vient de la fonction cons (abrégé de construct function) en Lisp qui construit une nouvelle paire à partir de ses deux arguments. En appelant cons sur une paire composée d'une valeur et d'une autre paire, nous pouvons construire des listes cons constituées de paires récursives.

Par exemple, voici une représentation en pseudo-code d'une liste cons contenant la liste 1, 2, 3 avec chaque paire entre parenthèses :

(1, (2, (3, Nil)))

Chaque élément d'une liste cons contient deux éléments : la valeur de l'élément actuel et l'élément suivant. Le dernier élément de la liste ne contient que une valeur appelée Nil sans élément suivant. Une liste cons est produite en appelant récursivement la fonction cons. Le nom canonique pour désigner le cas de base de la récursion est Nil. Notez que ce n'est pas la même chose que le concept de "null" ou "nil" du Chapitre 6, qui est une valeur invalide ou absente.

La liste cons n'est pas une structure de données couramment utilisée en Rust. La plupart du temps, lorsque vous avez une liste d'éléments en Rust, Vec<T> est un meilleur choix à utiliser. D'autres types de données récursives plus complexes sont utiles dans diverses situations, mais en commençant par la liste cons dans ce chapitre, nous pouvons explorer la manière dont les boîtes nous permettent de définir un type de données récursif sans trop de distractions.

Le Listing 15-2 contient une définition d'énumération pour une liste cons. Notez que ce code ne compilera pas encore car le type List n'a pas une taille connue, ce que nous allons démontrer.

Nom de fichier : src/main.rs

enum List {
    Cons(i32, List),
    Nil,
}

Listing 15-2 : La première tentative de définition d'un énumération pour représenter une structure de données de liste cons de valeurs i32

Note : Nous implémentons une liste cons qui ne contient que des valeurs i32 dans le cadre de cet exemple. Nous aurions pu l'implémenter à l'aide de génériques, comme nous l'avons discuté au Chapitre 10, pour définir un type de liste cons qui pourrait stocker des valeurs de tout type.

Utiliser le type List pour stocker la liste 1, 2, 3 ressemblerait au code du Listing 15-3.

Nom de fichier : src/main.rs

--snip--

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

Listing 15-3 : Utilisation de l'énumération List pour stocker la liste 1, 2, 3

La première valeur Cons contient 1 et une autre valeur de type List. Cette valeur de type List est une autre valeur Cons qui contient 2 et une autre valeur de type List. Cette valeur de type List est une nouvelle valeur Cons qui contient 3 et une valeur de type List, qui est finalement Nil, la variante non récursive qui indique la fin de la liste.

Si nous essayons de compiler le code du Listing 15-3, nous obtenons l'erreur affichée dans le Listing 15-4.

error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^ recursive type has infinite size
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List`
representable
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

Listing 15-4 : L'erreur que nous obtenons lorsqu'on essaie de définir un énumération récursive

L'erreur indique que ce type "a une taille infinie". La raison en est que nous avons défini List avec une variante qui est récursive : elle contient directement une autre valeur de soi-même. En conséquence, Rust ne peut pas déterminer combien d'espace il faut pour stocker une valeur de type List. Analysons pourquoi nous obtenons cette erreur. Tout d'abord, regardons comment Rust décide de la quantité d'espace qu'il faut pour stocker une valeur d'un type non récursif.

Calcul de la taille d'un type non récursif

Rappelez l'énumération Message que nous avons définie dans le Listing 6-2 lorsque nous avons discuté des définitions d'énumération au Chapitre 6 :

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Pour déterminer combien d'espace allouer pour une valeur de type Message, Rust examine chacun des variants pour voir lequel des variants nécessite le plus d'espace. Rust constate que Message::Quit n'a pas besoin d'espace, Message::Move a besoin d'un espace suffisant pour stocker deux valeurs de type i32, etc. Puisque seul un variant sera utilisé, l'espace maximum dont une valeur de type Message aura besoin est l'espace qu'elle occuperait pour stocker le plus grand de ses variants.

Comparez cela à ce qui se passe lorsque Rust essaie de déterminer combien d'espace un type récursif comme l'énumération List dans le Listing 15-2 nécessite. Le compilateur commence par examiner la variante Cons, qui contient une valeur de type i32 et une valeur de type List. Par conséquent, Cons a besoin d'une quantité d'espace égale à la taille d'un i32 plus la taille d'un List. Pour déterminer combien de mémoire le type List nécessite, le compilateur examine les variants, en commençant par la variante Cons. La variante Cons contient une valeur de type i32 et une valeur de type List, et ce processus continue indéfiniment, comme le montre la Figure 15-1.

Figure 15-1 : Une List infinie composée de variants Cons infinis

Utiliser Box<T>{=html} pour obtenir un type récursif avec une taille connue

Puisque Rust ne peut pas déterminer combien d'espace allouer pour les types définis de manière récursive, le compilateur renvoie une erreur avec cette suggestion utile :

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List`
representable
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

Dans cette suggestion, indirection signifie que plutôt que de stocker directement une valeur, nous devrions modifier la structure de données pour stocker la valeur indirectement en stockant un pointeur vers la valeur.

Parce qu'un Box<T> est un pointeur, Rust sait toujours combien d'espace un Box<T> nécessite : la taille d'un pointeur ne change pas en fonction de la quantité de données vers lesquelles il pointe. Cela signifie que nous pouvons placer un Box<T> dans la variante Cons au lieu d'une autre valeur de type List directement. Le Box<T> pointera vers la prochaine valeur de type List qui se trouvera sur le tas plutôt que dans la variante Cons. Conceptuellement, nous avons toujours une liste, créée avec des listes contenant d'autres listes, mais cette implémentation est maintenant plus similaire à placer les éléments les uns à côté des autres plutôt que les uns à l'intérieur des autres.

Nous pouvons modifier la définition de l'énumération List dans le Listing 15-2 et l'utilisation de List dans le Listing 15-3 pour le code du Listing 15-5, qui compilera.

Nom de fichier : src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(
        1,
        Box::new(Cons(
            2,
            Box::new(Cons(
                3,
                Box::new(Nil)
            ))
        ))
    );
}

Listing 15-5 : Définition de List qui utilise Box<T> pour avoir une taille connue

La variante Cons nécessite la taille d'un i32 plus l'espace pour stocker les données de pointeur de la boîte. La variante Nil ne stocke pas de valeurs, donc elle nécessite moins d'espace que la variante Cons. Nous savons maintenant qu'une valeur de type List prendra la taille d'un i32 plus la taille des données de pointeur d'une boîte. En utilisant une boîte, nous avons rompu la chaîne infinie et récursive, de sorte que le compilateur peut déterminer la taille qu'il doit allouer pour stocker une valeur de type List. La Figure 15-2 montre à quoi ressemble maintenant la variante Cons.

Figure 15-2 : Une List dont la taille n'est pas infinie, car Cons contient un Box

Les boîtes ne fournissent que l'indirection et l'allocation sur le tas ; elles n'ont pas d'autres capacités spéciales, comme celles que nous verrons avec les autres types de pointeurs intelligents. Elles n'ont également pas la surcharge de performance que ces capacités spéciales entraînent, de sorte qu'elles peuvent être utiles dans des cas comme la liste cons où l'indirection est la seule caractéristique dont nous avons besoin. Nous examinerons d'autres cas d'utilisation des boîtes au Chapitre 17.

Le type Box<T> est un pointeur intelligent car il implémente le trait Deref, qui permet de traiter les valeurs de type Box<T> comme des références. Lorsqu'une valeur de type Box<T> sort de portée, les données stockées sur le tas vers lesquelles la boîte pointe sont également nettoyées en raison de l'implémentation du trait Drop. Ces deux traits seront encore plus importants pour la fonctionnalité fournie par les autres types de pointeurs intelligents que nous discuterons dans le reste de ce chapitre. Explorerons ces deux traits en détail.

Sommaire

Félicitations ! Vous avez terminé le laboratoire Utiliser Box pour pointer vers des données sur le tas. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.