Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Utiliser Box<T> pour pointer vers des données sur le tas

Le pointeur intelligent le plus simple est une boîte (box), dont le type s’é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. Reportez-vous au chapitre 4 pour revoir la différence entre la pile et le tas.

Les boîtes n’ont pas de surcoût en performance, hormis le fait de stocker leurs données sur le tas au lieu de la pile. Mais elles n’ont pas non plus beaucoup de capacités supplémentaires. Vous les utiliserez le plus souvent dans ces situations : - Quand 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 - Quand vous avez une grande quantité de données, et que vous voulez transférer la possession tout en vous assurant que les données ne seront pas copiées - Quand vous voulez posséder une valeur, et que vous vous souciez uniquement du fait qu’elle implémente un trait particulier plutôt que d’être d’un type spécifique

  • Quand 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
  • Quand vous avez une grande quantité de données, et que vous voulez transférer la possession tout en vous assurant que les données ne seront pas copiées
  • Quand vous voulez posséder une valeur, et que vous vous souciez uniquement du fait qu’elle implémente un trait particulier plutôt que d’être d’un type spécifique

Nous démontrerons la première situation dans « Permettre les types récursifs avec les boîtes ». Dans le deuxième cas, transférer la possession 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. Ainsi, seule la petite quantité de données du pointeur est copiée sur la pile, tandis que les données référencées restent au même endroit sur le tas. Le troisième cas est connu sous le nom d’objet trait, et « Utiliser les objets trait pour abstraire des comportements communs » au chapitre 18 est consacré exclusivement à ce sujet. Donc, ce que vous apprenez ici vous sera utile à nouveau au chapitre 18{N}!

Stocker des données sur le tas

Avant de discuter du cas d’utilisation de Box<T> pour le stockage sur le tas, nous couvrirons la syntaxe et comment interagir avec les valeurs stockées dans un Box<T>.

L’encart 15-1 montre comment utiliser une boîte pour stocker une valeur i32 sur le tas.

Filename: src/main.rs
fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}
Listing 15-1: Storing an i32 value on the heap using a box

Nous définissons la variable b avec la valeur d’un 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 manière similaire à ce que nous ferions si ces données étaient sur la pile. Comme toute valeur possédée, quand une boîte sort de la 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 vers lesquelles elle pointe (stockées sur le tas).

Placer une seule valeur sur le tas n’est pas très utile, donc vous n’utiliserez pas les boîtes seules de cette manière très souvent. Avoir des valeurs comme un seul i32 sur la pile, où elles sont stockées par défaut, est plus approprié dans la majorité des situations. Examinons un cas où les boîtes nous permettent de définir des types que nous ne pourrions pas définir sans elles.

Permettre les types récursifs avec les boîtes

Une valeur d’un type récursif peut contenir une autre valeur du même type comme partie d’elle-même. Les types récursifs posent un problème car Rust a besoin de savoir à la compilation combien d’espace un type occupe. Cependant, l’imbrication de valeurs de types récursifs pourrait théoriquement continuer à l’infini, donc Rust ne peut pas savoir combien d’espace la valeur nécessite. Comme les boîtes ont une taille connue, nous pouvons permettre les types récursifs en insérant une boîte dans la définition du type récursif.

Comme exemple de type récursif, explorons la liste cons. C’est un type de données couramment trouvé dans les langages de programmation fonctionnelle. Le type de liste cons que nous définirons est simple à l’exception de la récursion ; par conséquent, les concepts de l’exemple avec lequel nous travaillerons seront utiles chaque fois que vous rencontrerez des situations plus complexes impliquant des types récursifs.

Comprendre la liste cons

Une liste cons est une structure de données qui provient du langage de programmation Lisp et de ses dialectes, elle est composée de paires imbriquées, et c’est la version Lisp d’une liste chaînée. Son nom vient de la fonction cons (abréviation de construct function, fonction de construction) 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 composé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 courant et celle de l’élément suivant. Le dernier élément de la liste ne contient qu’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” abordé au 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, quand vous avez une liste d’éléments en Rust, Vec<T> est un meilleur choix. D’autres types de données récursifs plus complexes sont utiles dans diverses situations, mais en commençant par la liste cons dans ce chapitre, nous pouvons explorer comment les boîtes nous permettent de définir un type de données récursif sans trop de distraction.

L’encart 15-2 contient une définition d’enum pour une liste cons. Notez que ce code ne compilera pas encore, car le type List n’a pas de taille connue, ce que nous allons démontrer.

Filename: src/main.rs
enum List {
    Cons(i32, List),
    Nil,
}

fn main() {}
Listing 15-2: The first attempt at defining an enum to represent a cons list data structure of i32 values

Remarque : nous implémentons une liste cons qui ne contient que des valeurs i32 pour les besoins de cet exemple. Nous aurions pu l’implémenter avec des génériques, comme nous l’avons vu au chapitre 10, pour définir un type de liste cons pouvant stocker des valeurs de n’importe quel type.

Utiliser le type List pour stocker la liste 1, 2, 3 ressemblerait au code de l’encart 15-3.

Filename: src/main.rs
enum List {
    Cons(i32, List),
    Nil,
}

// --snip--

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

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Listing 15-3: Using the List enum to store the list 1, 2, 3

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

