Zum Inhalt springen

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.

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

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.

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.

.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.

.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
.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
.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 150
▶ Selbst ausprobieren

Daten 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.14
6.00
▶ Selbst ausprobieren

In 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.

C# mit Nullable Reference Types warnt dich vor null, aber zur Laufzeit gibt es null trotzdem noch:

string? FindEmail(int userId) => userId == 1 ? "[email protected]" : null;
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 {
Some("[email protected]".to_string())
} 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 hinterlegt
unbekannt
▶ Selbst ausprobieren

In 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: 42
Fehler: invalid digit found in string
▶ Selbst ausprobieren

Die 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, Ada
Umsatz: 550
▶ Selbst ausprobieren

Iteratoren 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-db
Verbindung zu orders-db wird geschlossen
nach dem Block
▶ Selbst ausprobieren

Das 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.

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 verschiebt let b = a; das Ownership an einem String oder Vec nach b (ein Move), und a ist 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` trait
    3 | let copy = owner;
    | ----- value moved here
    4 | println!("{owner} und {copy}");
    | ^^^^^ value borrowed here after move

    Der Compiler sagt auf Englisch: owner wird 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.

  • String vs. &str. C# hat einen einzigen string-Typ. Rust hat zwei: String besitzt seinen Text, &str leiht ihn aus. Funktionen nehmen meist &str entgegen, und Structs speichern meist String. 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 dein main kann ein Result zurü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 Bearbeiten cargo check.

  • Async braucht eine Runtime. In .NET sind Runtime und Thread-Pool immer da. In Rust tut eine async fn nichts, bis ein Executor sie ausführt. Deshalb fügst du tokio hinzu und schreibst #[tokio::main] über main. Siehe Async/Await.

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:

  1. Erste Schritte mit Rust: Rust installieren und cargo new ausführen.
  2. Variablen und Datentypen: nur überfliegen, vor allem für let mut und die Zahlentypen.
  3. Ownership, dann Borrowing, dann Slices: die wichtigsten Seiten für dich. Lass dir Zeit.
  4. Strings: String vs. &str.
  5. Structs und Enums, dann Match und Pattern Matching.
  6. Option und Result, dann Eigene Fehlertypen.
  7. Traits, Generics, Wichtige Traits und Trait-Objekte.
  8. Closures und Iteratoren: dein LINQ.
  9. Vektoren, HashMap und VecDeque und BinaryHeap.
  10. Module, Cargo und Crates und Testen.
  11. Lifetimes und Smart Pointer.
  12. Nebenläufigkeit, Geteilter Zustand und Async/Await.
  13. JSON mit Serde und Logging, danach baust du das Web-API-Projekt.

Beide Übungen geben dir ein Stück C#-Code. Portiere es nach Rust.

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, Tasse
▶ Selbst ausprobieren

Eine 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 ==.

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 übrig
Fehler: keine Birnen mehr auf Lager
Fehler: kein Artikel namens Kiwis
▶ Selbst ausprobieren

Aus 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.

  • 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, Option statt null und Result + ? statt Exceptions.
  • Aus Klassen werden struct + impl, aus Interfaces Traits, und aus Vererbung wird Komposition plus Traits. Aus using/IDisposable wird das automatische Drop.
  • Cargo ersetzt die dotnet CLI, MSBuild und NuGet; clippy und rustfmt ersetzen Analyzer und dotnet 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

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