
Les tuples : regrouper des valeurs de types différents
Apprenez à utiliser les tuples en Rust pour regrouper un nombre fixe de valeurs de types potentiellement différents. Idéal pour retourner plusieurs valeurs d'une fonction.
Les tuples : l'art de combiner des données hétérogènes simplement
Au-delà des types scalaires qui représentent une seule valeur, Rust offre plusieurs moyens de regrouper des données. Parmi eux, le tuple se distingue par sa simplicité et sa capacité à agréger un nombre fixe de valeurs de types potentiellement différents en une seule entité. Les tuples sont une construction légère et efficace, souvent utilisée lorsque la structure des données est simple et qu'il n'est pas nécessaire de nommer explicitement chaque composant.
Ce sous-chapitre vous guidera à travers la création, l'utilisation et la déstructuration des tuples en Rust. Vous découvrirez comment ils peuvent être pratiques pour retourner plusieurs valeurs d'une fonction, ou pour créer des ensembles de données ad hoc sans la formalité de définir une structure (struct) complète.
Nous explorerons également le tuple vide, `()`, connu sous le nom de "type unité", qui joue un rôle particulier en Rust, notamment comme type de retour implicite des fonctions qui ne retournent aucune valeur significative. Comprendre les tuples enrichira votre boîte à outils pour structurer les informations dans vos programmes Rust.
Création et structure d'un tuple
Un tuple en Rust est créé en listant une séquence de valeurs, séparées par des virgules, et en les entourant de parenthèses. Chaque valeur à l'intérieur d'un tuple peut avoir un type différent. La longueur d'un tuple (le nombre d'éléments qu'il contient) est fixe et fait partie de son type. Par exemple, `(i32, f64, u8)` est un type de tuple différent de `(i32, f64)`.
Voici comment créer des tuples :
fn main() {
// Un tuple avec trois éléments de types différents
let personne: (String, i32, bool) = (String::from("Alice"), 30, true);
// Rust peut inférer les types si les valeurs sont fournies
let point = (10.5, -3.2, 7.0); // Inféré comme (f64, f64, f64)
// Un tuple contenant d'autres tuples
let rectangle = ((0, 0), (100, 50)); // Un tuple de deux tuples (i32, i32)
println!("Personne : {:?}", personne); // {:?} est utilisé pour afficher des types qui implémentent le trait Debug
println!("Point : ({}, {}, {})", point.0, point.1, point.2);
println!("Rectangle : coin_sup_gauche=({},{}), coin_inf_droit=({},{})",
rectangle.0.0, rectangle.0.1, rectangle.1.0, rectangle.1.1);
}Notez l'utilisation de `{:?}` dans `println!`. C'est un marqueur de formatage qui demande à Rust d'utiliser une sortie de débogage pour le type. Les tuples, comme de nombreux types en Rust, peuvent être affichés de cette manière s'ils contiennent des types qui implémentent eux-mêmes le trait `std::fmt::Debug`.
La taille d'un tuple est connue à la compilation. Une fois qu'un tuple est déclaré avec un certain nombre d'éléments et des types spécifiques pour ces éléments, cela ne peut plus changer. Si vous avez besoin d'une collection dont la taille peut varier, vous utiliserez d'autres types comme les vecteurs (`Vec<T>`).
Accéder aux éléments d'un tuple : déstructuration et indexation
Il existe deux manières principales d'accéder aux valeurs contenues dans un tuple : la déstructuration et l'accès par index.
La déstructuration (destructuring) permet d'extraire les valeurs d'un tuple et de les lier à des variables distinctes en une seule opération. C'est souvent la manière la plus idiomatique et la plus lisible d'utiliser les valeurs d'un tuple.Exemple de déstructuration :
fn main() {
let utilisateur: (String, i32, String) =
(String::from("Bob"), 42, String::from("bob@example.com"));
// Déstructuration du tuple `utilisateur`
let (nom, age, email) = utilisateur;
// `utilisateur` est déplacé ici car String n'est pas `Copy`.
// Si le tuple contenait que des types `Copy` (comme i32, f64, bool),
// le tuple lui-même serait toujours utilisable après la déstructuration.
println!("Nom : {}", nom);
println!("Age : {}", age);
println!("Email : {}", email);
// Si vous n'avez besoin que de certaines valeurs, vous pouvez utiliser `_` pour ignorer les autres.
let coordonnees = (10, 20, 30);
let (x, _, z) = coordonnees; // Ignore la valeur y (20)
println!("x = {}, z = {}", x, z);
}Lorsque vous déstructurez un tuple contenant des types qui ne sont pas `Copy` (comme `String`), les valeurs sont déplacées du tuple vers les nouvelles variables. Le tuple original pourrait alors ne plus être utilisable si toutes ses valeurs ont été déplacées.
L'accès par index permet de récupérer un élément spécifique d'un tuple en utilisant un point (`.`) suivi de l'index numérique de l'élément. Les indices des tuples commencent à 0.Exemple d'accès par index :
fn main() {
let mesures = (25.5, 10.2, 100u16);
let temperature = mesures.0; // Accède au premier élément (25.5)
let humidite = mesures.1; // Accède au deuxième élément (10.2)
let pression = mesures.2; // Accède au troisième élément (100)
println!("Température : {}°C", temperature);
println!("Humidité : {}%", humidite);
println!("Pression : {} hPa", pression);
}Cette méthode est directe mais peut être moins lisible que la déstructuration si le tuple a de nombreux éléments ou si la signification des positions n'est pas immédiatement évidente.
Cas d'usage des tuples : retours de fonctions et le type unité `()`
Les tuples sont particulièrement pratiques pour permettre à une fonction de retourner plusieurs valeurs. De nombreux langages nécessitent de retourner un objet ou une structure dédiée, mais Rust permet de simplement retourner un tuple.
Exemple d'une fonction retournant un tuple :
fn calculer_stats(nombres: &[i32]) -> (i32, i32, f64) {
if nombres.is_empty() {
return (0, 0, 0.0); // Retourne un tuple de valeurs par défaut si la slice est vide
}
let mut somme = 0;
let mut min = nombres[0];
let mut max = nombres[0];
for &nombre in nombres {
somme += nombre;
if nombre < min { min = nombre; }
if nombre > max { max = nombre; }
}
let moyenne = somme as f64 / nombres.len() as f64;
(min, max, moyenne) // Expression de retour du tuple
}
fn main() {
let data = [10, 20, 5, 15, 25];
let (minimum, maximum, moyenne_val) = calculer_stats(&data);
println!("Données : {:?}", data);
println!("Minimum : {}", minimum);
println!("Maximum : {}", maximum);
println!("Moyenne : {:.2}", moyenne_val);
}Un cas spécial de tuple est le tuple vide, noté `()`. C'est un type appelé le type unité (ou simplement "unité"). Il n'a qu'une seule valeur possible, également notée `()`. Le type unité est le type de retour implicite des fonctions qui ne retournent aucune autre valeur. Par exemple, la fonction `main` retourne `()`, tout comme une fonction `println!` ou une fonction qui effectue une action sans produire de résultat.
fn saluer(nom: &str) {
println!("Bonjour, {}!", nom);
// Cette fonction retourne implicitement ()
}
fn main() {
let resultat: () = saluer("Monde");
// `resultat` est de type () et a la valeur ().
// Cela n'est généralement pas utile d'assigner le résultat d'une telle fonction,
// mais c'est pour illustrer le concept.
}Les expressions qui ne produisent pas de valeur, comme une instruction `let` seule ou un bloc de code se terminant par un point-virgule (sauf si c'est la dernière expression d'une fonction qui est retournée), ont également le type unité `()`. Ce concept unifie la manière dont Rust traite les expressions et les instructions.
En conclusion, les tuples offrent un moyen concis et efficace de regrouper des ensembles fixes de données hétérogènes. Leur utilisation pour les retours de fonctions multiples et la compréhension du type unité `()` sont des aspects importants de la programmation idiomatique en Rust.