Rust pour les développeurs C#
Si vous écrivez du C# au quotidien, bonne nouvelle : une bonne partie de Rust va vous sembler familière. Rust a des génériques avec contraintes, async/await, un pattern matching (filtrage par motif) très puissant, des chaînes d’itérateurs qui ressemblent beaucoup à LINQ, et un excellent outil en ligne de commande (Cargo) qui fait ce que fait dotnet. Vous savez déjà raisonner en types, en interfaces et en lambdas. Tout ça vous servira.
Le grand changement concerne la mémoire et les erreurs. Rust n’a pas de ramasse-miettes (garbage collector). À la place, chaque valeur a un seul propriétaire, et le compilateur vérifie l’ownership (possession) et le borrowing (emprunt) avant même que votre programme ne s’exécute. Rust n’a pas non plus de null (vous utilisez Option) ni d’exceptions (vous utilisez Result). C’est sur ces trois changements que vous passerez le plus de temps. Cette page fait le lien entre ce que vous connaissez de C# et .NET et Rust, pour que vous puissiez consacrer ce temps à ce qui est vraiment nouveau.
Si vous cherchez plutôt une comparaison générale avec C++, Go et Python, voyez Rust vs C++, Go et Python.
Les concepts du langage : de C# à Rust
Section intitulée « Les concepts du langage : de C# à Rust »Utilisez ce tableau comme un guide de conversation. La dernière colonne renvoie à la leçon de ce site qui explique le côté Rust.
| C# | Rust | Pour en savoir plus |
|---|---|---|
class avec des champs et des méthodes |
struct pour les données + un bloc impl pour les méthodes |
Structs |
interface |
trait |
Traits |
| Classe abstraite, héritage | Pas d’héritage. Utilisez la composition (une struct dans une struct) + des traits avec des méthodes par défaut | Traits, Patrons de conception |
List<IShape> de classes différentes |
Vec<Box<dyn Shape>> (un trait object) |
Trait objects |
| Méthodes d’extension | Votre propre trait, implémenté pour un type que vous n’avez pas écrit (comme String) |
Traits |
Génériques avec where T : IComparable<T> |
Génériques avec des contraintes de trait : T: Ord ou where T: Ord |
Génériques |
null, Nullable<int>, types référence nullables |
Option<T> : soit Some(value), soit None |
Option |
?? et ?. |
.unwrap_or(...), .map(...), .and_then(...) et ? |
Option |
Exceptions, throw, try/catch |
Result<T, E> + l’opérateur ? |
Result, Erreurs personnalisées |
Une exception pour un vrai bug (comme InvalidOperationException) |
panic! (arrête le thread) |
Panic |
IDisposable + using |
Drop, appelé automatiquement quand le propriétaire sort de la portée (RAII) |
Portée, Pointeurs intelligents |
| Ramasse-miettes | Ownership et borrowing, vérifiés à la compilation | Ownership, Borrowing |
record |
struct avec #[derive(Debug, Clone, PartialEq)] |
Structs, Traits courants |
Expression with sur un record |
Syntaxe de mise à jour de struct : Order { total: 5, ..old } |
Structs |
enum (uniquement des entiers nommés) |
enum dont chaque variante peut porter ses propres données |
Enums |
Expressions switch, motifs is |
match (le compilateur vérifie que vous avez couvert tous les cas) |
match, Pattern matching |
Tuples (int, string) |
Tuples (i32, String) |
Tuples |
Paramètres ref / out |
&mut T, ou renvoyer un tuple ou une Option |
Borrowing |
Lambdas, Func<T>, Action<T>, délégués |
Closures, décrites par les traits Fn, FnMut, FnOnce |
Closures, Fn, FnMut et FnOnce |
event |
Un canal (channel), ou une liste de closures dans des Box (callbacks) |
Canaux, Fn, FnMut et FnOnce |
LINQ : Where, Select, Sum, OrderBy, ToList |
Adaptateurs d’itérateurs : filter, map, sum, sort_by (sur un Vec), collect |
Itérateurs, Trier et rechercher |
IEnumerable<T> et yield return |
impl Iterator<Item = T>, ou votre propre type qui implémente Iterator |
Itérateurs personnalisés |
async Task<T> / await |
async fn renvoie un Future ; il faut un runtime comme tokio pour l’exécuter |
Async/Await |
Task.Run, Thread, lock |
std::thread::spawn ou tokio::spawn ; Mutex<T> enveloppe les données qu’il protège |
Concurrence, État partagé |
Propriétés (get; set;) |
Pas de propriétés. Utilisez des champs pub, ou des méthodes getter et setter |
Structs |
string et StringBuilder |
&str (une vue empruntée sur du texte) et String (possédée, qui peut grandir) |
Chaînes |
Interpolation de chaînes $"Hi {name}" |
format!("Hi {name}") et println!("Hi {name}") |
Afficher du texte |
var |
let (immuable par défaut ; écrivez let mut pour autoriser les modifications) |
Variables |
const, readonly |
const, et un simple let (déjà en lecture seule) |
Variables |
Champ static readonly |
static, ou LazyLock pour des valeurs construites au démarrage |
Variables |
namespace et directives using |
Modules (mod) et use |
Modules |
| Assembly / projet | Crate (une bibliothèque ou un binaire) | Cargo et les crates |
Attributs comme [Serializable] |
Attributs comme #[derive(Serialize)] et macros derive |
Macros, JSON avec Serde |
| Réflexion | Quasiment rien à l’exécution. Les macros et derive génèrent du code à la compilation à la place |
Macros |
unsafe et pointeurs |
Blocs unsafe et pointeurs bruts |
Unsafe en Rust |
Span<T>, ReadOnlySpan<char> |
Slices : &[T] et &str |
Slices |
Redéfinition de ToString(), IFormattable |
Implémenter Display (et .to_string() marche gratuitement) |
Traits courants |
IEquatable<T>, IComparable<T>, GetHashCode |
PartialEq/Eq, PartialOrd/Ord, Hash (en général dérivés) |
Traits courants |
Opérateurs de conversion implicit / explicit |
From/Into (et TryFrom quand la conversion peut échouer) |
Conversions |
Surcharge d’opérateurs (operator +) |
Implémenter des traits de std::ops, comme Add |
Traits avancés |
Arithmétique checked / unchecked |
checked_add, wrapping_add, saturating_add |
Dépassement d’entier |
decimal |
Pas de type décimal intégré ; utilisez la crate rust_decimal, ou stockez des centimes dans un entier |
Types de données |
Les collections
Section intitulée « Les collections »La bibliothèque standard couvre presque tout ce que contient System.Collections.Generic. Les noms changent, les idées restent.
| C# | Rust | Pour en savoir plus |
|---|---|---|
List<T> |
Vec<T> |
Vecteurs |
T[] (longueur fixe) |
[T; N] quand la longueur est connue à la compilation, sinon Vec<T> |
Tableaux |
Dictionary<K, V> |
HashMap<K, V> |
HashMap |
SortedDictionary<K, V> |
BTreeMap<K, V> (les clés restent triées) |
HashMap |
HashSet<T> |
HashSet<T> |
HashSet |
SortedSet<T> |
BTreeSet<T> |
HashSet |
Queue<T> |
VecDeque<T> (push_back / pop_front) |
VecDeque et BinaryHeap |
Stack<T> |
Vec<T> avec push / pop |
Vecteurs |
PriorityQueue<TElement, TPriority> |
BinaryHeap<T> (le plus grand d’abord ; enveloppez dans Reverse pour avoir le plus petit d’abord) |
VecDeque et BinaryHeap |
LinkedList<T> |
LinkedList<T> existe, mais vous en aurez rarement besoin ; VecDeque est généralement plus rapide |
VecDeque et BinaryHeap |
ConcurrentDictionary<K, V> |
Mutex<HashMap<K, V>>, ou la crate dashmap |
État partagé |
Un point d’attention : comme Dictionary, une HashMap Rust n’a pas d’ordre fixe. Si vous affichez son contenu, triez-le d’abord ou utilisez une BTreeMap.
L’outillage : de dotnet à cargo
Section intitulée « L’outillage : de dotnet à cargo »C’est la partie que la plupart des développeurs C# apprécient tout de suite. Cargo fait le travail de la CLI dotnet, de MSBuild et de NuGet en un seul outil.
| .NET | Rust | Remarques |
|---|---|---|
CLI dotnet |
cargo |
Compile, exécute, teste, ajoute des paquets, publie. Voir Cargo et les crates |
.csproj |
Cargo.toml |
Court et facile à modifier à la main |
packages.lock.json |
Cargo.lock |
Créé pour vous |
.sln avec plusieurs projets |
Un workspace Cargo | Un Cargo.toml à la racine liste les crates membres |
| NuGet / nuget.org | crates.io | La documentation de l’API de chaque crate publiée est sur docs.rs |
dotnet add package Serilog |
cargo add tracing |
|
dotnet new console |
cargo new my_app (cargo new --lib pour une bibliothèque) |
|
dotnet run / build / test |
cargo run / cargo build / cargo test |
|
-c Release |
--release |
Les builds debug sont beaucoup plus lents à l’exécution ; mesurez les performances en release |
dotnet watch |
cargo watch ou bacon (installés avec cargo install) |
|
dotnet tool install -g |
cargo install |
|
global.json (version du SDK) |
rustup et un fichier rust-toolchain.toml |
|
| Analyseurs Roslyn | cargo clippy |
Des centaines de lints utiles, officiels |
dotnet format |
cargo fmt |
Un seul style standard pour tout le monde |
| Visual Studio / Rider | VS Code + rust-analyzer, ou JetBrains RustRover | rust-analyzer offre la complétion, l’accès à la définition et les erreurs dans le code |
Commentaires de doc XML /// |
Commentaires de doc /// en Markdown + cargo doc |
Les exemples de code dans la doc sont exécutés comme des tests. Voir Documentation |
| xUnit / NUnit / MSTest | #[test] et cargo test intégrés (cargo nextest est un lanceur plus rapide très populaire) |
Voir Les tests |
| Moq | mockall, ou de fausses implémentations écrites à la main qui implémentent votre trait |
|
| Coverlet (couverture de code) | cargo llvm-cov ou cargo tarpaulin |
|
| BenchmarkDotNet | criterion |
Voir Performance |
| dotnet-trace, profileurs | cargo flamegraph, perf, ou n’importe quel profileur natif |
Voici un petit Cargo.toml, pour que vous puissiez le comparer à un .csproj :
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }Bibliothèques et frameworks : « on utilise quoi à la place de… ? »
Section intitulée « Bibliothèques et frameworks : « on utilise quoi à la place de… ? » »C’est la première question que pose chaque équipe .NET. « On utilise Entity Framework. Qu’est-ce qu’on a en Rust ? » Voici les équivalents les plus proches. Rust n’a pas une seule entreprise derrière une « stack par défaut », comme .NET avec Microsoft : vous choisirez donc souvent entre deux ou trois bonnes crates de la communauté.
Accès aux données
Section intitulée « Accès aux données »| .NET | Rust | Remarques |
|---|---|---|
| Entity Framework Core | Diesel : ORM et générateur de requêtes ; vos requêtes sont vérifiées par le système de types à la compilation. Migrations avec diesel_cli |
Mature (2.x), synchrone par défaut (diesel-async existe) |
| Entity Framework Core (async, avec entités) | SeaORM : ORM async avec entités, relations et migrations (sea-orm-cli) |
Le plus proche d’EF ; construit sur sqlx |
| Dapper | sqlx : vous écrivez du SQL brut, et la macro query! le vérifie à la compilation contre votre vraie base de données. Migrations avec sqlx-cli |
Async, très populaire, pas un ORM |
| ADO.NET | sqlx, tokio-postgres, rusqlite (SQLite) |
Pilotes de plus bas niveau |
| StackExchange.Redis | redis |
Si vous aimiez EF, essayez d’abord SeaORM. Si vous aimiez Dapper et écrire votre propre SQL, essayez sqlx. Si vous voulez les garanties les plus fortes à la compilation, essayez Diesel. La page Les bases de données en Rust montre les trois avec du code qui fonctionne.
Web et réseau
Section intitulée « Web et réseau »| .NET | Rust | Remarques |
|---|---|---|
| ASP.NET Core Web API | axum (nous en construisons une dans Projet : API web avec Axum), actix-web, Rocket | axum et actix-web sont très utilisés en production |
| Minimal APIs | axum : les routes sont de simples handlers async fn |
Le style est très proche |
| Pipeline de middlewares | Les layers tower et la crate tower-http (CORS, compression, traçage, timeouts) |
axum utilise tower directement |
| Injection de dépendances intégrée | State dans axum, de simples constructeurs, et des traits |
Voir la ligne sur l’injection de dépendances plus bas |
| Swashbuckle / OpenAPI | utoipa |
|
| SignalR | Les WebSockets dans axum (la feature ws), ou tokio-tungstenite |
Pas de couche de protocole à la SignalR intégrée |
| gRPC (Grpc.AspNetCore) | tonic (avec prost pour protobuf) |
|
HttpClient |
reqwest |
Async par défaut, avec un client bloquant en option |
| Authentification JWT bearer | jsonwebtoken |
Les briques du quotidien
Section intitulée « Les briques du quotidien »| .NET | Rust | Remarques |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde + serde_json |
La référence ; gère aussi TOML, YAML, et d’autres formats. Voir JSON avec Serde |
ILogger / Serilog |
tracing (+ tracing-subscriber), ou log + env_logger |
Journalisation structurée avec des spans. Voir Journalisation |
IConfiguration / appsettings.json |
config ou figment |
Superposez des fichiers et des variables d’environnement, puis désérialisez avec serde |
Conteneur d’injection de dépendances (IServiceCollection) |
En général rien : passez les dépendances aux constructeurs, et utilisez des traits pour ce que vous voulez remplacer dans les tests | shaku existe, mais peu de projets utilisent un conteneur |
| AutoMapper | Écrivez des implémentations de From / Into |
Explicite et vérifié par le compilateur. Voir Conversions |
| FluentValidation / DataAnnotations | validator ou garde (validation par derive) |
|
| Polly (nouvelles tentatives) | backon, ou le middleware retry de tower |
|
| MediatR | Pas d’équivalent direct | De simples traits, ou des canaux entre les tâches |
| Hangfire / tâches en arrière-plan | tokio::spawn pour des tâches dans le processus ; apalis pour des files de tâches persistantes |
|
CancellationToken |
Abandonner (drop) le future, ou utiliser CancellationToken de tokio-util |
Desktop, jeux et plus encore
Section intitulée « Desktop, jeux et plus encore »| .NET | Rust | Remarques |
|---|---|---|
| Blazor WebAssembly | Leptos, Yew, Dioxus |
Tous avant la version 1.0 ; les API changent encore. Voir Rust et WebAssembly |
| WPF / WinForms / MAUI | Tauri (interface web + backend Rust), Slint, egui, Iced |
Tauri 2 et Slint 1.x sont les plus stables. Pas de toolkit graphique officiel |
| Unity | Bevy, ou Rust dans Godot avec la crate godot |
Bevy est avant la 1.0 et casse son API d’une version à l’autre |
| ML.NET | candle, burn, linfa |
Beaucoup plus jeunes que les outils de ML de Python |
| Azure SDK for .NET | Le SDK Azure officiel pour Rust : azure_core, azure_identity, azure_storage_blob, … |
Couvre moins de services que le SDK .NET |
Côte à côte : une classe devient une struct + impl
Section intitulée « Côte à côte : une classe devient une struct + impl »Passons au vrai code. Voici un petit compte bancaire en C# :
var account = new BankAccount("Ada", 100);account.Deposit(50);Console.WriteLine($"{account.Owner} a {account.Balance}");
public class BankAccount{ public string Owner { get; } private decimal balance;
public BankAccount(string owner, decimal balance) { Owner = owner; this.balance = balance; }
public void Deposit(decimal amount) => balance += amount; public decimal Balance => balance;}Et en Rust :
struct BankAccount { owner: String, balance: i64,}
impl BankAccount { // `new` est juste une convention, pas un mot-clé. fn new(owner: &str, balance: i64) -> Self { BankAccount { owner: owner.to_string(), balance } }
// `&mut self` : cette méthode modifie le compte. fn deposit(&mut self, amount: i64) { self.balance += amount; }
// `&self` : cette méthode ne fait que lire. fn balance(&self) -> i64 { self.balance }}
fn main() { let mut account = BankAccount::new("Ada", 100); account.deposit(50); println!("{} a {}", account.owner, account.balance());}Sortie :
Ada a 150Les données et le comportement vivent dans deux blocs séparés. Il n’y a pas de constructeurs, seulement des fonctions qui renvoient Self. Chaque méthode annonce d’emblée si elle lit (&self) ou modifie (&mut self) la valeur. Et comme deposit modifie le compte, la variable doit être déclarée avec let mut. En C#, n’importe quelle méthode peut modifier n’importe quel champ ; en Rust, les modifications se voient dans la signature.
Côte à côte : une interface devient un trait
Section intitulée « Côte à côte : une interface devient un trait »En C#, vous pourriez avoir une interface IShape avec deux classes et une List<IShape> :
var shapes = new List<IShape> { new Circle { Radius = 1 }, new Rectangle { Width = 2, Height = 3 } };foreach (var s in shapes) Console.WriteLine($"{s.Area():F2}");
public interface IShape { double Area(); }
public class Circle : IShape{ public double Radius { get; init; } public double Area() => Math.PI * Radius * Radius;}
public class Rectangle : IShape{ public double Width { get; init; } public double Height { get; init; } public double Area() => Width * Height;}En Rust, l’interface est un trait, et chaque struct l’implémente dans son propre bloc impl :
trait Shape { fn area(&self) -> f64;}
struct Circle { radius: f64,}
struct Rectangle { width: f64, height: f64,}
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }}
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
fn main() { // Box<dyn Shape> joue le rôle de « une référence vers un IShape quelconque ». let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Rectangle { width: 2.0, height: 3.0 }), ];
for shape in &shapes { println!("{:.2}", shape.area()); }}Sortie :
3.146.00En C#, chaque appel via une interface passe par un appel virtuel. En Rust, vous avez le choix : Box<dyn Shape> utilise un appel virtuel (voir Trait objects), alors qu’une fonction générique comme fn print_area<T: Shape>(s: &T) est compilée séparément pour chaque type, sans aucun appel virtuel. Petit bonus : vous pouvez implémenter votre propre trait pour des types que vous n’avez pas écrits, même String ou i32. C’est comme ça que Rust fait ce que font les méthodes d’extension en C#.
Côte à côte : null devient Option
Section intitulée « Côte à côte : null devient Option »Avec les types référence nullables, C# vous avertit à propos de null, mais null existe toujours à l’exécution :
var email = FindEmail(2);if (email is not null) Console.WriteLine(email.ToUpper());else Console.WriteLine("aucune adresse e-mail enregistrée");
Console.WriteLine(FindEmail(2) ?? "inconnue");En Rust, une valeur qui peut manquer a le type Option<T>. Vous ne pouvez pas l’utiliser comme une valeur ordinaire tant que vous n’avez pas traité le cas None :
fn find_email(user_id: u32) -> Option<String> { if user_id == 1 { } else { None }}
fn main() { match find_email(1) { Some(email) => println!("{}", email.to_uppercase()), None => println!("aucune adresse e-mail enregistrée"), }
// `if let` est la forme courte quand un seul cas vous intéresse. if let Some(email) = find_email(2) { println!("trouvée : {email}"); } else { println!("aucune adresse e-mail enregistrée"); }
// unwrap_or fonctionne comme l'opérateur ?? de C#. let email = find_email(2).unwrap_or("inconnue".to_string()); println!("{email}");}Sortie :
aucune adresse e-mail enregistréeinconnueIl n’y a pas de NullReferenceException en Rust, parce qu’il n’y a pas de null à déréférencer. Le compilateur refuse tout simplement que vous appeliez .to_uppercase() sur une Option<String>. Découvrez les méthodes utiles sur la page Option, et les formes courtes sur If Let / While Let.
Côte à côte : try/catch devient Result et ?
Section intitulée « Côte à côte : try/catch devient Result et ? »En C#, l’analyse d’une chaîne peut lever une exception, et vous l’attrapez quelque part plus haut dans la pile d’appels :
static int TotalSeats(string a, string b) => int.Parse(a) + int.Parse(b);
try{ Console.WriteLine(TotalSeats("12", "30")); Console.WriteLine(TotalSeats("12", "trente"));}catch (FormatException e){ Console.WriteLine($"Erreur : {e.Message}");}En Rust, une fonction qui peut échouer le dit dans son type de retour. L’opérateur ? veut dire « si ça a échoué, renvoie l’erreur à mon appelant tout de suite ». Il fait le même travail qu’une exception qui remonte, mais vous le voyez dans le code :
use std::num::ParseIntError;
fn total_seats(a: &str, b: &str) -> Result<i32, ParseIntError> { let first: i32 = a.parse()?; let second: i32 = b.parse()?; Ok(first + second)}
fn main() { for (a, b) in [("12", "30"), ("12", "trente")] { match total_seats(a, b) { Ok(total) => println!("Total : {total}"), Err(e) => println!("Erreur : {e}"), } }}Sortie :
Total : 42Erreur : invalid digit found in stringLe message « invalid digit found in string » (chiffre invalide trouvé dans la chaîne) vient de la bibliothèque standard de Rust : il reste donc en anglais. La grande différence : en C#, n’importe quelle méthode peut lever une exception, et sa signature ne vous le dit pas. En Rust, Result fait partie de la signature, donc les appelants ne peuvent pas oublier de le traiter. Pour vos propres types d’erreur, voyez Erreurs personnalisées. Dans les applications, la crate anyhow vous donne quelque chose de proche de « attraper n’importe quelle exception » (voir anyhow et thiserror).
Côte à côte : LINQ devient une chaîne d’itérateurs
Section intitulée « Côte à côte : LINQ devient une chaîne d’itérateurs »Une requête LINQ typique :
var orders = new List<Order> { new("Ada", 120), new("Grace", 80), new("Linus", 200), new("Ken", 150) };
var bigSpenders = orders .Where(o => o.Total > 100) .OrderByDescending(o => o.Total) .Select(o => o.Customer) .ToList();
Console.WriteLine(string.Join(", ", bigSpenders));Console.WriteLine($"Chiffre d'affaires : {orders.Sum(o => o.Total)}");
record Order(string Customer, int Total);La version Rust utilise des adaptateurs d’itérateurs et des closures :
struct Order { customer: String, total: u32,}
fn main() { let orders = vec![ Order { customer: "Ada".to_string(), total: 120 }, Order { customer: "Grace".to_string(), total: 80 }, Order { customer: "Linus".to_string(), total: 200 }, Order { customer: "Ken".to_string(), total: 150 }, ];
// Where(...).ToList() let mut big: Vec<&Order> = orders.iter().filter(|o| o.total > 100).collect(); // OrderByDescending(...) : le tri se fait sur un Vec, pas sur l'itérateur. big.sort_by(|a, b| b.total.cmp(&a.total)); // Select(...) let names: Vec<&str> = big.iter().map(|o| o.customer.as_str()).collect();
println!("{}", names.join(", "));
// Sum(...) let revenue: u32 = orders.iter().map(|o| o.total).sum(); println!("Chiffre d'affaires : {revenue}");}Sortie :
Linus, Ken, AdaChiffre d'affaires : 550Les itérateurs Rust sont paresseux, exactement comme l’exécution différée de LINQ : rien ne s’exécute tant que vous n’appelez pas quelque chose comme collect(), sum() ou une boucle for. collect(), c’est votre ToList(). Contrairement à LINQ, les chaînes d’itérateurs sont en général compilées vers le même code machine qu’une boucle écrite à la main : ce style agréable ne vous coûte rien. Le tri est expliqué sur la page Trier et rechercher.
Côte à côte : using/IDisposable devient Drop
Section intitulée « Côte à côte : using/IDisposable devient Drop »En C#, vous devez penser à using pour libérer une ressource au bon moment :
using (var conn = new Connection("orders-db")){ Console.WriteLine($"requête sur {conn.Name}");}Console.WriteLine("après le bloc");
class Connection : IDisposable{ public string Name { get; } public Connection(string name) => Name = name; public void Dispose() => Console.WriteLine($"fermeture de la connexion à {Name}");}En Rust, le nettoyage est automatique. Quand le propriétaire d’une valeur sort de la portée, Rust appelle son implémentation de Drop. Il n’y a rien à oublier :
struct Connection { name: String,}
impl Drop for Connection { fn drop(&mut self) { println!("fermeture de la connexion à {}", self.name); }}
fn main() { { let conn = Connection { name: "orders-db".to_string() }; println!("requête sur {}", conn.name); } // `conn` sort de la portée ici, donc drop() s'exécute.
println!("après le bloc");}Sortie :
requête sur orders-dbfermeture de la connexion à orders-dbaprès le blocC’est ce qu’on appelle le RAII, et c’est ainsi que Rust gère tout : les fichiers, les sockets, les verrous et la mémoire elle-même. Un fichier se ferme quand sa valeur File est libérée (dropped). Un Mutex se déverrouille quand son guard est libéré. Il n’y a pas de finaliseur ni de pause du GC ; le nettoyage a lieu à un moment prévisible.
Les pièges quand on vient de C#
Section intitulée « Les pièges quand on vient de C# »Tous les développeurs qui viennent de C# butent sur les mêmes obstacles. Les connaître à l’avance aide.
-
L’affectation déplace la valeur, elle ne copie pas une référence. En C#,
var b = a;avec une classe vous donne deux références vers un même objet. En Rust,let b = a;déplace (move) l’ownership d’uneStringou d’unVecversb, etane peut plus être utilisé :fn main() {let owner = String::from("Ada");let copy = owner;println!("{owner} et {copy}");}error[E0382]: borrow of moved value: `owner`--> src/main.rs:4:16|2 | let owner = String::from("Ada");| ----- move occurs because `owner` has type `String`, which does not implement the `Copy` trait3 | let copy = owner;| ----- value moved here4 | println!("{owner} et {copy}");| ^^^^^ value borrowed here after moveLe compilateur vous dit (en anglais) que
ownera été déplacé à la ligne 3, puis emprunté à la ligne 4 alors qu’il n’est plus valide. La solution : emprunter (let copy = &owner;) ou cloner volontairement (owner.clone()). Lisez d’abord L’ownership ; cette page explique cette règle. -
Le borrow checker. Vous pouvez avoir plusieurs lecteurs (
&T) ou un seul rédacteur (&mut T) à la fois, jamais les deux. Des schémas normaux en C#, comme des objets qui pointent vers leur parent, ou plusieurs objets qui modifient un même objet partagé, demandent des pointeurs intelligents ou une conception différente. Attendez-vous à quelques semaines frustrantes. Ça devient beaucoup plus facile ensuite. -
Stringcontre&str. C# a un seul typestring. Rust en a deux :Stringpossède son texte,&strl’emprunte. Les fonctions prennent en général un&stret les structs stockent en général uneString. Voir Les chaînes. -
Pas d’héritage. Vous ne pouvez pas écrire une classe de base avec des champs communs et des méthodes redéfinies. Utilisez des traits (avec des méthodes par défaut) pour le comportement commun, et placez les données communes dans une struct contenue par les autres structs.
-
Pas d’exceptions. Les erreurs sont des valeurs. Vous écrirez souvent
?, et votremainpeut aussi renvoyer unResult. Appeler.unwrap()est acceptable pendant l’apprentissage, mais c’est la version Rust de « ignorer l’exception et planter ». -
Pas de null. Au début, mettre des
Optionpartout semble demander plus de travail. Au bout d’un moment, vous remarquerez que vous n’avez plus de bugs de référence nulle. -
Des clones explicites. Rust ne copie jamais une valeur du tas dans votre dos. Si vous voyez
.clone(), une vraie copie est faite. C’est voulu : le coût est visible. -
Les temps de compilation. Une compilation complète d’un projet avec beaucoup de crates prend plus de temps qu’un build .NET. Les compilations incrémentales et
cargo check(qui saute la génération de code) sont bien plus rapides ; utilisezcargo checkpendant que vous modifiez votre code. -
L’async a besoin d’un runtime. En .NET, le runtime et le pool de threads sont toujours là. En Rust, une
async fnne fait rien tant qu’un exécuteur ne la lance pas : vous ajoutez donc tokio et mettez#[tokio::main]surmain. Voir Async/Await.
Un parcours d’apprentissage pour les développeurs C#
Section intitulée « Un parcours d’apprentissage pour les développeurs C# »Vous n’êtes pas obligé de lire ce site dans l’ordre. En tant que développeur C#, ce parcours saute ce que vous savez déjà et prend son temps là où Rust est différent :
- Débuter avec Rust : installez Rust et lancez
cargo new. - Les variables et Les types de données : survolez-les, surtout pour apprendre
let mutet les types numériques. - L’ownership, puis Le borrowing, puis Les slices : les pages les plus importantes pour vous. Prenez votre temps.
- Les chaînes :
Stringcontre&str. - Les structs et Les enums, puis Match et Le pattern matching.
- Option et Result, puis Erreurs personnalisées.
- Les traits, Les génériques, Les traits courants et Trait objects.
- Les closures et Les itérateurs : votre LINQ.
- Les vecteurs, HashMap et VecDeque et BinaryHeap.
- Les modules, Cargo et les crates et Les tests.
- Les lifetimes et Pointeurs intelligents.
- La concurrence, L’état partagé et Async/Await.
- JSON avec Serde et Journalisation, puis construisez le projet d’API web.
Exercices
Section intitulée « Exercices »Les deux exercices vous donnent un extrait de code C#. Portez-le en Rust.
1. Porter un record et une requête LINQ
Section intitulée « 1. Porter un record et une requête LINQ »Portez ce code C#. Il doit afficher le nom des produits qui sont en stock et coûtent moins de 20. Indice : créez une struct avec #[derive(Debug, Clone, PartialEq)], puis utilisez filter, map et collect vers un Vec<&str>. Vec<&str> a une méthode .join(", ").
var products = new List<Product>{ new("Cahier", 5, true), new("Casque", 45, true), new("Stylo", 2, false), new("Tasse", 12, true),};
var cheap = products.Where(p => p.InStock && p.Price < 20).Select(p => p.Name);Console.WriteLine(string.Join(", ", cheap));
record Product(string Name, int Price, bool InStock);Voir la solution
#[derive(Debug, Clone, PartialEq)]struct Product { name: String, price: u32, in_stock: bool,}
fn product(name: &str, price: u32, in_stock: bool) -> Product { Product { name: name.to_string(), price, in_stock }}
fn main() { let products = vec![ product("Cahier", 5, true), product("Casque", 45, true), product("Stylo", 2, false), product("Tasse", 12, true), ];
let cheap: Vec<&str> = products .iter() .filter(|p| p.in_stock && p.price < 20) .map(|p| p.name.as_str()) .collect();
println!("{}", cheap.join(", "));}Sortie :
Cahier, TasseUne petite fonction utilitaire joue le rôle du constructeur positionnel du record. Les derives vous donnent ce qu’un record C# offre gratuitement : l’affichage avec {:?}, la copie avec .clone() et la comparaison avec ==.
2. Porter un Dictionary avec des exceptions
Section intitulée « 2. Porter un Dictionary avec des exceptions »Ce code C# retire un article d’un dictionnaire de stock et lève une exception quand quelque chose ne va pas. Portez-le pour que take renvoie un Result<u32, String> au lieu de lever une exception. Indice : stock.get_mut(item) renvoie une Option<&mut u32>. Transformez-la en Result avec .ok_or(...), puis utilisez ?.
var stock = new Dictionary<string, int> { ["pommes"] = 3, ["poires"] = 0 };
int Take(string item){ if (!stock.TryGetValue(item, out var count)) throw new KeyNotFoundException($"aucun article nommé {item}"); if (count == 0) throw new InvalidOperationException($"{item} en rupture de stock"); stock[item] = count - 1; return count - 1;}
foreach (var item in new[] { "pommes", "poires", "kiwis" }){ try { Console.WriteLine($"{item} : une unité retirée, il en reste {Take(item)}"); } catch (Exception e) { Console.WriteLine($"erreur : {e.Message}"); }}Voir la solution
use std::collections::HashMap;
fn take(stock: &mut HashMap<String, u32>, item: &str) -> Result<u32, String> { let count = stock .get_mut(item) .ok_or(format!("aucun article nommé {item}"))?;
if *count == 0 { return Err(format!("{item} en rupture de stock")); }
*count -= 1; Ok(*count)}
fn main() { let mut stock = HashMap::new(); stock.insert("pommes".to_string(), 3); stock.insert("poires".to_string(), 0);
for item in ["pommes", "poires", "kiwis"] { match take(&mut stock, item) { Ok(left) => println!("{item} : une unité retirée, il en reste {left}"), Err(e) => println!("erreur : {e}"), } }}Sortie :
pommes : une unité retirée, il en reste 2erreur : poires en rupture de stockerreur : aucun article nommé kiwisTryGetValue avec un paramètre out devient get_mut, qui renvoie une Option. Les deux throw deviennent des valeurs Err, et le try/catch devient un match. Le &mut stock dans l’appel montre clairement que take modifie le dictionnaire.
Récapitulatif
Section intitulée « Récapitulatif »- Une grande partie de C# se retrouve en Rust : les génériques, async/await, les lambdas, le pattern matching et les itérateurs à la LINQ ont tous des équivalents proches.
- Les grands changements : l’ownership au lieu d’un ramasse-miettes,
Optionau lieu de null, etResult+?au lieu des exceptions. - Les classes deviennent une
struct+impl, les interfaces deviennent des traits, et l’héritage devient de la composition plus des traits.using/IDisposabledevient unDropautomatique. - Cargo remplace la CLI
dotnet, MSBuild et NuGet ; clippy et rustfmt remplacent les analyseurs etdotnet format; les tests sont intégrés. - Pour EF Core, regardez SeaORM ou Diesel ; pour Dapper, sqlx ; et pour ASP.NET Core, axum ou actix-web. Les options pour les interfaces graphiques, les jeux et le ML sont plus jeunes.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Qu'est-ce que Rust utilise à la place de null ?
2. Votre équipe utilise Dapper et aime écrire son SQL à la main. Quelle crate Rust s'en rapproche le plus ?
3. Quel est l'équivalent Rust le plus proche d'un bloc using avec IDisposable en C# ?
4. En C#, var b = a; avec une classe copie une référence. Que fait let b = a; avec une String en Rust ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Rust for C#/.NET Developers — Microsoft (en anglais) : le guide de Microsoft lui-même, avec beaucoup d’autres exemples C# et Rust côte à côte.
- The Rust Programming Language (en anglais) : le livre officiel. Les chapitres 4 (ownership), 9 (erreurs) et 10 (génériques et traits) sont les plus importants pour les développeurs C#.
- Getting Started with Diesel et la documentation de SeaORM (en anglais) : les deux principaux ORM, si vous cherchez l’équivalent d’Entity Framework.
- sqlx sur docs.rs (en anglais) : le choix à la Dapper, avec du SQL vérifié à la compilation.
- Are We Web Yet? et Are We GUI Yet? (en anglais) : des panoramas de la communauté sur les bibliothèques Rust pour le web et les interfaces graphiques de bureau.