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

Les types de données

Chaque valeur en Rust est d’un certain type de données, qui indique à Rust quel genre de données est spécifié afin qu’il sache comment travailler avec ces données. Nous examinerons deux sous-ensembles de types de données : les scalaires et les composés.

Gardez à l’esprit que Rust est un langage à typage statique, ce qui signifie qu’il doit connaître les types de toutes les variables à la compilation. Le compilateur peut généralement inférer le type que nous voulons utiliser en se basant sur la valeur et la façon dont nous l’utilisons. Dans les cas où plusieurs types sont possibles, comme lorsque nous avons converti un String en type numérique en utilisant parse dans la section [« Comparer la supposition au nombre secret »][comparing-the-guess-to-the-secret-number] du chapitre 2, nous devons ajouter une annotation de type, comme ceci :

#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

Si nous n’ajoutons pas l’annotation de type : u32 montrée dans le code précédent, Rust affichera l’erreur suivante, ce qui signifie que le compilateur a besoin de plus d’informations de notre part pour savoir quel type nous voulons utiliser : console {{#include ../listings/ch03-common-programming-concepts/output-only-01-no-type-annotations/output.txt}}

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
  |
2 |     let guess: /* Type */ = "42".parse().expect("Not a number!");
  |              ++++++++++++

For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error

Vous verrez différentes annotations de type pour d’autres types de données.

Les types scalaires

Un type scalaire représente une valeur unique. Rust possède quatre types scalaires principaux : les entiers, les nombres à virgule flottante, les booléens et les caractères. Vous les reconnaîtrez peut-être d’autres langages de programmation. Voyons comment ils fonctionnent en Rust.

Les types entiers

Un entier est un nombre sans composante fractionnaire. Nous avons utilisé un type entier au chapitre 2, le type u32. Cette déclaration de type indique que la valeur qui lui est associée doit être un entier non signé (les types entiers signés commencent par i au lieu de u) qui occupe 32 bits d’espace. Le tableau 3-1 montre les types entiers intégrés à Rust. Nous pouvons utiliser n’importe laquelle de ces variantes pour déclarer le type d’une valeur entière.

Tableau 3-1 : Les types entiers en Rust

TailleSignéNon signé
8 bitsi8u8
16 bitsi16u16
32 bitsi32u32
64 bitsi64u64
128 bitsi128u128
Dépend de l’architectureisizeusize

Chaque variante peut être signée ou non signée et possède une taille explicite. Signé et non signé font référence à la possibilité que le nombre soit négatif — en d’autres termes, si le nombre doit porter un signé (signé) ou s’il sera toujours positif et peut donc être représenté sans signé (non signé). C’est comme écrire des nombres sur du papier : quand le signé importe, un nombre est affiché avec un signé plus ou un signé moins ; cependant, quand on peut supposer sans risque que le nombre est positif, il est affiché sans signé. Les nombres signés sont stockés en utilisant la représentation en [complément à deux][twos-complement].

Chaque variante signée peut stocker des nombres de −(2n − 1) à 2n − 1 − 1 inclus, où n est le nombre de bits que la variante utilise. Ainsi, un i8 peut stocker des nombres de −(27) à 27 − 1, soit de −128 à 127. Les variantes non signées peuvent stocker des nombres de 0 à 2n − 1, donc un u8 peut stocker des nombres de 0 à 28 − 1, soit de 0 à 255.

De plus, les types isize et usize dépendent de l’architecture de l’ordinateur sur lequel votre programme s’exécute : 64 bits si vous êtes sur une architecture 64 bits et 32 bits si vous êtes sur une architecture 32 bits.

Vous pouvez écrire des littéraux entiers sous n’importe laquelle des formes indiquées dans le tableau 3-2. Notez que les littéraux numériques qui peuvent être de plusieurs types numériques permettent un suffixe de type, tel que 57u8, pour désigner le type. Les littéraux numériques peuvent également utiliser _ comme séparateur visuel pour rendre le nombre plus facile à lire, comme 1_000, qui aura la même valeur que si vous aviez spécifié 1000.

Tableau 3-2 : Les littéraux entiers en Rust

Littéraux numériquesExemple
Décimal98_222
Hexadécimal0xff
Octal0o77
Binaire0b1111_0000
Octet (u8 uniquement)b'A'

Alors comment savoir quel type d’entier utiliser ? Si vous n’êtes pas sûr, les valeurs par défaut de Rust sont généralement un bon point de départ : les types entiers ont pour valeur par défaut i32. La situation principale dans laquelle vous utiliseriez isize ou usize est lors de l’indexation d’une collection.

Dépassement d’entier

Supposons que vous ayez une variable de type u8 qui peut contenir des valeurs entre 0 et 255. Si vous essayez de changer la variable pour une valeur en dehors de cette plage, comme 256, un dépassement d’entier se produira, ce qui peut entraîner l’un des deux comportements suivants. Lorsque vous compilez en mode débogage, Rust inclut des vérifications de dépassement d’entier qui provoquent un panic de votre programme à l’exécution si ce comportement se produit. Rust utilise le terme panicking (paniquer) lorsqu’un programme se terminé avec une erreur ; nous discuterons des paniques plus en détail dans la section « Les erreurs irrécupérables avec panic! » du chapitre 9.

Lorsque vous compilez en mode release avec le drapeau --release, Rust n’inclut pas les vérifications de dépassement d’entier qui provoquent des panics. Au lieu de cela, si un dépassement se produit, Rust effectue un bouclage en complément à deux. En bref, les valeurs supérieures à la valeur maximale que le type peut contenir « bouclent » vers la valeur minimale que le type peut contenir. Dans le cas d’un u8, la valeur 256 devient 0, la valeur 257 devient 1, et ainsi de suite. Le programme ne paniquera pas, mais la variable aura une valeur qui n’est probablement pas celle que vous attendiez. S’appuyer sur le comportement de bouclage du dépassement d’entier est considéré comme une erreur.

Pour gérer explicitement la possibilité de dépassement, vous pouvez utiliser ces familles de méthodes fournies par la bibliothèque standard pour les types numériques primitifs :

  • Boucler dans tous les modes avec les méthodes wrapping_*, comme wrapping_add.
  • Retourner la valeur None s’il y a dépassement avec les méthodes checked_*.
  • Retourner la valeur et un booléen indiquant s’il y a eu dépassement avec les méthodes overflowing_*.
  • Saturer aux valeurs minimale ou maximale avec les méthodes saturating_*.

Les types à virgule flottante

Rust possède également deux types primitifs pour les nombres à virgule flottante, qui sont des nombres avec des décimales. Les types à virgule flottante de Rust sont f32 et f64, qui font respectivement 32 bits et 64 bits. Le type par défaut est f64 car sur les processeurs modernes, il est à peu près aussi rapide que f32 mais offre une plus grande précision. Tous les types à virgule flottante sont signés.

Voici un exemple qui montre les nombres à virgule flottante en action :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

Les nombres à virgule flottante sont représentés conformément à la norme IEEE-754.

Les opérations numériques

Rust prend en charge les opérations mathématiques de base que vous attendez pour tous les types numériques : addition, soustraction, multiplication, division et reste. La division entière tronque vers zéro à l’entier le plus proche. Le code suivant montre comment utiliser chaque opération numérique dans une instruction let :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

Chaque expression dans ces instructions utilise un opérateur mathématique et s’évalue en une seule valeur, qui est ensuite liée à une variable. [L’annexe B][appendix_b] contient une liste de tous les opérateurs que Rust fournit.

Le type booléen

Comme dans la plupart des autres langages de programmation, un type booléen en Rust a deux valeurs possibles : true et false. Les booléens font un octet. Le type booléen en Rust est spécifié avec bool. Par exemple :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

La principale façon d’utiliser les valeurs booléennes est à travers les conditions, comme une expression if. Nous verrons comment les expressions if fonctionnent en Rust dans la section [« Flux de contrôle »][control-flow].

Le type caractère

Le type char de Rust est le type alphabétique le plus primitif du langage. Voici quelques exemples de déclaration de valeurs char :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

Notez que nous spécifions les littéraux char avec des guillemets simples, contrairement aux littéraux de chaîne de caractères, qui utilisent des guillemets doubles. Le type char de Rust fait 4 octets et représente une valeur scalaire Unicode, ce qui signifie qu’il peut représenter bien plus que de l’ASCII. Les lettres accentuées ; les caractères chinois, japonais et coréens ; les emojis ; et les espaces de largeur nulle sont tous des valeurs char valides en Rust. Les valeurs scalaires Unicode vont de U+0000 à U+D7FF et de U+E000 à U+10FFFF inclus. Cependant, un « caractère » n’est pas vraiment un concept en Unicode, donc votre intuition humaine de ce qu’est un « caractère » peut ne pas correspondre à ce qu’est un char en Rust. Nous aborderons ce sujet en détail dans [« Stocker du texte encodé en UTF-8 avec les String »][strings] au chapitre 8.

Les types composés

Les types composés peuvent regrouper plusieurs valeurs en un seul type. Rust possède deux types composés primitifs : les tuples et les tableaux.

Le type tuple

Un tuple est un moyen général de regrouper un certain nombre de valeurs de types variés en un seul type composé. Les tuples ont une longueur fixe : une fois déclarés, ils ne peuvent ni grandir ni rétrécir.

Nous créons un tuple en écrivant une liste de valeurs séparées par des virgules entre parenthèses. Chaque position dans le tuple à un type, et les types des différentes valeurs du tuple n’ont pas besoin d’être identiques. Nous avons ajouté des annotations de type optionnelles dans cet exemple :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

La variable tup est liée au tuple entier car un tuple est considéré comme un seul élément composé. Pour extraire les valeurs individuelles d’un tuple, nous pouvons utiliser le filtrage par motif pour déstructurer une valeur de tuple, comme ceci :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

Ce programme crée d’abord un tuple et le lie à la variable tup. Il utilise ensuite un motif avec let pour prendre tup et le transformer en trois variables distinctes, x, y et z. C’est ce qu’on appelle la déstructuration car elle décompose le tuple unique en trois parties. Enfin, le programme affiche la valeur de y, qui est 6.4.

Nous pouvons également accéder directement à un élément du tuple en utilisant un point (.) suivi de l’index de la valeur à laquelle nous voulons accéder. Par exemple :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

Ce programme crée le tuple x puis accède à chaque élément du tuple en utilisant leurs indices respectifs. Comme dans la plupart des langages de programmation, le premier index d’un tuple est 0.

Le tuple sans aucune valeur porte un nom spécial, unit. Cette valeur et son type correspondant s’écrivent tous deux () et représentent une valeur vide ou un type de retour vide. Les expressions retournent implicitement la valeur unit si elles ne retournent aucune autre valeur.

Le type tableau

Une autre façon d’avoir une collection de plusieurs valeurs est d’utiliser un tableau (array). Contrairement à un tuple, chaque élément d’un tableau doit avoir le même type. Contrairement aux tableaux dans certains autres langages, les tableaux en Rust ont une longueur fixe.

Nous écrivons les valeurs d’un tableau sous forme de liste séparée par des virgules entre crochets :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

fn main() {
    let a = [1, 2, 3, 4, 5];
}

Les tableaux sont utiles lorsque vous voulez que vos données soient allouées sur la pile, de la même manière que les autres types que nous avons vus jusqu’ici, plutôt que sur le tas (nous aborderons la pile et le tas plus en détail au [chapitre 4][stack-and-heap]) ou lorsque vous voulez vous assurer d’avoir toujours un nombre fixe d’éléments. Un tableau n’est cependant pas aussi flexible que le type vecteur. Un vecteur est un type de collection similaire fourni par la bibliothèque standard qui peut grandir ou rétrécir car son contenu vit sur le tas. Si vous n’êtes pas sûr de devoir utiliser un tableau ou un vecteur, il y a de fortes chances que vous devriez utiliser un vecteur. Le [chapitre 8][vectors] traite des vecteurs plus en détail.

Cependant, les tableaux sont plus utiles lorsque vous savez que le nombre d’éléments n’aura pas besoin de changer. Par exemple, si vous utilisiez les noms des mois dans un programme, vous utiliseriez probablement un tableau plutôt qu’un vecteur car vous savez qu’il contiendra toujours 12 éléments :

#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

Vous écrivez le type d’un tableau en utilisant des crochets avec le type de chaque élément, un point-virgule, puis le nombre d’éléments dans le tableau, comme ceci :

#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

Ici, i32 est le type de chaque élément. Après le point-virgule, le nombre 5 indique que le tableau contient cinq éléments.

Vous pouvez également initialiser un tableau pour qu’il contienne la même valeur pour chaque élément en spécifiant la valeur initiale, suivie d’un point-virgule, puis la longueur du tableau entre crochets, comme montré ici :

#![allow(unused)]
fn main() {
let a = [3; 5];
}

Le tableau nommé a contiendra 5 éléments qui seront tous initialisés à la valeur 3. C’est la même chose que d’écrire let a = [3, 3, 3, 3, 3]; mais de manière plus concise.

Accès aux éléments d’un tableau

Un tableau est un bloc de mémoire unique d’une taille connue et fixe qui peut être alloué sur la pile. Vous pouvez accéder aux éléments d’un tableau en utilisant l’indexation, comme ceci :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

Dans cet exemple, la variable nommée first obtiendra la valeur 1 car c’est la valeur à l’index [0] dans le tableau. La variable nommée second obtiendra la valeur 2 à l’index [1] dans le tableau.

Accès invalide à un élément de tableau

Voyons ce qui se passe si vous essayez d’accéder à un élément d’un tableau qui dépasse la fin du tableau. Supposons que vous exécutiez ce code, similaire au jeu de devinettes du chapitre 2, pour obtenir un index de tableau de la part de l’utilisateur :

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

Ce code compilé avec succès. Si vous exécutez ce code avec cargo run et saisissez 0, 1, 2, 3 ou 4, le programme affichera la valeur correspondante à cet index dans le tableau. Si vous saisissez plutôt un nombre dépassant la fin du tableau, comme 10, vous verrez une sortie comme celle-ci :

thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Le programme a provoqué une erreur à l’exécution au moment de l’utilisation d’une valeur invalide dans l’opération d’indexation. Le programme s’est terminé avec un message d’erreur et n’a pas exécuté la dernière instruction println!. Lorsque vous tentez d’accéder à un élément par indexation, Rust vérifie que l’index que vous avez spécifié est inférieur à la longueur du tableau. Si l’index est supérieur ou égal à la longueur, Rust paniquera. Cette vérification doit avoir lieu à l’exécution, surtout dans ce cas, car le compilateur ne peut pas savoir quelle valeur un utilisateur saisira lorsqu’il exécutera le code plus tard.

C’est un exemple des principes de sécurité mémoire de Rust en action. Dans de nombreux langages bas niveau, ce type de vérification n’est pas effectué, et lorsque vous fournissez un index incorrect, de la mémoire invalide peut être accédée. Rust vous protège contre ce type d’erreur en quittant immédiatement au lieu de permettre l’accès mémoire et de continuer. Le chapitre 9 aborde plus en détail la gestion des erreurs de Rust et comment vous pouvez écrire du code lisible et sûr qui ne panique pas et ne permet pas d’accès mémoire invalide.