Acheter version papier

Rust

Fork me on GitHub

II. Spécificités de Rust

8. Sized et String vs str

Ce chapitre approfondit ce dont nous avons déjà vu dans le chapitre sur les variables et plus particulièrement les slices, à savoir : la différence entre String et str. Ou encore : "Pourquoi deux types pour représenter la même chose ?". Tâchons d'y répondre !

str

Le type str représente tout simplement une adresse mémoire et une taille. C'est pourquoi on ne peut modifier son contenu. Mais ce n'est pas la seule chose à savoir à son sujet. Commençons par regarder le code suivant :

Runlet x = "str";

x est donc une variable de type &str. Mais que se passe-t-il si nous tentons de déréférencer x pour obtenir un type str ?

Runlet x = *"str";

Ce qui donnera :

error: the trait `core::marker::Sized` is not implemented for the type `str` [E0277]

Mais quel est donc ce trait Sized, et pourquoi ça pose un problème que str ne l'implémente pas ?

Le trait Sized

str n'est pas le seul type qui n'implémente pas le trait Sized. Les slice non plus ne l'implémentent pas :

Runfn fonction(x: [u32]) {
    // ...
}

Ce qui donne :

error[E0277]: the size for values of type `[u32]` cannot be known at compilation time
 --> src/main.rs:1:8
  |
1 | fn foo(x: [u32]) {
  |        ^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `[u32]`

Le problème est donc que si le trait Sized n'est pas implémenté sur le type, cela signifie que l'on ne peut pas connaître sa taille au moment de la compilation car on ne sait pas combien d'éléments le type contiendra et donc quelle taille en mémoire il occupera. Par conséquent, nous sommes obligés de passer par d'autres types pour les manipuler. Dans le cas des str et des slice, on peut se contenter d'utiliser des références qui ont une taille connue au moment de la compilation :

Runfn fonction(x: &[u32], s: &str) {
    // ...
}

Maintenant revenons-en aux String et aux str.

String

Les String permettent donc de manipuler des chaînes de caractères. En plus de ce que contient str (à savoir : une adresse mémoire et une taille), elles contiennent aussi une capacité qui représente la quantité de mémoire réservée (mais pas nécessairement utilisée).

Pour résumer un peu le tout, str est une vue mémoire de taille constante tandis que String est une structure permettant de manipuler des chaînes de caractères (et donc d'en changer la taille au besoin) et qui peut être déréférencée en str. C'est d'ailleurs pour ça qu'il est très simple de passer de l'un à l'autre :

Runlet x: &str = "a";
// On pourrait aussi utiliser `String::from` ou `str::into`.
let y: String = x.to_owned();
let z: &str = &y;

Vec vs slice

C'est plus ou moins le même fonctionnement : une slice est une vue mémoire de taille constant tandis que le type Vec permet de manipuler une "vue mémoire" (et notamment d'en modifier la taille). En rentrant dans les détails plus techniques, voyez cela comme un pointeur qui pointerait vers une zone mémoire dont la taille serait réallouée au besoin. Exemple :

Runlet x: &[i32] = &[0, 1, 2];
let y: Vec<i32> = x.to_vec();
let z: &[i32] = &y;

Le type String n'est d'ailleurs qu'un wrapper sur un Vec<u8> qu'elle utilise pour manipuler les chaînes de caractères. C'est d'ailleurs pour ça qu'il est possible de créer une String à partir d'un Vec<u8> (avec la méthode String::from_utf8 notamment).

Ce chapitre (et notamment le trait Sized) est particulièrement important pour bien comprendre les mécanismes sous-jacents de Rust. Soyez bien sûr d’avoir tout compris avant de passer à la suite !