Rust per sviluppatori C#
Se scrivi C# per lavoro, ho una buona notizia: molto di Rust ti sembrerà familiare. Rust ha generici con vincoli, async/await, un pattern matching potente, catene di iteratori che si leggono quasi come LINQ, e un ottimo strumento da riga di comando (Cargo) che fa quello che fa dotnet. Sai già ragionare per tipi, interfacce e lambda. Tutto questo ti torna utile.
Il grande cambiamento riguarda la memoria e gli errori. Rust non ha garbage collector. Ogni valore ha invece un solo proprietario, e il compilatore controlla l’ownership (possesso) e il prestito (borrowing) prima che il programma parta. Rust inoltre non ha null (usi Option) e non ha eccezioni (usi Result). È su questi tre cambiamenti che passerai la maggior parte del tempo di studio. Questa pagina mette in corrispondenza quello che conosci di C# e .NET con Rust, così puoi dedicare quel tempo alle parti davvero nuove.
Se invece cerchi un confronto generale con C++, Go e Python, guarda Rust vs C++, Go e Python.
Concetti del linguaggio: da C# a Rust
Sezione intitolata “Concetti del linguaggio: da C# a Rust”Usa questa tabella come un frasario. L’ultima colonna rimanda alla lezione di questo sito che spiega il lato Rust.
| C# | Rust | Per saperne di più |
|---|---|---|
class con campi e metodi |
struct per i dati + un blocco impl per i metodi |
Struct |
interface |
trait |
Trait |
| Classe astratta, ereditarietà | Niente ereditarietà. Usa la composizione (una struct dentro una struct) + trait con metodi predefiniti | Trait, Design pattern |
List<IShape> di classi diverse |
Vec<Box<dyn Shape>> (un trait object) |
Trait object |
| Metodi di estensione | Un tuo trait, implementato per un tipo che non hai scritto tu (come String) |
Trait |
Generici con where T : IComparable<T> |
Generici con vincoli di trait (trait bound): T: Ord o where T: Ord |
Generici |
null, Nullable<int>, tipi riferimento nullable |
Option<T>: o Some(valore) o None |
Option |
?? e ?. |
.unwrap_or(...), .map(...), .and_then(...) e ? |
Option |
Eccezioni, throw, try/catch |
Result<T, E> + l’operatore ? |
Result, Errori personalizzati |
Un’eccezione per un vero bug (come InvalidOperationException) |
panic! (ferma il thread) |
Panic |
IDisposable + using |
Drop, che viene eseguito da solo quando il proprietario esce dallo scope (RAII) |
Scope, Smart pointer |
| Garbage collector | Ownership e prestito, controllati in compilazione | Ownership, Prestito |
record |
struct con #[derive(Debug, Clone, PartialEq)] |
Struct, Trait comuni |
Espressione with su un record |
Sintassi di aggiornamento delle struct: Order { total: 5, ..old } |
Struct |
enum (solo interi con nome) |
enum in cui ogni variante può portare i propri dati |
Enum |
Espressioni switch, pattern is |
match (il compilatore controlla che tu abbia coperto ogni caso) |
match, Pattern matching |
Tuple (int, string) |
Tuple (i32, String) |
Tuple |
Parametri ref / out |
&mut T, oppure restituisci una tupla o un Option |
Prestito |
Lambda, Func<T>, Action<T>, delegate |
Closure, descritte dai trait Fn, FnMut, FnOnce |
Closure, Fn, FnMut e FnOnce |
event |
Un canale, o una lista di closure in un box (callback) | Canali, Fn, FnMut e FnOnce |
LINQ: Where, Select, Sum, OrderBy, ToList |
Adattatori di iteratori: filter, map, sum, sort_by (su un Vec), collect |
Iteratori, Ordinamento |
IEnumerable<T> e yield return |
impl Iterator<Item = T>, o un tuo tipo che implementa Iterator |
Iteratori personalizzati |
async Task<T> / await |
async fn restituisce un Future; per eseguirlo ti serve un runtime come tokio |
Async/await |
Task.Run, Thread, lock |
std::thread::spawn o tokio::spawn; Mutex<T> avvolge i dati che protegge |
Concorrenza, Stato condiviso |
Proprietà (get; set;) |
Niente proprietà. Usa campi pub, o metodi getter e setter |
Struct |
string e StringBuilder |
&str (una vista presa in prestito su un testo) e String (posseduta, può crescere) |
Stringhe |
Interpolazione di stringhe $"Hi {name}" |
format!("Hi {name}") e println!("Hi {name}") |
Output |
var |
let (immutabile per default; scrivi let mut per permettere modifiche) |
Variabili |
const, readonly |
const, e un semplice let (è già di sola lettura) |
Variabili |
Campo static readonly |
static, o LazyLock per valori costruiti all’avvio |
Variabili |
namespace e direttive using |
Moduli (mod) e use |
Moduli |
| Assembly / progetto | Crate (una libreria o un binario) | Cargo e crate |
Attributi come [Serializable] |
Attributi come #[derive(Serialize)] e macro derive |
Macro, JSON con Serde |
| Reflection | Quasi niente durante l’esecuzione. Al suo posto, macro e derive generano codice in compilazione |
Macro |
unsafe e puntatori |
Blocchi unsafe e puntatori grezzi |
Rust unsafe |
Span<T>, ReadOnlySpan<char> |
Slice: &[T] e &str |
Slice |
Override di ToString(), IFormattable |
Implementa Display (e .to_string() funziona gratis) |
Trait comuni |
IEquatable<T>, IComparable<T>, GetHashCode |
PartialEq/Eq, PartialOrd/Ord, Hash (di solito con derive) |
Trait comuni |
Operatori di conversione implicit / explicit |
From/Into (e TryFrom quando può fallire) |
Conversioni |
Overloading degli operatori (operator +) |
Implementa i trait di std::ops, come Add |
Trait avanzati |
Aritmetica checked / unchecked |
checked_add, wrapping_add, saturating_add |
Overflow degli interi |
decimal |
Nessun tipo decimale integrato; usa il crate rust_decimal, o salva i centesimi in un intero |
Tipi di dati |
Collezioni
Sezione intitolata “Collezioni”La libreria standard copre quasi tutto quello che c’è in System.Collections.Generic. Cambiano i nomi, non le idee.
| C# | Rust | Per saperne di più |
|---|---|---|
List<T> |
Vec<T> |
Vettori |
T[] (lunghezza fissa) |
[T; N] quando la lunghezza è nota in compilazione, altrimenti Vec<T> |
Array |
Dictionary<K, V> |
HashMap<K, V> |
HashMap |
SortedDictionary<K, V> |
BTreeMap<K, V> (le chiavi restano ordinate) |
HashMap |
HashSet<T> |
HashSet<T> |
HashSet |
SortedSet<T> |
BTreeSet<T> |
HashSet |
Queue<T> |
VecDeque<T> (push_back / pop_front) |
VecDeque e BinaryHeap |
Stack<T> |
Vec<T> con push / pop |
Vettori |
PriorityQueue<TElement, TPriority> |
BinaryHeap<T> (prima il più grande; avvolgi in Reverse per avere prima il più piccolo) |
VecDeque e BinaryHeap |
LinkedList<T> |
LinkedList<T> esiste, ma serve di rado; di solito VecDeque è più veloce |
VecDeque e BinaryHeap |
ConcurrentDictionary<K, V> |
Mutex<HashMap<K, V>>, o il crate dashmap |
Stato condiviso |
Una cosa a cui fare attenzione: come Dictionary, anche una HashMap di Rust non ha un ordine fisso. Se ne stampi il contenuto, ordinalo prima o usa una BTreeMap.
Strumenti: da dotnet a cargo
Sezione intitolata “Strumenti: da dotnet a cargo”Questa è la parte che piace subito alla maggior parte degli sviluppatori C#. Cargo fa il lavoro della CLI dotnet, di MSBuild e di NuGet in un solo strumento.
| .NET | Rust | Note |
|---|---|---|
CLI dotnet |
cargo |
Compila, esegue, testa, aggiunge pacchetti, pubblica. Vedi Cargo e crate |
.csproj |
Cargo.toml |
Breve e modificabile a mano |
packages.lock.json |
Cargo.lock |
Viene creato per te |
.sln con più progetti |
Un workspace Cargo | Un Cargo.toml in cima elenca i crate membri |
| NuGet / nuget.org | crates.io | La documentazione delle API di ogni crate pubblicato è su docs.rs (in inglese) |
dotnet add package Serilog |
cargo add tracing |
|
dotnet new console |
cargo new my_app (cargo new --lib per una libreria) |
|
dotnet run / build / test |
cargo run / cargo build / cargo test |
|
-c Release |
--release |
Le build di debug sono molto più lente in esecuzione; fai i benchmark in release |
dotnet watch |
cargo watch o bacon (si installano con cargo install) |
|
dotnet tool install -g |
cargo install |
|
global.json (versione dell’SDK) |
rustup e un file rust-toolchain.toml |
|
| Analizzatori Roslyn | cargo clippy |
Centinaia di lint utili, ufficiali |
dotnet format |
cargo fmt |
Un solo stile standard per tutti |
| Visual Studio / Rider | VS Code + rust-analyzer, o JetBrains RustRover | rust-analyzer offre completamento, vai-alla-definizione ed errori in linea |
Commenti di documentazione XML /// |
Commenti di documentazione /// in Markdown + cargo doc |
Gli esempi di codice nella documentazione vengono eseguiti come test. Vedi Documentazione |
| xUnit / NUnit / MSTest | #[test] integrato e cargo test (cargo nextest è un runner più veloce molto usato) |
Vedi Test |
| Moq | mockall, o finti oggetti scritti a mano che implementano il tuo trait |
|
| Coverlet (copertura del codice) | cargo llvm-cov o cargo tarpaulin |
|
| BenchmarkDotNet | criterion |
Vedi Prestazioni |
| dotnet-trace, profiler | cargo flamegraph, perf, o qualsiasi profiler nativo |
Ecco un piccolo Cargo.toml, così puoi confrontarlo con 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"] }Librerie e framework: «cosa usiamo al posto di…?»
Sezione intitolata “Librerie e framework: «cosa usiamo al posto di…?»”È la prima domanda che fa ogni team .NET. «Usiamo Entity Framework. Cosa c’è in Rust?» Ecco gli equivalenti più vicini. Dietro Rust non c’è una singola azienda con uno «stack predefinito», come Microsoft per .NET, quindi spesso scegli tra due o tre buoni crate della comunità.
Accesso ai dati
Sezione intitolata “Accesso ai dati”| .NET | Rust | Note |
|---|---|---|
| Entity Framework Core | Diesel: ORM e query builder; le tue query vengono controllate sui tipi in compilazione. Migrazioni con diesel_cli |
Maturo (2.x), sincrono per default (esiste diesel-async) |
| Entity Framework Core (async, stile a entità) | SeaORM: ORM asincrono con entità, relazioni e migrazioni (sea-orm-cli) |
È il più simile a EF; costruito sopra sqlx |
| Dapper | sqlx: scrivi SQL semplice, e la macro query! lo controlla sul tuo vero database in compilazione. Migrazioni con sqlx-cli |
Asincrono, molto popolare, non è un ORM |
| ADO.NET | sqlx, tokio-postgres, rusqlite (SQLite) |
Driver di livello più basso |
| StackExchange.Redis | redis |
Se ti piaceva EF, prova prima SeaORM. Se ti piaceva Dapper e scrivere il tuo SQL, prova sqlx. Se vuoi le garanzie più forti in compilazione, prova Diesel. Database in Rust li mostra tutti e tre con codice funzionante.
Web e rete
Sezione intitolata “Web e rete”| .NET | Rust | Note |
|---|---|---|
| ASP.NET Core Web API | axum (ne costruiamo una in Progetto: API web), actix-web, Rocket | axum e actix-web sono molto usati in produzione |
| Minimal API | axum: le rotte sono semplici handler async fn |
Lo stile è molto simile |
| Pipeline di middleware | Layer di tower e il crate tower-http (CORS, compressione, tracing, timeout) |
axum usa tower direttamente |
| Dependency injection integrata | State in axum, costruttori semplici e trait |
Vedi la riga sulla DI più sotto |
| Swashbuckle / OpenAPI | utoipa |
|
| SignalR | WebSocket in axum (la feature ws), o tokio-tungstenite |
Nessun livello di protocollo stile SignalR integrato |
| gRPC (Grpc.AspNetCore) | tonic (con prost per protobuf) |
|
HttpClient |
reqwest |
Asincrono per default, con un client bloccante opzionale |
| Autenticazione JWT bearer | jsonwebtoken |
I mattoni di tutti i giorni
Sezione intitolata “I mattoni di tutti i giorni”| .NET | Rust | Note |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde + serde_json |
Lo standard; gestisce anche TOML, YAML e altro. Vedi JSON con Serde |
ILogger / Serilog |
tracing (+ tracing-subscriber), o log + env_logger |
Logging strutturato con span. Vedi Logging |
IConfiguration / appsettings.json |
config o figment |
Combina file e variabili d’ambiente, poi deserializza con serde |
Container DI (IServiceCollection) |
Di solito niente: passa le dipendenze ai costruttori, usa trait per le cose che vuoi sostituire nei test | Esiste shaku, ma pochi progetti usano un container |
| AutoMapper | Scrivi implementazioni di From / Into |
Esplicito e controllato dal compilatore. Vedi Conversioni |
| FluentValidation / DataAnnotations | validator o garde (validazione basata su derive) |
|
| Polly (tentativi ripetuti) | backon, o il middleware di retry di tower |
|
| MediatR | Nessun equivalente diretto | Semplici trait, o canali tra task |
| Hangfire / job in background | tokio::spawn per task nel processo; apalis per code di job persistenti |
|
CancellationToken |
Elimina il future (drop), o usa CancellationToken di tokio-util |
Desktop, giochi e altro
Sezione intitolata “Desktop, giochi e altro”| .NET | Rust | Note |
|---|---|---|
| Blazor WebAssembly | Leptos, Yew, Dioxus |
Tutti prima della versione 1.0; le API cambiano ancora. Vedi WebAssembly |
| WPF / WinForms / MAUI | Tauri (interfaccia web + backend Rust), Slint, egui, Iced |
Tauri 2 e Slint 1.x sono i più stabili. Nessun toolkit grafico ufficiale |
| Unity | Bevy, o Rust dentro Godot con il crate godot |
Bevy è prima della 1.0 e rompe le sue API tra una release e l’altra |
| ML.NET | candle, burn, linfa |
Molto più giovani degli strumenti di ML di Python |
| Azure SDK per .NET | L’Azure SDK ufficiale per Rust: azure_core, azure_identity, azure_storage_blob, … |
Copre meno servizi dell’SDK .NET |
Fianco a fianco: una classe diventa struct + impl
Sezione intitolata “Fianco a fianco: una classe diventa struct + impl”Guardiamo del codice vero. Ecco un piccolo conto bancario in C#:
var account = new BankAccount("Ada", 100);account.Deposit(50);Console.WriteLine($"{account.Owner} ha {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;}E in Rust:
struct BankAccount { owner: String, balance: i64,}
impl BankAccount { // `new` è solo una convenzione, non una parola chiave. fn new(owner: &str, balance: i64) -> Self { BankAccount { owner: owner.to_string(), balance } }
// `&mut self`: questo metodo modifica il conto. fn deposit(&mut self, amount: i64) { self.balance += amount; }
// `&self`: questo metodo si limita a leggere. fn balance(&self) -> i64 { self.balance }}
fn main() { let mut account = BankAccount::new("Ada", 100); account.deposit(50); println!("{} ha {}", account.owner, account.balance());}Output:
Ada ha 150Dati e comportamento vivono in due blocchi separati. Non ci sono costruttori, solo funzioni che restituiscono Self. Ogni metodo dichiara subito se legge (&self) o modifica (&mut self) il valore. E siccome deposit modifica il conto, la variabile deve essere let mut. In C# qualsiasi metodo può modificare qualsiasi campo; in Rust le modifiche si vedono nella firma.
Fianco a fianco: un’interfaccia diventa un trait
Sezione intitolata “Fianco a fianco: un’interfaccia diventa un trait”In C# potresti avere un’interfaccia IShape con due classi e una 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;}In Rust l’interfaccia è un trait, e ogni struct lo implementa nel proprio blocco 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> fa la parte di «un riferimento a un IShape qualsiasi». 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()); }}Output:
3.146.00In C# ogni chiamata a un’interfaccia passa per una chiamata virtuale. In Rust scegli tu: Box<dyn Shape> usa una chiamata virtuale (vedi Trait object), mentre una funzione generica come fn print_area<T: Shape>(s: &T) viene compilata a parte per ogni tipo, senza nessuna chiamata virtuale. Un bel vantaggio in più: puoi implementare un tuo trait per tipi che non hai scritto tu, perfino String o i32. È così che Rust fa quello che in C# fanno i metodi di estensione.
Fianco a fianco: null diventa Option
Sezione intitolata “Fianco a fianco: null diventa Option”Il C# con i tipi riferimento nullable ti avvisa sui null, ma null esiste ancora durante l’esecuzione:
var email = FindEmail(2);if (email is not null) Console.WriteLine(email.ToUpper());else Console.WriteLine("nessuna email registrata");
Console.WriteLine(FindEmail(2) ?? "sconosciuta");In Rust un valore che potrebbe mancare ha il tipo Option<T>. Non puoi usarlo come un valore normale finché non hai gestito il caso 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!("nessuna email registrata"), }
// `if let` è la forma breve quando ti interessa un solo caso. if let Some(email) = find_email(2) { println!("trovata {email}"); } else { println!("nessuna email registrata"); }
// unwrap_or funziona come l'operatore ?? di C#. let email = find_email(2).unwrap_or("sconosciuta".to_string()); println!("{email}");}Output:
nessuna email registratasconosciutaIn Rust non esiste la NullReferenceException, perché non c’è nessun null da dereferenziare. Il compilatore semplicemente non ti lascia chiamare .to_uppercase() su un Option<String>. Impara i metodi di supporto nella pagina Option, e le forme brevi in if let.
Fianco a fianco: try/catch diventa Result e ?
Sezione intitolata “Fianco a fianco: try/catch diventa Result e ?”In C# il parsing può lanciare un’eccezione, e tu la catturi da qualche parte più in alto nello stack delle chiamate:
static int TotalSeats(string a, string b) => int.Parse(a) + int.Parse(b);
try{ Console.WriteLine(TotalSeats("12", "30")); Console.WriteLine(TotalSeats("12", "trenta"));}catch (FormatException e){ Console.WriteLine($"Errore: {e.Message}");}In Rust una funzione che può fallire lo dichiara nel tipo di ritorno. L’operatore ? significa «se questa cosa è fallita, restituisci subito l’errore a chi mi ha chiamato». Fa lo stesso lavoro di un’eccezione che risale, ma lo vedi nel codice:
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", "trenta")] { match total_seats(a, b) { Ok(total) => println!("Totale: {total}"), Err(e) => println!("Errore: {e}"), } }}Output:
Totale: 42Errore: invalid digit found in stringIl testo dell’errore, «invalid digit found in string» (cifra non valida nella stringa), arriva dalla libreria standard ed è sempre in inglese.
La grande differenza: in C# qualsiasi metodo potrebbe lanciare un’eccezione, e la firma non te lo dice. In Rust Result fa parte della firma, quindi chi chiama non può dimenticarsi di gestirlo. Per i tuoi tipi di errore, vedi Errori personalizzati. Nelle applicazioni, il crate anyhow ti dà qualcosa di simile a «cattura qualsiasi eccezione» (vedi anyhow e thiserror).
Fianco a fianco: LINQ diventa una catena di iteratori
Sezione intitolata “Fianco a fianco: LINQ diventa una catena di iteratori”Una tipica query LINQ:
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($"Incasso: {orders.Sum(o => o.Total)}");
record Order(string Customer, int Total);La versione Rust usa gli adattatori degli iteratori e le closure:
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(...): l'ordinamento si fa su un Vec, non sull'iteratore. 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!("Incasso: {revenue}");}Output:
Linus, Ken, AdaIncasso: 550Gli iteratori di Rust sono pigri (lazy), proprio come l’esecuzione differita di LINQ: non succede niente finché non chiami qualcosa come collect(), sum() o un ciclo for. collect() è il tuo ToList(). A differenza di LINQ, le catene di iteratori di solito vengono compilate nello stesso codice macchina di un ciclo scritto a mano, quindi non paghi niente per lo stile elegante. L’ordinamento è spiegato nella pagina Ordinamento e ricerca.
Fianco a fianco: using/IDisposable diventa Drop
Sezione intitolata “Fianco a fianco: using/IDisposable diventa Drop”In C# devi ricordarti di using per rilasciare una risorsa al momento giusto:
using (var conn = new Connection("orders-db")){ Console.WriteLine($"interrogo {conn.Name}");}Console.WriteLine("dopo il blocco");
class Connection : IDisposable{ public string Name { get; } public Connection(string name) => Name = name; public void Dispose() => Console.WriteLine($"chiudo la connessione a {Name}");}In Rust la pulizia è automatica. Quando il proprietario di un valore esce dallo scope, Rust chiama la sua implementazione di Drop. Non c’è niente da dimenticare:
struct Connection { name: String,}
impl Drop for Connection { fn drop(&mut self) { println!("chiudo la connessione a {}", self.name); }}
fn main() { { let conn = Connection { name: "orders-db".to_string() }; println!("interrogo {}", conn.name); } // qui `conn` esce dallo scope, quindi viene eseguito drop().
println!("dopo il blocco");}Output:
interrogo orders-dbchiudo la connessione a orders-dbdopo il bloccoSi chiama RAII, ed è il modo in cui Rust gestisce tutto: file, socket, lock e la memoria stessa. Un file si chiude quando il suo valore File viene eliminato. Un Mutex si sblocca quando la sua guardia viene eliminata. Non c’è nessun finalizzatore e nessuna pausa del GC; la pulizia avviene in un momento prevedibile.
Le cose che ti faranno inciampare se vieni da C#
Sezione intitolata “Le cose che ti faranno inciampare se vieni da C#”Chiunque arrivi da C# prende le stesse buche. Conoscerle in anticipo aiuta.
-
L’assegnamento sposta, non copia un riferimento. In C#,
var b = a;con una classe ti dà due riferimenti allo stesso oggetto. In Rust,let b = a;sposta l’ownership di unaStringo di unVecinb, eanon si può più usare:fn main() {let owner = String::from("Ada");let copy = owner;println!("{owner} and {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} and {copy}");| ^^^^^ value borrowed here after moveIl compilatore dice che
ownerè stato spostato incopye poi usato di nuovo. La soluzione è prendere in prestito (let copy = &owner;) o clonare di proposito (owner.clone()). Leggi prima Ownership: spiega questa regola. -
Il borrow checker. Puoi avere molti lettori (
&T) oppure un solo scrittore (&mut T) alla volta, mai entrambi. Schemi normali in C#, come oggetti che puntano al loro genitore o tanti oggetti che modificano un unico oggetto condiviso, richiedono smart pointer o un design diverso. Mettiti in conto qualche settimana frustrante. Poi diventa molto più facile. -
Stringcontro&str. C# ha un solo tipostring. Rust ne ha due:Stringpossiede il suo testo,&strlo prende in prestito. Di solito le funzioni accettano&stre le struct contengonoString. Vedi Stringhe. -
Niente ereditarietà. Non puoi scrivere una classe base con campi condivisi e metodi da ridefinire. Usa i trait (con metodi predefiniti) per il comportamento condiviso, e metti i dati condivisi in una struct contenuta nelle altre.
-
Niente eccezioni. Gli errori sono valori. Scriverai
?molto spesso, e anche il tuomainpuò restituireResult. Chiamare.unwrap()va bene mentre impari, ma è la versione Rust di «ignora l’eccezione e vai in crash». -
Niente null. All’inizio
Optiondappertutto sembra lavoro in più. Dopo un po’ ti accorgerai che non hai più bug da riferimento null. -
Clonazioni esplicite. Rust non copia mai un valore sull’heap alle tue spalle. Se vedi
.clone(), viene fatta una copia vera. È una caratteristica voluta: il costo è visibile. -
Tempi di compilazione. Una build pulita di un progetto con molti crate richiede più tempo di una build .NET. Le build incrementali e
cargo check(che salta la generazione del codice) sono molto più veloci; usacargo checkmentre scrivi. -
L’async ha bisogno di un runtime. In .NET il runtime e il pool di thread ci sono sempre. In Rust una
async fnnon fa niente finché un executor non la esegue, quindi aggiungi tokio e metti#[tokio::main]sumain. Vedi Async/await.
Un percorso di studio per sviluppatori C#
Sezione intitolata “Un percorso di studio per sviluppatori C#”Non devi leggere questo sito in ordine. Da sviluppatore C#, questo percorso salta quello che sai già e si sofferma dove Rust è diverso:
- Primi passi: installa Rust ed esegui
cargo new. - Variabili e Tipi di dati: scorrile velocemente, soprattutto per imparare
let mute i tipi numerici. - Ownership, poi Prestito, poi Slice: le pagine più importanti per te. Prenditi il tuo tempo.
- Stringhe:
Stringcontro&str. - Struct ed Enum, poi match e Pattern matching.
- Option e Result, poi Errori personalizzati.
- Trait, Generici, Trait comuni e Trait object.
- Closure e Iteratori: il tuo LINQ.
- Vettori, HashMap e VecDeque e BinaryHeap.
- Moduli, Cargo e crate e Test.
- Lifetime e Smart pointer.
- Concorrenza, Stato condiviso e Async/await.
- JSON con Serde e Logging, poi costruisci il progetto API web.
Pratica
Sezione intitolata “Pratica”Entrambi gli esercizi ti danno un frammento di C#. Portalo in Rust.
1. Porta un record e una query LINQ
Sezione intitolata “1. Porta un record e una query LINQ”Porta questo codice C#. Deve stampare i nomi dei prodotti disponibili che costano meno di 20. Suggerimento: crea una struct con #[derive(Debug, Clone, PartialEq)], poi usa filter, map e collect in un Vec<&str>. Vec<&str> ha un metodo .join(", ").
var products = new List<Product>{ new("Quaderno", 5, true), new("Cuffie", 45, true), new("Penna", 2, false), new("Tazza", 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);Mostra la soluzione
#[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("Quaderno", 5, true), product("Cuffie", 45, true), product("Penna", 2, false), product("Tazza", 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(", "));}Output:
Quaderno, TazzaUna piccola funzione di supporto fa la parte del costruttore posizionale del record. I derive ti danno quello che un record C# ti dà gratis: stampa con {:?}, copia con .clone() e confronto con ==.
2. Porta un Dictionary con le eccezioni
Sezione intitolata “2. Porta un Dictionary con le eccezioni”Questo codice C# toglie un articolo da un dizionario di scorte e lancia un’eccezione quando qualcosa non va. Portalo in modo che take restituisca un Result<u32, String> invece di lanciare eccezioni. Suggerimento: stock.get_mut(item) restituisce un Option<&mut u32>. Trasformalo in un Result con .ok_or(...), poi usa ?.
var stock = new Dictionary<string, int> { ["mele"] = 3, ["pere"] = 0 };
int Take(string item){ if (!stock.TryGetValue(item, out var count)) throw new KeyNotFoundException($"nessun articolo chiamato {item}"); if (count == 0) throw new InvalidOperationException($"{item}: scorte esaurite"); stock[item] = count - 1; return count - 1;}
foreach (var item in new[] { "mele", "pere", "kiwi" }){ try { Console.WriteLine($"{item}: preso 1, ne restano {Take(item)}"); } catch (Exception e) { Console.WriteLine($"errore: {e.Message}"); }}Mostra la soluzione
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!("nessun articolo chiamato {item}"))?;
if *count == 0 { return Err(format!("{item}: scorte esaurite")); }
*count -= 1; Ok(*count)}
fn main() { let mut stock = HashMap::new(); stock.insert("mele".to_string(), 3); stock.insert("pere".to_string(), 0);
for item in ["mele", "pere", "kiwi"] { match take(&mut stock, item) { Ok(left) => println!("{item}: preso 1, ne restano {left}"), Err(e) => println!("errore: {e}"), } }}Output:
mele: preso 1, ne restano 2errore: pere: scorte esauriteerrore: nessun articolo chiamato kiwiTryGetValue con un parametro out diventa get_mut, che restituisce un Option. I due throw diventano valori Err, e il try/catch diventa un match. Il &mut stock nella chiamata rende chiaro che take modifica il dizionario.
Riepilogo
Sezione intitolata “Riepilogo”- Molto di C# si porta dietro: generici, async/await, lambda, pattern matching e iteratori stile LINQ hanno tutti equivalenti vicini in Rust.
- I grandi cambiamenti sono l’ownership al posto del garbage collector,
Optional posto di null, eResult+?al posto delle eccezioni. - Le classi diventano
struct+impl, le interfacce diventano trait, e l’ereditarietà diventa composizione più trait.using/IDisposablediventa ilDropautomatico. - Cargo sostituisce la CLI
dotnet, MSBuild e NuGet; clippy e rustfmt sostituiscono gli analizzatori edotnet format; i test sono integrati. - Al posto di EF Core guarda SeaORM o Diesel, al posto di Dapper guarda sqlx, e al posto di ASP.NET Core guarda axum o actix-web. Le opzioni per interfacce grafiche, giochi e ML sono più giovani.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Cosa usa Rust al posto di null?
2. Il tuo team usa Dapper e ama scrivere SQL a mano. Quale crate Rust è il più simile?
3. Qual è l'equivalente Rust più vicino al blocco using di C# con IDisposable?
4. In C#, var b = a; con una classe copia un riferimento. Cosa fa let b = a; con una String in Rust?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Rust for C#/.NET Developers — Microsoft (in inglese): la guida di Microsoft, con molti altri esempi C# e Rust affiancati.
- The Rust Programming Language (in inglese): il libro ufficiale. I capitoli 4 (ownership), 9 (errori) e 10 (generici e trait) sono i più importanti per chi viene da C#.
- Getting Started with Diesel e SeaORM documentation (in inglese): i due ORM principali, se cerchi un sostituto di Entity Framework.
- sqlx su docs.rs (in inglese): la scelta stile Dapper, con SQL controllato in compilazione.
- Are We Web Yet? e Are We GUI Yet? (in inglese): panoramiche della comunità sulle librerie Rust per il web e per le interfacce grafiche desktop.