Si nous essayons de compiler le code de l’encart 15-3, nous obtenons l’erreur montrée dans l’encart 15-4.

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

error[E0391]: cycle detected when computing when `List` needs drop
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which immediately requires computing when `List` needs drop again
  = note: cycle used when computing whether `List` needs drop
  = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
Listing 15-4: The error we get when attempting to define a recursive enum

L’erreur indique que ce type “à une taille infinie”. La raison est que nous avons défini List avec une variante qui est récursive : elle contient directement une autre valeur d’elle-même. En conséquence, Rust ne peut pas déterminer combien d’espace il a besoin pour stocker une valeur List. Analysons pourquoi nous obtenons cette erreur. D’abord, nous verrons comment Rust décide de l’espace nécessaire pour stocker une valeur d’un type non récursif.

Calculer la taille d’un type non récursif

Rappelez-vous l’enum Message que nous avons définie dans l’encart 6-2 lorsque nous avons abordé les définitions d’enum au chapitre 6 : rust {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-02/src/main.rs:here}}

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

fn main() {}

Pour déterminer combien d’espace allouer pour une valeur Message, Rust parcourt chacune des variantes pour voir laquelle nécessite le plus d’espace. Rust constate que Message::Quit n’a besoin d’aucun espace, Message::Move a besoin de suffisamment d’espace pour stocker deux valeurs i32, et ainsi de suite. Comme une seule variante sera utilisée, l’espace maximum dont une valeur Message aura besoin est l’espace nécessaire pour stocker la plus grande de ses variantes.

Comparez cela avec ce qui se passe lorsque Rust essaie de déterminer combien d’espace un type récursif comme l’enum List de l’encart 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’un espace égal à 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 variantes, 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 à l’infini, comme illustré dans la figure 15-1.

Une liste Cons infinie : un rectangle étiqueté « Cons » divisé en deux plus petits rectangles. Le premier petit rectangle contient l’étiquette « i32 », et le second petit rectangle contient l’étiquette « Cons » ainsi qu’une version réduite du rectangle « Cons » extérieur. Les rectangles « Cons » continuent de contenir des versions de plus en plus petites d’eux-mêmes jusqu’à ce que le plus petit rectangle d’une taille raisonnable contienne un symbole d’infini, indiquant que cette répétition continue indéfiniment.

Figure 15-1 : Une List infinie composée de variantes Cons infinies

Obtenir un type récursif avec une taille connue

Comme Rust ne peut pas déterminer combien d’espace allouer pour les types définis récursivement, le compilateur donne une erreur avec cette suggestion utile :

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

Dans cette suggestion, indirection signifie qu’au lieu de stocker une valeur directement, nous devrions modifier la structure de données pour stocker la valeur indirectement en stockant un pointeur vers la valeur à la place.

Comme 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 laquelle il pointe. Cela signifie que nous pouvons mettre un Box<T> dans la variante Cons au lieu d’une autre valeur List directement. Le Box<T> pointera vers la prochaine valeur List qui sera sur le tas plutôt qu’à l’intérieur de la variante Cons. Conceptuellement, nous avons toujours une liste, créée avec des listes contenant d’autres listes, mais cette implémentation ressemble désormais davantage à placer les éléments les uns à côté des autres plutôt que les uns dans les autres.

Nous pouvons modifier la définition de l’enum List de l’encart 15-2 et l’utilisation de List de l’encart 15-3 pour obtenir le code de l’encart 15-5, qui compilera.

Filename: 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: The definition of List that uses Box<T> in order to have a known size

La variante Cons a besoin de la taille d’un i32 plus l’espace pour stocker les données du pointeur de la boîte. La variante Nil ne stocké aucune valeur, donc elle nécessite moins d’espace sur la pile que la variante Cons. Nous savons maintenant que toute valeur List occupera la taille d’un i32 plus la taille des données du pointeur d’une boîte. En utilisant une boîte, nous avons brisé la chaîne récursive infinie, de sorte que le compilateur peut déterminer la taille nécessaire pour stocker une valeur List. La figure 15-2 montre à quoi ressemble désormais la variante Cons.

Un rectangle étiqueté « Cons » divisé en deux plus petits rectangles. Le premier petit rectangle contient l’étiquette « i32 », et le second petit rectangle contient l’étiquette « Box » avec un rectangle intérieur contenant l’étiquette « usize », représentant la taille finie du pointeur de la box.

Figure 15-2 : Une List qui n’est pas de taille 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 pas non plus le surcoût en performance que ces capacités spéciales entraînent, ce qui les rend utiles dans des cas comme la liste cons où l’indirection est la seule fonctionnalité dont nous avons besoin. Nous examinerons d’autres cas d’utilisation des boîtes au chapitre 18.

Le type Box<T> est un pointeur intelligent car il implémente le trait Deref, qui permet aux valeurs Box<T> d’être traitées comme des références. Quand une valeur Box<T> sort de la portée, les données du tas vers lesquelles la boîte pointe sont également nettoyées grâce à l’implémentation du trait Drop. Ces deux traits seront encore plus importants pour les fonctionnalités fournies par les autres types de pointeurs intelligents que nous aborderons dans le reste de ce chapitre. Explorons ces deux traits plus en détail.