Rust für C#-Entwickler
Wenn du beruflich C# schreibst, gibt es eine gute Nachricht: Vieles an Rust wird dir bekannt vorkommen. Rust hat Generics mit Einschränkungen, async/await, mächtiges Pattern Matching, Iterator-Ketten, die sich ganz ähnlich lesen wie LINQ, und ein ausgezeichnetes Kommandozeilen-Tool (Cargo), das dieselben Aufgaben erledigt wie dotnet. Du weißt schon, wie man in Typen, Interfaces und Lambdas denkt. Dieses Wissen nimmst du mit.
Der große Unterschied betrifft Speicher und Fehler. Rust hat keinen Garbage Collector. Stattdessen hat jeder Wert genau einen Besitzer, und der Compiler prüft Ownership (Eigentum) und Borrowing (Ausleihen), bevor dein Programm läuft. Außerdem hat Rust kein null (du nutzt Option) und keine Exceptions (du nutzt Result). In diese drei Änderungen fließt der Großteil deiner Lernzeit. Diese Seite übersetzt das, was du aus C# und .NET kennst, in Rust. So kannst du deine Zeit in die Teile stecken, die wirklich neu sind.
Wenn du lieber einen allgemeinen Vergleich mit C++, Go und Python suchst, schau unter Rust vs. C++, Go und Python.
Sprachkonzepte: von C# zu Rust
Abschnitt betitelt „Sprachkonzepte: von C# zu Rust“Nutze diese Tabelle wie einen Sprachführer. Die letzte Spalte verlinkt die Lektion auf dieser Seite, die die Rust-Seite erklärt.
| C# | Rust | Mehr dazu |
|---|---|---|
class mit Feldern und Methoden |
struct für die Daten + ein impl-Block für die Methoden |
Structs |
interface |
trait |
Traits |
| Abstrakte Klasse, Vererbung | Keine Vererbung. Nutze Komposition (ein Struct in einem Struct) + Traits mit Standardmethoden | Traits, Entwurfsmuster |
List<IShape> mit verschiedenen Klassen |
Vec<Box<dyn Shape>> (ein Trait-Objekt) |
Trait-Objekte |
| Extension Methods | Ein eigener Trait, implementiert für einen Typ, den du nicht geschrieben hast (etwa String) |
Traits |
Generics mit where T : IComparable<T> |
Generics mit Trait Bounds: T: Ord oder where T: Ord |
Generics |
null, Nullable<int>, Nullable Reference Types |
Option<T>: entweder Some(value) oder None |
Option |
?? und ?. |
.unwrap_or(...), .map(...), .and_then(...) und ? |
Option |
Exceptions, throw, try/catch |
Result<T, E> + der Operator ? |
Result, Eigene Fehlertypen |
Eine Exception für einen echten Bug (etwa InvalidOperationException) |
panic! (beendet den Thread) |
Panic |
IDisposable + using |
Drop, das automatisch läuft, wenn der Besitzer seinen Gültigkeitsbereich verlässt (RAII) |
Gültigkeitsbereich, Smart Pointer |
| Garbage Collector | Ownership und Borrowing, geprüft beim Kompilieren | Ownership, Borrowing |
record |
struct mit #[derive(Debug, Clone, PartialEq)] |
Structs, Wichtige Traits |
with-Ausdruck auf einem Record |
Struct-Update-Syntax: Order { total: 5, ..old } |
Structs |
enum (nur benannte Ganzzahlen) |
enum, bei dem jede Variante eigene Daten tragen kann |
Enums |
switch-Ausdrücke, is-Patterns |
match (der Compiler prüft, ob du jeden Fall abgedeckt hast) |
Match, Pattern Matching |
Tupel (int, string) |
Tupel (i32, String) |
Tupel |
ref- / out-Parameter |
&mut T, oder ein Tupel bzw. eine Option zurückgeben |
Borrowing |
Lambdas, Func<T>, Action<T>, Delegates |
Closures, beschrieben durch die Traits Fn, FnMut, FnOnce |
Closures, Fn, FnMut und FnOnce |
event |
Ein Channel oder eine Liste geboxter Closures (Callbacks) | Channels, Fn, FnMut und FnOnce |
LINQ: Where, Select, Sum, OrderBy, ToList |
Iterator-Adapter: filter, map, sum, sort_by (auf einem Vec), collect |
Iteratoren, Sortieren |
IEnumerable<T> und yield return |
impl Iterator<Item = T> oder ein eigener Typ, der Iterator implementiert |
Eigene Iteratoren |
async Task<T> / await |
async fn gibt ein Future zurück; zum Ausführen brauchst du eine Runtime wie tokio |
Async/Await |
Task.Run, Thread, lock |
std::thread::spawn oder tokio::spawn; Mutex<T> umschließt die Daten, die er schützt |
Nebenläufigkeit, Geteilter Zustand |
Properties (get; set;) |
Keine Properties. Nutze pub-Felder oder Getter- und Setter-Methoden |
Structs |
string und StringBuilder |
&str (eine geliehene Sicht auf Text) und String (besessen, wachsend) |
Strings |
String-Interpolation $"Hi {name}" |
format!("Hi {name}") und println!("Hi {name}") |
Ausgabe |
var |
let (standardmäßig unveränderbar; mit let mut erlaubst du Änderungen) |
Variablen |
const, readonly |
const und einfaches let (ist schon schreibgeschützt) |
Variablen |
static readonly-Feld |
static, oder LazyLock für Werte, die beim Start erzeugt werden |
Variablen |
namespace und using-Direktiven |
Module (mod) und use |
Module |
| Assembly / Projekt | Crate (eine Bibliothek oder ein Programm) | Cargo und Crates |
Attribute wie [Serializable] |
Attribute wie #[derive(Serialize)] und Derive-Makros |
Makros, JSON mit Serde |
| Reflection | Zur Laufzeit praktisch keine. Stattdessen erzeugen Makros und derive Code beim Kompilieren |
Makros |
unsafe und Pointer |
unsafe-Blöcke und Raw Pointer |
Unsafe Rust |
Span<T>, ReadOnlySpan<char> |
Slices: &[T] und &str |
Slices |
ToString() überschreiben, IFormattable |
Display implementieren (dann funktioniert .to_string() gratis) |
Wichtige Traits |
IEquatable<T>, IComparable<T>, GetHashCode |
PartialEq/Eq, PartialOrd/Ord, Hash (meist per derive) |
Wichtige Traits |
implicit / explicit Konvertierungsoperatoren |
From/Into (und TryFrom, wenn es fehlschlagen kann) |
Konvertierungen |
Operatorüberladung (operator +) |
Traits aus std::ops implementieren, etwa Add |
Fortgeschrittene Traits |
checked / unchecked Arithmetik |
checked_add, wrapping_add, saturating_add |
Integer-Überlauf |
decimal |
Kein eingebauter Dezimaltyp; nutze das Crate rust_decimal oder speichere Cent als Ganzzahl |
Datentypen |
Collections
Abschnitt betitelt „Collections“Die Standardbibliothek deckt fast alles aus System.Collections.Generic ab. Die Namen ändern sich, die Ideen nicht.
| C# | Rust | Mehr dazu |
|---|---|---|
List<T> |
Vec<T> |
Vektoren |
T[] (feste Länge) |
[T; N], wenn die Länge beim Kompilieren bekannt ist, sonst Vec<T> |
Arrays |
Dictionary<K, V> |
HashMap<K, V> |
HashMap |
SortedDictionary<K, V> |
BTreeMap<K, V> (Schlüssel bleiben sortiert) |
HashMap |
HashSet<T> |
HashSet<T> |
HashSet |
SortedSet<T> |
BTreeSet<T> |
HashSet |
Queue<T> |
VecDeque<T> (push_back / pop_front) |
VecDeque und BinaryHeap |
Stack<T> |
Vec<T> mit push / pop |
Vektoren |
PriorityQueue<TElement, TPriority> |
BinaryHeap<T> (größtes Element zuerst; mit Reverse umschlossen das kleinste zuerst) |
VecDeque und BinaryHeap |
LinkedList<T> |
LinkedList<T> gibt es, du brauchst sie aber selten; VecDeque ist meist schneller |
VecDeque und BinaryHeap |
ConcurrentDictionary<K, V> |
Mutex<HashMap<K, V>> oder das Crate dashmap |
Geteilter Zustand |
Ein Punkt, auf den du achten solltest: Wie ein Dictionary hat auch eine HashMap in Rust keine feste Reihenfolge. Wenn du ihren Inhalt ausgibst, sortiere ihn vorher oder nimm eine BTreeMap.
Werkzeuge: von dotnet zu cargo
Abschnitt betitelt „Werkzeuge: von dotnet zu cargo“Das ist der Teil, über den sich die meisten C#-Entwickler sofort freuen. Cargo übernimmt die Aufgaben der dotnet CLI, von MSBuild und von NuGet in einem einzigen Tool.
| .NET | Rust | Hinweise |
|---|---|---|
dotnet CLI |
cargo |
Baut, startet, testet, fügt Pakete hinzu, veröffentlicht. Siehe Cargo und Crates |
.csproj |
Cargo.toml |
Kurz und gut von Hand zu bearbeiten |
packages.lock.json |
Cargo.lock |
Wird für dich erzeugt |
.sln mit mehreren Projekten |
Ein Cargo-Workspace | Eine Cargo.toml ganz oben listet die zugehörigen Crates auf |
| NuGet / nuget.org | crates.io | Die API-Doku jedes veröffentlichten Crates steht auf docs.rs |
dotnet add package Serilog |
cargo add tracing |
|
dotnet new console |
cargo new my_app (cargo new --lib für eine Bibliothek) |
|
dotnet run / build / test |
cargo run / cargo build / cargo test |
|
-c Release |
--release |
Debug-Builds laufen deutlich langsamer; Benchmarks immer im Release-Modus |
dotnet watch |
cargo watch oder bacon (installiert mit cargo install) |
|
dotnet tool install -g |
cargo install |
|
global.json (SDK-Version) |
rustup und eine Datei rust-toolchain.toml |
|
| Roslyn-Analyzer | cargo clippy |
Hunderte hilfreiche Lints, offiziell |
dotnet format |
cargo fmt |
Ein einheitlicher Stil für alle |
| Visual Studio / Rider | VS Code + rust-analyzer oder JetBrains RustRover | rust-analyzer bietet Vervollständigung, Sprung zur Definition und Fehler direkt im Code |
XML-Doc-Kommentare /// |
///-Doc-Kommentare in Markdown + cargo doc |
Code-Beispiele in der Doku laufen als Tests. Siehe Dokumentation |
| xUnit / NUnit / MSTest | Eingebautes #[test] und cargo test (cargo nextest ist ein beliebter, schnellerer Runner) |
Siehe Testen |
| Moq | mockall oder handgeschriebene Fakes, die deinen Trait implementieren |
|
| Coverlet (Code Coverage) | cargo llvm-cov oder cargo tarpaulin |
|
| BenchmarkDotNet | criterion |
Siehe Performance |
| dotnet-trace, Profiler | cargo flamegraph, perf oder ein beliebiger nativer Profiler |
Hier ist eine kleine Cargo.toml, damit du sie mit einer .csproj vergleichen kannst:
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }Bibliotheken und Frameworks: „Was nehmen wir statt …?“
Abschnitt betitelt „Bibliotheken und Frameworks: „Was nehmen wir statt …?““Das ist die Frage, die jedes .NET-Team zuerst stellt: „Wir nutzen Entity Framework. Was gibt es dafür in Rust?“ Hier sind die nächsten Entsprechungen. Hinter Rust steht keine einzelne Firma mit einem „Standard-Stack“, so wie Microsoft hinter .NET. Deshalb wählst du oft zwischen zwei oder drei guten Crates aus der Community.
Datenzugriff
Abschnitt betitelt „Datenzugriff“| .NET | Rust | Hinweise |
|---|---|---|
| Entity Framework Core | Diesel: ORM und Query Builder; deine Abfragen werden beim Kompilieren typgeprüft. Migrationen mit diesel_cli |
Ausgereift (2.x), standardmäßig synchron (diesel-async gibt es) |
| Entity Framework Core (async, mit Entities) | SeaORM: asynchrones ORM mit Entities, Relationen und Migrationen (sea-orm-cli) |
Fühlt sich am ehesten wie EF an; baut auf sqlx auf |
| Dapper | sqlx: Du schreibst einfaches SQL, und das Makro query! prüft es beim Kompilieren gegen deine echte Datenbank. Migrationen mit sqlx-cli |
Async, sehr beliebt, kein ORM |
| ADO.NET | sqlx, tokio-postgres, rusqlite (SQLite) |
Treiber auf niedrigerer Ebene |
| StackExchange.Redis | redis |
Wenn du EF mochtest, probier zuerst SeaORM. Wenn du Dapper mochtest und dein SQL gern selbst schreibst, probier sqlx. Wenn du die stärksten Garantien beim Kompilieren willst, probier Diesel. Datenbanken in Rust zeigt alle drei mit funktionierendem Code.
Web und Netzwerk
Abschnitt betitelt „Web und Netzwerk“| .NET | Rust | Hinweise |
|---|---|---|
| ASP.NET Core Web API | axum (wir bauen eine in Projekt: Web-API mit Axum), actix-web, Rocket | axum und actix-web sind in Produktion weit verbreitet |
| Minimal APIs | axum: Routen sind einfache async fn-Handler |
Der Stil ist sehr ähnlich |
| Middleware-Pipeline | tower-Layer und das Crate tower-http (CORS, Kompression, Tracing, Timeouts) |
axum nutzt tower direkt |
| Eingebaute Dependency Injection | State in axum, einfache Konstruktorfunktionen und Traits |
Siehe die DI-Zeile weiter unten |
| Swashbuckle / OpenAPI | utoipa |
|
| SignalR | WebSockets in axum (das Feature ws) oder tokio-tungstenite |
Keine eingebaute Protokollschicht wie bei SignalR |
| gRPC (Grpc.AspNetCore) | tonic (mit prost für Protobuf) |
|
HttpClient |
reqwest |
Standardmäßig async, mit optionalem blockierendem Client |
| JWT-Bearer-Authentifizierung | jsonwebtoken |
Bausteine für den Alltag
Abschnitt betitelt „Bausteine für den Alltag“| .NET | Rust | Hinweise |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde + serde_json |
Der Standard; kann auch TOML, YAML und mehr. Siehe JSON mit Serde |
ILogger / Serilog |
tracing (+ tracing-subscriber) oder log + env_logger |
Strukturiertes Logging mit Spans. Siehe Logging |
IConfiguration / appsettings.json |
config oder figment |
Dateien und Umgebungsvariablen übereinanderlegen, dann mit serde deserialisieren |
DI-Container (IServiceCollection) |
Meist gar nichts: Abhängigkeiten an Konstruktorfunktionen übergeben, Traits für alles, was du in Tests austauschen willst | shaku gibt es, aber nur wenige Projekte nutzen einen Container |
| AutoMapper | From- / Into-Implementierungen schreiben |
Explizit und vom Compiler geprüft. Siehe Konvertierungen |
| FluentValidation / DataAnnotations | validator oder garde (Validierung per derive) |
|
| Polly (Wiederholungen) | backon oder die Retry-Middleware von tower |
|
| MediatR | Keine direkte Entsprechung | Einfache Traits oder Channels zwischen Tasks |
| Hangfire / Hintergrundjobs | tokio::spawn für Tasks im selben Prozess; apalis für persistente Job-Queues |
|
CancellationToken |
Das Future droppen oder CancellationToken aus tokio-util nutzen |
Desktop, Spiele und mehr
Abschnitt betitelt „Desktop, Spiele und mehr“| .NET | Rust | Hinweise |
|---|---|---|
| Blazor WebAssembly | Leptos, Yew, Dioxus |
Alle noch vor Version 1.0; APIs ändern sich noch. Siehe WebAssembly |
| WPF / WinForms / MAUI | Tauri (Web-UI + Rust-Backend), Slint, egui, Iced |
Tauri 2 und Slint 1.x sind am stabilsten. Kein offizielles GUI-Toolkit |
| Unity | Bevy oder Rust in Godot mit dem Crate godot |
Bevy ist noch vor 1.0 und ändert seine API zwischen Releases |
| ML.NET | candle, burn, linfa |
Deutlich jünger als die ML-Tools von Python |
| Azure SDK for .NET | Das offizielle Azure SDK for Rust: azure_core, azure_identity, azure_storage_blob, … |
Deckt weniger Dienste ab als das .NET SDK |
Im direkten Vergleich: aus einer Klasse wird struct + impl
Abschnitt betitelt „Im direkten Vergleich: aus einer Klasse wird struct + impl“Schauen wir uns echten Code an. Hier ist ein kleines Bankkonto in C#:
var account = new BankAccount("Ada", 100);account.Deposit(50);Console.WriteLine($"{account.Owner} hat {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;}Und in Rust:
struct BankAccount { owner: String, balance: i64,}
impl BankAccount { // `new` ist nur eine Konvention, kein Schlüsselwort. fn new(owner: &str, balance: i64) -> Self { BankAccount { owner: owner.to_string(), balance } }
// `&mut self`: Diese Methode verändert das Konto. fn deposit(&mut self, amount: i64) { self.balance += amount; }
// `&self`: Diese Methode liest nur. fn balance(&self) -> i64 { self.balance }}
fn main() { let mut account = BankAccount::new("Ada", 100); account.deposit(50); println!("{} hat {}", account.owner, account.balance());}Ausgabe:
Ada hat 150Daten und Verhalten stehen in zwei getrennten Blöcken. Es gibt keine Konstruktoren, nur Funktionen, die Self zurückgeben. Jede Methode sagt gleich zu Beginn, ob sie den Wert nur liest (&self) oder verändert (&mut self). Und weil deposit das Konto verändert, muss die Variable mit let mut angelegt sein. In C# kann jede Methode jedes Feld ändern. In Rust siehst du Änderungen schon an der Signatur.
Im direkten Vergleich: aus einem Interface wird ein Trait
Abschnitt betitelt „Im direkten Vergleich: aus einem Interface wird ein Trait“In C# hast du vielleicht ein Interface IShape mit zwei Klassen und eine 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 wird das Interface zu einem Trait, und jedes Struct implementiert ihn in einem eigenen impl-Block:
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> spielt die Rolle von „eine Referenz auf irgendein IShape“. 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()); }}Ausgabe:
3.146.00In C# läuft jeder Aufruf über ein Interface als virtueller Aufruf. In Rust hast du die Wahl: Box<dyn Shape> nutzt einen virtuellen Aufruf (siehe Trait-Objekte). Eine generische Funktion wie fn print_area<T: Shape>(s: &T) wird dagegen für jeden Typ einzeln kompiliert, ganz ohne virtuellen Aufruf. Ein schöner Bonus: Du kannst deinen eigenen Trait auch für Typen implementieren, die du nicht geschrieben hast, sogar für String oder i32. So erreicht Rust das, wofür es in C# Extension Methods gibt.
Im direkten Vergleich: aus null wird Option
Abschnitt betitelt „Im direkten Vergleich: aus null wird Option“C# mit Nullable Reference Types warnt dich vor null, aber zur Laufzeit gibt es null trotzdem noch:
var email = FindEmail(2);if (email is not null) Console.WriteLine(email.ToUpper());else Console.WriteLine("keine E-Mail hinterlegt");
Console.WriteLine(FindEmail(2) ?? "unbekannt");In Rust hat ein Wert, der fehlen könnte, den Typ Option<T>. Du kannst ihn erst wie einen normalen Wert benutzen, wenn du den Fall None behandelt hast:
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!("keine E-Mail hinterlegt"), }
// `if let` ist die Kurzform, wenn dich nur ein Fall interessiert. if let Some(email) = find_email(2) { println!("gefunden: {email}"); } else { println!("keine E-Mail hinterlegt"); }
// unwrap_or funktioniert wie der Operator ?? in C#. let email = find_email(2).unwrap_or("unbekannt".to_string()); println!("{email}");}Ausgabe:
keine E-Mail hinterlegtunbekanntIn Rust gibt es keine NullReferenceException, weil es kein null gibt, das man dereferenzieren könnte. Der Compiler lässt dich .to_uppercase() einfach nicht auf einer Option<String> aufrufen. Die Hilfsmethoden lernst du auf der Seite Option kennen, die Kurzformen unter If Let / While Let.
Im direkten Vergleich: aus try/catch wird Result und ?
Abschnitt betitelt „Im direkten Vergleich: aus try/catch wird Result und ?“In C# kann das Parsen eine Exception werfen, und du fängst sie irgendwo weiter oben im Call Stack ab:
static int TotalSeats(string a, string b) => int.Parse(a) + int.Parse(b);
try{ Console.WriteLine(TotalSeats("12", "30")); Console.WriteLine(TotalSeats("12", "dreißig"));}catch (FormatException e){ Console.WriteLine($"Fehler: {e.Message}");}In Rust sagt eine Funktion, die fehlschlagen kann, das in ihrem Rückgabetyp. Der Operator ? bedeutet: „Wenn das fehlgeschlagen ist, gib den Fehler sofort an meinen Aufrufer zurück.“ Er erledigt dieselbe Aufgabe, wie eine Exception nach oben durchzureichen – nur siehst du es im 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", "dreißig")] { match total_seats(a, b) { Ok(total) => println!("Gesamt: {total}"), Err(e) => println!("Fehler: {e}"), } }}Ausgabe:
Gesamt: 42Fehler: invalid digit found in stringDie Meldung „invalid digit found in string“ kommt von Rust selbst und ist deshalb auf Englisch. Sie bedeutet: „ungültige Ziffer im String gefunden“.
Der große Unterschied: In C# kann jede Methode eine Exception werfen, und die Signatur verrät es dir nicht. In Rust ist Result Teil der Signatur. Aufrufer können also nicht vergessen, den Fehler zu behandeln. Für eigene Fehlertypen siehe Eigene Fehlertypen. In Anwendungen gibt dir das Crate anyhow etwas, das „fang jede Exception“ sehr nahe kommt (siehe anyhow und thiserror).
Im direkten Vergleich: aus LINQ wird eine Iterator-Kette
Abschnitt betitelt „Im direkten Vergleich: aus LINQ wird eine Iterator-Kette“Eine typische LINQ-Abfrage:
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($"Umsatz: {orders.Sum(o => o.Total)}");
record Order(string Customer, int Total);Die Rust-Version nutzt Iterator-Adapter und 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(...): Sortiert wird auf einem Vec, nicht auf dem Iterator. 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!("Umsatz: {revenue}");}Ausgabe:
Linus, Ken, AdaUmsatz: 550Iteratoren in Rust sind lazy (sie arbeiten erst bei Bedarf), genau wie die verzögerte Ausführung bei LINQ: Nichts passiert, bis du etwas wie collect(), sum() oder eine for-Schleife aufrufst. collect() ist dein ToList(). Anders als bei LINQ werden Iterator-Ketten meist zu demselben Maschinencode kompiliert wie eine handgeschriebene Schleife. Für den schönen Stil zahlst du also nichts. Mehr zum Sortieren steht auf der Seite Sortieren und Suchen.
Im direkten Vergleich: aus using/IDisposable wird Drop
Abschnitt betitelt „Im direkten Vergleich: aus using/IDisposable wird Drop“In C# musst du an using denken, damit eine Ressource rechtzeitig freigegeben wird:
using (var conn = new Connection("orders-db")){ Console.WriteLine($"Abfrage an {conn.Name}");}Console.WriteLine("nach dem Block");
class Connection : IDisposable{ public string Name { get; } public Connection(string name) => Name = name; public void Dispose() => Console.WriteLine($"Verbindung zu {Name} wird geschlossen");}In Rust passiert das Aufräumen automatisch. Wenn der Besitzer eines Werts seinen Gültigkeitsbereich (Scope) verlässt, ruft Rust dessen Drop-Implementierung auf. Da gibt es nichts zu vergessen:
struct Connection { name: String,}
impl Drop for Connection { fn drop(&mut self) { println!("Verbindung zu {} wird geschlossen", self.name); }}
fn main() { { let conn = Connection { name: "orders-db".to_string() }; println!("Abfrage an {}", conn.name); } // Hier verlässt `conn` seinen Gültigkeitsbereich, also läuft drop().
println!("nach dem Block");}Ausgabe:
Abfrage an orders-dbVerbindung zu orders-db wird geschlossennach dem BlockDas nennt sich RAII, und so geht Rust mit allem um: Dateien, Sockets, Locks und sogar dem Speicher selbst. Eine Datei wird geschlossen, wenn ihr File-Wert gedroppt wird. Ein Mutex wird entsperrt, wenn sein Guard gedroppt wird. Es gibt keinen Finalizer und keine GC-Pause; aufgeräumt wird zu einem vorhersehbaren Zeitpunkt.
Stolpersteine für C#-Entwickler
Abschnitt betitelt „Stolpersteine für C#-Entwickler“Alle, die von C# kommen, stolpern über dieselben Dinge. Wenn du sie vorher kennst, hilft das.
-
Eine Zuweisung verschiebt den Wert, statt eine Referenz zu kopieren. In C# bekommst du mit
var b = a;bei einer Klasse zwei Referenzen auf ein Objekt. In Rust verschiebtlet b = a;das Ownership an einemStringoderVecnachb(ein Move), undaist danach nicht mehr benutzbar:fn main() {let owner = String::from("Ada");let copy = owner;println!("{owner} und {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} und {copy}");| ^^^^^ value borrowed here after moveDer Compiler sagt auf Englisch:
ownerwird ausgeliehen, nachdem sein Wert schon verschoben wurde. Die Lösung ist, auszuleihen (let copy = &owner;) oder bewusst zu klonen (owner.clone()). Lies zuerst Ownership; dort wird diese Regel erklärt. -
Der Borrow Checker. Du kannst viele Leser (
&T) oder genau einen Schreiber (&mut T) gleichzeitig haben, nie beides. Muster, die in C# ganz normal sind, etwa Objekte, die auf ihr Elternobjekt zurückzeigen, oder viele Objekte, die ein gemeinsames Objekt verändern, brauchen Smart Pointer oder ein anderes Design. Rechne mit ein paar frustrierenden Wochen. Danach wird es viel leichter. -
Stringvs.&str. C# hat einen einzigenstring-Typ. Rust hat zwei:Stringbesitzt seinen Text,&strleiht ihn aus. Funktionen nehmen meist&strentgegen, und Structs speichern meistString. Siehe Strings. -
Keine Vererbung. Du kannst keine Basisklasse mit gemeinsamen Feldern schreiben und Methoden überschreiben. Nutze Traits (mit Standardmethoden) für gemeinsames Verhalten, und pack gemeinsame Daten in ein Struct, das andere Structs enthalten.
-
Keine Exceptions. Fehler sind Werte. Du wirst oft
?schreiben, und auch deinmainkann einResultzurückgeben..unwrap()aufzurufen ist beim Lernen in Ordnung. Es ist aber die Rust-Version von „Exception ignorieren und abstürzen“. -
Kein null. Am Anfang fühlt sich
Optionüberall nach Mehrarbeit an. Nach einer Weile merkst du, dass du keine Null-Reference-Bugs mehr bekommst. -
Explizites Klonen. Rust kopiert nie heimlich einen Wert auf dem Heap. Wenn du
.clone()siehst, wird wirklich eine Kopie angelegt. Das ist ein Feature: Die Kosten sind sichtbar. -
Kompilierzeiten. Ein kompletter Build eines Projekts mit vielen Crates dauert länger als ein .NET-Build. Inkrementelle Builds und
cargo check(das die Codeerzeugung überspringt) sind viel schneller. Nutze beim Bearbeitencargo check. -
Async braucht eine Runtime. In .NET sind Runtime und Thread-Pool immer da. In Rust tut eine
async fnnichts, bis ein Executor sie ausführt. Deshalb fügst du tokio hinzu und schreibst#[tokio::main]übermain. Siehe Async/Await.
Ein Lernpfad für C#-Entwickler
Abschnitt betitelt „Ein Lernpfad für C#-Entwickler“Du musst diese Seite nicht der Reihe nach lesen. Als C#-Entwickler überspringst du auf diesem Weg, was du schon kannst, und nimmst dir Zeit für das, was in Rust anders ist:
- Erste Schritte mit Rust: Rust installieren und
cargo newausführen. - Variablen und Datentypen: nur überfliegen, vor allem für
let mutund die Zahlentypen. - Ownership, dann Borrowing, dann Slices: die wichtigsten Seiten für dich. Lass dir Zeit.
- Strings:
Stringvs.&str. - Structs und Enums, dann Match und Pattern Matching.
- Option und Result, dann Eigene Fehlertypen.
- Traits, Generics, Wichtige Traits und Trait-Objekte.
- Closures und Iteratoren: dein LINQ.
- Vektoren, HashMap und VecDeque und BinaryHeap.
- Module, Cargo und Crates und Testen.
- Lifetimes und Smart Pointer.
- Nebenläufigkeit, Geteilter Zustand und Async/Await.
- JSON mit Serde und Logging, danach baust du das Web-API-Projekt.
Übungen
Abschnitt betitelt „Übungen“Beide Übungen geben dir ein Stück C#-Code. Portiere es nach Rust.
1. Einen Record und eine LINQ-Abfrage portieren
Abschnitt betitelt „1. Einen Record und eine LINQ-Abfrage portieren“Portiere diesen C#-Code. Er soll die Namen der Produkte ausgeben, die vorrätig sind und weniger als 20 kosten. Tipp: Leg ein struct mit #[derive(Debug, Clone, PartialEq)] an und nutze dann filter, map und collect in einen Vec<&str>. Vec<&str> hat eine Methode .join(", ").
var products = new List<Product>{ new("Notizbuch", 5, true), new("Kopfhörer", 45, true), new("Stift", 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);Lösung anzeigen
#[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("Notizbuch", 5, true), product("Kopfhörer", 45, true), product("Stift", 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(", "));}Ausgabe:
Notizbuch, TasseEine kleine Hilfsfunktion übernimmt die Rolle des positionellen Konstruktors des Records. Die Derives geben dir das, was ein C#-Record gratis mitbringt: Ausgabe mit {:?}, Kopieren mit .clone() und Vergleichen mit ==.
2. Ein Dictionary mit Exceptions portieren
Abschnitt betitelt „2. Ein Dictionary mit Exceptions portieren“Dieser C#-Code nimmt einen Artikel aus einem Lager-Dictionary und wirft eine Exception, wenn etwas nicht stimmt. Portiere ihn so, dass take ein Result<u32, String> zurückgibt, statt eine Exception zu werfen. Tipp: stock.get_mut(item) gibt eine Option<&mut u32> zurück. Mach daraus mit .ok_or(...) ein Result und nutze dann ?.
var stock = new Dictionary<string, int> { ["Äpfel"] = 3, ["Birnen"] = 0 };
int Take(string item){ if (!stock.TryGetValue(item, out var count)) throw new KeyNotFoundException($"kein Artikel namens {item}"); if (count == 0) throw new InvalidOperationException($"keine {item} mehr auf Lager"); stock[item] = count - 1; return count - 1;}
foreach (var item in new[] { "Äpfel", "Birnen", "Kiwis" }){ try { Console.WriteLine($"{item}: 1 entnommen, noch {Take(item)} übrig"); } catch (Exception e) { Console.WriteLine($"Fehler: {e.Message}"); }}Lösung anzeigen
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!("kein Artikel namens {item}"))?;
if *count == 0 { return Err(format!("keine {item} mehr auf Lager")); }
*count -= 1; Ok(*count)}
fn main() { let mut stock = HashMap::new(); stock.insert("Äpfel".to_string(), 3); stock.insert("Birnen".to_string(), 0);
for item in ["Äpfel", "Birnen", "Kiwis"] { match take(&mut stock, item) { Ok(left) => println!("{item}: 1 entnommen, noch {left} übrig"), Err(e) => println!("Fehler: {e}"), } }}Ausgabe:
Äpfel: 1 entnommen, noch 2 übrigFehler: keine Birnen mehr auf LagerFehler: kein Artikel namens KiwisAus TryGetValue mit einem out-Parameter wird get_mut, das eine Option zurückgibt. Die beiden throws werden zu Err-Werten, und aus try/catch wird ein match. Das &mut stock im Aufruf zeigt klar, dass take das Dictionary verändert.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Vieles aus C# nimmst du mit: Generics, async/await, Lambdas, Pattern Matching und Iteratoren im LINQ-Stil haben alle nahe Entsprechungen in Rust.
- Die großen Umstellungen sind Ownership statt Garbage Collector,
Optionstatt null undResult+?statt Exceptions. - Aus Klassen werden
struct+impl, aus Interfaces Traits, und aus Vererbung wird Komposition plus Traits. Aususing/IDisposablewird das automatischeDrop. - Cargo ersetzt die
dotnetCLI, MSBuild und NuGet; clippy und rustfmt ersetzen Analyzer unddotnet format; Tests sind eingebaut. - Statt EF Core schau dir SeaORM oder Diesel an, statt Dapper sqlx und statt ASP.NET Core axum oder actix-web. Die Optionen für GUI, Spiele und ML sind jünger.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was nutzt Rust statt null?
2. Dein Team nutzt Dapper und schreibt SQL gern von Hand. Welches Rust-Crate passt am besten?
3. Was ist in Rust die nächste Entsprechung zum using-Block mit IDisposable aus C#?
4. In C# kopiert var b = a; bei einer Klasse eine Referenz. Was macht let b = a; mit einem String in Rust?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Rust for C#/.NET Developers – Microsoft (auf Englisch): Microsofts eigener Leitfaden, mit vielen weiteren Beispielen in C# und Rust nebeneinander.
- Die Programmiersprache Rust – Das Rust-Buch: die deutsche Übersetzung des offiziellen Buchs. Für C#-Entwickler sind die Kapitel 4 (Ownership), 9 (Fehlerbehandlung) und 10 (Generics und Traits) am wichtigsten.
- Getting Started with Diesel und SeaORM-Dokumentation (beide auf Englisch): die zwei wichtigsten ORMs, falls du etwas wie Entity Framework suchst.
- sqlx auf docs.rs (auf Englisch): die Wahl im Stil von Dapper, mit SQL, das beim Kompilieren geprüft wird.
- Are We Web Yet? und Are We GUI Yet? (beide auf Englisch): Übersichten aus der Community zu den Web- und Desktop-GUI-Bibliotheken von Rust.