En 2020, quand on parle de langage "natif" ou bas niveau, étonnamment, on pense encore beaucoup au C et au C++. Bien que les dernières versions du C++ fassent évoluer la syntaxe, le fond même du langage n'a pas vraiment changé.
Et il faut bien avouer qu'en face de langages plus haut niveau comme le JavaScript ou le Python et l'évolution fulgurante de tous les frameworks de la communauté web, le C++ souffre de quelques défauts qui deviennent récurrents, notamment :
- Intégrer une bibliothèque peut être vraiment (très) fastidieux, même parfois jusqu'au point de préférer réimplémenter la fonctionnalité soi-même.
- Il y a souvent des problèmes d'accès mémoire : des pointeurs zombies, des erreurs de segmentation, des conflits d'accès à des ressources partagées entre plusieurs threads...
Rust, lui, ne souffre pas de ces défauts.
La philosophie Rust
Rust est un langage safe grâce à un système de borrow checker redoutable :
- Les variables sont par défaut immutables.
- Chaque variable mutable ne peut être modifiée que dans un seul scope à la fois, c'est le concept d'ownership.
- La durée de vie est vérifiée pour chaque variable : le lifetime. Si le compilateur a un doute par exemple lorsqu'on compare 2 variables entre elles, il faudra spécifier de manière explicite la durée de vie de chaque variable.
Le compilateur rustc
est strict. Vraiment strict. Dites vous bien que vous ne vous êtes jamais vraiment battu avec un compilateur tant que vous n'avez pas essayé rustc
! Mais la bonne nouvelle, c'est que ses messages d'erreurs sont explicites, et les suggestions aideront à résoudre les problèmes.
Rust a donc une syntaxe qui se veut explicite mais concise.
Avec Cargo : un super environnement de dev
Packages
Rust et son gestionnaire de paquets Cargo fonctionnent sous forme de modules hiérarchiques. Un programme Rust est donc structuré, et l'intégration d'une bibliothèque externe consiste simplement à ajouter nom_module=1.0.0
dans le Cargo.toml
et à laisser la commande cargo install
faire le reste ! (Hey les développeurs Node.js, ça vous rappelle quelque chose ?)
Tests
Avec cargo test
il est possible d'exécuter toutes les portions de code taggées #[test]
pour les tests unitaires :
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
mais aussi tous les fichiers placés dans un répertoire test/
pour les tests d'intégration.
Compilation
Le code se compile avec cargo build
ou directement cargo run
pour les feignants ;)
Il est possible de faire de la cross-compilation en spécifiant la target (et en installant le nécessaire) : cargo build --target=arm-linux-androideabi
Il est même possible de compiler en Web Assembly pour intégrer un traitement important côté client, comme expliqué dans ce tutoriel de Mozilla.
Autres concepts clés du langage
Par ailleurs, le Rust est également doté (liste non exhaustive bien sûr):
- d'inférence de type :
let a = 1 + 2; // -> i32
- de fonction lambda appelées closures :
let closure_annotated = |i: i32| -> i32 { i + 1 };
- de tuples :
let tuple = (1, "hello", 4.5, true);
- de structures :
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
Mais également d'objets bien spécifiques au langage :
- les objets
Option
etResult
qui servent à encapsuler de manière sûre le résultat d'une fonction - les traits : on peut les comparer au design pattern "Interface". Ils sont l'unique solution pour l'héritage de fonctions sur des structures de données. L'avantage en Rust, c'est que beaucoup de traits standards sont déjà implémentés, et il suffit alors de les spécifier sur une structure pour bénéficier de tous leurs bienfaits :
Debug
,Format
,Serialize
,Clone
,Copy
,... -
match
: les débutants croiront qu'il s'agit d'un simpleswitch/case
. Erreur ! C'est bien plus que ça ! Unmatch
ne se contente pas de tester des valeurs, mais des patterns !
let x = 'c';
match x {
'a'..='i' => println!("early ASCII letter"), // with range pattern
'j' => println("middle ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"), // all other values
}
À noter aussi que le compilateur fera la tête tant que toutes les valeurs possibles de x
ne seront pas gérées par le match
! Souvenez-vous, Rust est un langage safe !
- macros (pour un public averti) : contrairement au C++, une macro n'est pas seulement du code qui se substituera à un symbole : c'est une instruction de pré-compilation qui va réagir à des patterns, tel un parseur de code ! On les reconnaît facilement, car elles se terminent par un point d'exclamation, comme
println!(...)
. Voici par exemple la définition de la macro standardvec![...]
:
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
Elle est très pratique pour instancier rapidement un conteneur vec
directement avec des valeurs :
let my_vec = vec![1, 2, 3, 10];
... et bien d'autres !
Pour aller plus loin
La doc officielle pour apprendre le Rust : Rust by Example
Essayer du Rust en ligne, sans rien installer : Rust Playground