
Opérations de base et inférence de type
Découvrez les opérations arithmétiques et logiques en Rust, et comprenez comment l'inférence de type simplifie l'écriture du code tout en maintenant la sécurité.
Manipuler les données : opérations et la souplesse du typage en Rust
Après avoir exploré les types de données scalaires, il est temps de voir comment interagir avec eux. Ce sous-chapitre se penche sur les opérations de base que vous pouvez effectuer sur les types numériques et booléens en Rust. De plus, nous allons approfondir un mécanisme clé qui rend l'écriture de code Rust à la fois sûre et agréable : l'inférence de type.
Vous apprendrez à effectuer des calculs arithmétiques classiques et à utiliser des opérateurs logiques pour combiner des conditions. Ces opérations sont le pain quotidien de la programmation et Rust les implémente de manière robuste et prévisible.
Parallèlement, nous démystifierons l'inférence de type. Rust est un langage à typage statique, ce qui signifie que chaque variable a un type fixe déterminé à la compilation. Cependant, Rust est souvent capable de déduire ce type sans que vous ayez à l'écrire explicitement. Comprendre quand et comment fonctionne l'inférence de type est essentiel pour écrire du code Rust concis et idiomatique.
Opérations arithmétiques et logiques : les outils du calcul et de la décision
Rust supporte toutes les opérations arithmétiques standard pour ses types numériques. Celles-ci incluent :
- L'addition :
+ - La soustraction :
- - La multiplication :
* - La division :
/ - Le reste (modulo) :
%
Ces opérateurs fonctionnent comme on pourrait s'y attendre. Voici quelques exemples :
fn main() {
// Opérations sur les entiers
let sum = 5 + 10; // sum est 15 (i32 par défaut)
let difference = 95 - 40; // difference est 55 (i32)
let product = 4 * 30; // product est 120 (i32)
let quotient_int = 56 / 32; // quotient_int est 1 (division entière, i32)
let remainder = 43 % 5; // remainder est 3 (i32)
println!("Somme entière : {}", sum);
println!("Différence entière : {}", difference);
println!("Produit entier : {}", product);
println!("Quotient entier : {}", quotient_int);
println!("Reste entier : {}", remainder);
// Opérations sur les flottants
let quotient_float = 56.0 / 32.0; // quotient_float est 1.75 (f64 par défaut)
let twenty = 20.0f32; // type f32 explicite
let point_five = 0.5f32;
let ten = twenty * point_five; // ten est 10.0 (f32)
println!("Quotient flottant : {}", quotient_float);
println!("Dix (flottant) : {}", ten);
}Il est important de noter que la division entière (par exemple, `5 / 2`) tronque vers zéro, donnant `2` dans ce cas. Si vous avez besoin d'une division avec un résultat flottant, au moins l'un des opérandes doit être un type flottant (ou vous devez convertir les entiers en flottants avant la division).
Pour les types booléens (`bool`), Rust fournit les opérateurs logiques standard :
- ET logique :
&&(évalue à `true` si les deux opérandes sont `true`) - OU logique :
||(évalue à `true` si au moins un des opérandes est `true`) - NON logique :
!(inverse la valeur booléenne de son opérande)
Ces opérateurs sont souvent utilisés dans les expressions conditionnelles :
fn main() {
let a = true;
let b = false;
println!("a ET b : {}", a && b); // Affiche: a ET b : false
println!("a OU b : {}", a || b); // Affiche: a OU b : true
println!("NON a : {}", !a); // Affiche: NON a : false
println!("NON b : {}", !b); // Affiche: NON b : true
let x = 5;
let y = 10;
if x > 0 && y < 20 {
println!("x est positif ET y est inférieur à 20.");
}
}Rust utilise également des opérateurs de comparaison qui produisent des valeurs booléennes : `==` (égal), `!=` (non égal), `<` (inférieur), `>` (supérieur), `<=` (inférieur ou égal), `>=` (supérieur ou égal).
L'inférence de type : quand Rust devine pour vous
Rust est un langage à typage statique. Cela signifie que le type de chaque variable, fonction et expression doit être connu par le compilateur au moment de la compilation. Ce typage statique est une source majeure de la sécurité et de la performance de Rust. Cependant, écrire explicitement les types pour chaque petite variable peut devenir verbeux.
C'est là qu'intervient l'inférence de type. Le compilateur Rust est souvent capable de déduire le type d'une variable en se basant sur la valeur qui lui est assignée et sur la manière dont cette variable est utilisée par la suite. Cela vous permet d'omettre l'annotation de type dans de nombreux cas, rendant le code plus concis.
Par exemple :
fn main() {
let an_integer = 42; // Rust infère i32 (type entier par défaut)
let a_float = 3.14; // Rust infère f64 (type flottant par défaut)
let is_true = true; // Rust infère bool
let a_char = '🚀'; // Rust infère char
// Vous pourriez écrire explicitement :
// let an_integer: i32 = 42;
// let a_float: f64 = 3.14;
// let is_true: bool = true;
// let a_char: char = '🚀';
println!("Entier : {}, Flottant : {}, Booléen : {}, Caractère : {}",
an_integer, a_float, is_true, a_char);
}Dans l'exemple ci-dessus, nous n'avons pas eu besoin de spécifier les types car le compilateur a pu les déduire sans ambiguïté à partir des littéraux fournis.
L'inférence de type fonctionne bien pour les littéraux et les opérations simples. Cependant, il y a des situations où le compilateur a besoin d'un peu d'aide. C'est notamment le cas lorsque plusieurs types pourraient être valides pour une même valeur ou une même opération. Un exemple classique est la méthode `parse()` qui peut convertir une chaîne de caractères en divers types numériques.
fn main() {
let s = "100";
// Ici, une annotation de type est nécessaire car .parse() peut retourner plusieurs types numériques.
let number_u32: u32 = s.parse().expect("Ce n'est pas un nombre u32!");
let number_i64: i64 = s.parse().expect("Ce n'est pas un nombre i64!");
// let ambiguous_number = s.parse().expect("Quel type ?"); // ERREUR: type annotations needed
println!("Nombre u32 : {}", number_u32);
println!("Nombre i64 : {}", number_i64);
}Dans le cas de `ambiguous_number`, si nous ne fournissons pas d'annotation de type (`: u32`, `: i64`, etc.), le compilateur émettra une erreur car il ne sait pas vers quel type numérique spécifique il doit essayer de convertir la chaîne. L'annotation `turbofish` (::) est une autre syntaxe pour fournir cette information : let number_u32 = s.parse::.
Il est important de se rappeler que l'inférence de type ne rend pas Rust dynamiquement typé. Chaque variable a toujours un type unique et fixe, déterminé à la compilation. L'inférence est simplement un mécanisme de commodité qui permet au compilateur de déterminer ce type pour vous lorsque c'est possible.
En pratique, les développeurs Rust tendent à omettre les annotations de type lorsque le type est évident d'après le contexte (par exemple, `let count = 0;`) et à les ajouter lorsque cela améliore la clarté ou lorsque le compilateur l'exige. C'est un équilibre entre concision et lisibilité. Les signatures de fonctions, en revanche, requièrent presque toujours des annotations de type explicites pour les paramètres et les valeurs de retour, car elles forment le contrat de l'interface de la fonction.