86 KiB
Rust Training for C# Programmers
A comprehensive guide to learning Rust for developers with C# experience, focusing on the conceptual shifts and practical differences between the two languages.
Table of Contents
1. Introduction and Philosophy
2. Type System Differences
- Null Safety: Nullable vs Option
- Value Types vs Reference Types vs Ownership
- Algebraic Data Types vs C# Unions
- Exhaustive Pattern Matching: Compiler Guarantees vs Runtime Errors
- True Immutability vs Record Illusions
- Memory Safety: Runtime Checks vs Compile-Time Proofs
3. Object-Oriented vs Functional Paradigms
- Inheritance vs Composition
- Interfaces vs Traits
- Virtual Methods vs Static Dispatch
- Sealed Classes vs Rust Immutability
4. Error Handling Philosophy
5. Concurrency and Safety
6. Collections and Iterators
7. Generics and Constraints
8. Practical Migration Patterns
- Incremental Adoption Strategy
- C# to Rust Concept Mapping
- Team Adoption Timeline
- Common C# Patterns in Rust
- Ecosystem Comparison
- Testing and Documentation
9. Performance and Adoption
10. Advanced Topics
11. Best Practices for C# Developers
Language Philosophy Comparison
C# Philosophy
- Productivity first: Rich tooling, extensive framework, "pit of success"
- Managed runtime: Garbage collection handles memory automatically
- Enterprise-focused: Strong typing with reflection, extensive standard library
- Object-oriented: Classes, inheritance, interfaces as primary abstractions
Rust Philosophy
- Performance without sacrifice: Zero-cost abstractions, no runtime overhead
- Memory safety: Compile-time guarantees prevent crashes and security vulnerabilities
- Systems programming: Direct hardware access with high-level abstractions
- Functional + systems: Immutability by default, ownership-based resource management
graph TD
subgraph "C# Development Model"
CS_CODE["C# Source Code<br/>Classes, Methods, Properties"]
CS_COMPILE["C# Compiler<br/>(csc.exe)"]
CS_IL["Intermediate Language<br/>(IL bytecode)"]
CS_RUNTIME[".NET Runtime<br/>(CLR)"]
CS_JIT["Just-In-Time Compiler"]
CS_NATIVE["Native Machine Code"]
CS_GC["Garbage Collector<br/>(Memory management)"]
CS_CODE --> CS_COMPILE
CS_COMPILE --> CS_IL
CS_IL --> CS_RUNTIME
CS_RUNTIME --> CS_JIT
CS_JIT --> CS_NATIVE
CS_RUNTIME --> CS_GC
CS_BENEFITS["[OK] Fast development<br/>[OK] Rich ecosystem<br/>[OK] Automatic memory management<br/>[ERROR] Runtime overhead<br/>[ERROR] GC pauses<br/>[ERROR] Platform dependency"]
end
subgraph "Rust Development Model"
RUST_CODE["Rust Source Code<br/>Structs, Enums, Functions"]
RUST_COMPILE["Rust Compiler<br/>(rustc)"]
RUST_NATIVE["Native Machine Code<br/>(Direct compilation)"]
RUST_ZERO["Zero Runtime<br/>(No VM, No GC)"]
RUST_CODE --> RUST_COMPILE
RUST_COMPILE --> RUST_NATIVE
RUST_NATIVE --> RUST_ZERO
RUST_BENEFITS["[OK] Maximum performance<br/>[OK] Memory safety<br/>[OK] No runtime dependencies<br/>[ERROR] Steeper learning curve<br/>[ERROR] Longer compile times<br/>[ERROR] More explicit code"]
end
style CS_BENEFITS fill:#e3f2fd
style RUST_BENEFITS fill:#e8f5e8
style CS_GC fill:#fff3e0
style RUST_ZERO fill:#e8f5e8
Memory Management: GC vs RAII
C# Garbage Collection
// C# - Automatic memory management
public class Person
{
public string Name { get; set; }
public List<string> Hobbies { get; set; } = new List<string>();
public void AddHobby(string hobby)
{
Hobbies.Add(hobby); // Memory allocated automatically
}
// No explicit cleanup needed - GC handles it
// But IDisposable pattern for resources
}
using var file = new FileStream("data.txt", FileMode.Open);
// 'using' ensures Dispose() is called
Rust Ownership and RAII
// Rust - Compile-time memory management
pub struct Person {
name: String,
hobbies: Vec<String>,
}
impl Person {
pub fn add_hobby(&mut self, hobby: String) {
self.hobbies.push(hobby); // Memory management tracked at compile time
}
// Drop trait automatically implemented - cleanup is guaranteed
}
// RAII - Resource Acquisition Is Initialization
{
let file = std::fs::File::open("data.txt")?;
// File automatically closed when 'file' goes out of scope
// No 'using' statement needed - handled by type system
}
graph TD
subgraph "C# Memory Management"
CS_ALLOC["Object Allocation<br/>new Person()"]
CS_HEAP["Managed Heap"]
CS_REF["References point to heap"]
CS_GC_CHECK["GC periodically checks<br/>for unreachable objects"]
CS_SWEEP["Mark and sweep<br/>collection"]
CS_PAUSE["[ERROR] GC pause times"]
CS_ALLOC --> CS_HEAP
CS_HEAP --> CS_REF
CS_REF --> CS_GC_CHECK
CS_GC_CHECK --> CS_SWEEP
CS_SWEEP --> CS_PAUSE
CS_ISSUES["[ERROR] Non-deterministic cleanup<br/>[ERROR] Memory pressure<br/>[ERROR] Finalization complexity<br/>[OK] Easy to use"]
end
subgraph "Rust Ownership System"
RUST_ALLOC["Value Creation<br/>Person { ... }"]
RUST_OWNER["Single owner<br/>on stack or heap"]
RUST_BORROW["Borrowing system<br/>&T, &mut T"]
RUST_SCOPE["Scope-based cleanup<br/>Drop trait"]
RUST_COMPILE["Compile-time verification"]
RUST_ALLOC --> RUST_OWNER
RUST_OWNER --> RUST_BORROW
RUST_BORROW --> RUST_SCOPE
RUST_SCOPE --> RUST_COMPILE
RUST_BENEFITS["[OK] Deterministic cleanup<br/>[OK] Zero runtime cost<br/>[OK] No memory leaks<br/>[ERROR] Learning curve"]
end
style CS_ISSUES fill:#ffebee
style RUST_BENEFITS fill:#e8f5e8
style CS_PAUSE fill:#ffcdd2
style RUST_COMPILE fill:#c8e6c9
Null Safety: Nullable vs Option
C# Null Handling Evolution
// C# - Traditional null handling (error-prone)
public class User
{
public string Name { get; set; } // Can be null!
public string Email { get; set; } // Can be null!
}
public string GetUserDisplayName(User user)
{
if (user?.Name != null) // Null conditional operator
{
return user.Name;
}
return "Unknown User";
}
// C# 8+ Nullable Reference Types
public class User
{
public string Name { get; set; } // Non-nullable
public string? Email { get; set; } // Explicitly nullable
}
// C# Nullable<T> for value types
int? maybeNumber = GetNumber();
if (maybeNumber.HasValue)
{
Console.WriteLine(maybeNumber.Value);
}
Rust Option System
// Rust - Explicit null handling with Option<T>
#[derive(Debug)]
pub struct User {
name: String, // Never null
email: Option<String>, // Explicitly optional
}
impl User {
pub fn get_display_name(&self) -> &str {
&self.name // No null check needed - guaranteed to exist
}
pub fn get_email_or_default(&self) -> String {
self.email
.as_ref()
.map(|e| e.clone())
.unwrap_or_else(|| "no-email@example.com".to_string())
}
}
// Pattern matching forces handling of None case
fn handle_optional_user(user: Option<User>) {
match user {
Some(u) => println!("User: {}", u.get_display_name()),
None => println!("No user found"),
// Compiler error if None case is not handled!
}
}
graph TD
subgraph "C# Null Handling Evolution"
CS_NULL["Traditional: string name<br/>[ERROR] Can be null"]
CS_NULLABLE["Nullable<T>: int? value<br/>[OK] Explicit for value types"]
CS_NRT["Nullable Reference Types<br/>string? name<br/>[WARNING] Compile-time warnings only"]
CS_RUNTIME["Runtime NullReferenceException<br/>[ERROR] Can still crash"]
CS_NULL --> CS_RUNTIME
CS_NRT -.-> CS_RUNTIME
CS_CHECKS["Manual null checks<br/>if (obj?.Property != null)"]
end
subgraph "Rust Option<T> System"
RUST_OPTION["Option<T><br/>Some(value) | None"]
RUST_FORCE["Compiler forces handling<br/>[OK] Cannot ignore None"]
RUST_MATCH["Pattern matching<br/>match option { ... }"]
RUST_METHODS["Rich API<br/>.map(), .unwrap_or(), .and_then()"]
RUST_OPTION --> RUST_FORCE
RUST_FORCE --> RUST_MATCH
RUST_FORCE --> RUST_METHODS
RUST_SAFE["Compile-time null safety<br/>[OK] No null pointer exceptions"]
RUST_MATCH --> RUST_SAFE
RUST_METHODS --> RUST_SAFE
end
style CS_RUNTIME fill:#ffcdd2
style RUST_SAFE fill:#c8e6c9
style CS_NRT fill:#fff3e0
style RUST_FORCE fill:#c8e6c9
Algebraic Data Types vs C# Unions
C# Discriminated Unions (Limited)
// C# - Limited union support with inheritance
public abstract class Result
{
public abstract T Match<T>(Func<Success, T> onSuccess, Func<Error, T> onError);
}
public class Success : Result
{
public string Value { get; }
public Success(string value) => Value = value;
public override T Match<T>(Func<Success, T> onSuccess, Func<Error, T> onError)
=> onSuccess(this);
}
public class Error : Result
{
public string Message { get; }
public Error(string message) => Message = message;
public override T Match<T>(Func<Success, T> onSuccess, Func<Error, T> onError)
=> onError(this);
}
// C# 9+ Records with pattern matching (better)
public abstract record Shape;
public record Circle(double Radius) : Shape;
public record Rectangle(double Width, double Height) : Shape;
public static double Area(Shape shape) => shape switch
{
Circle(var radius) => Math.PI * radius * radius,
Rectangle(var width, var height) => width * height,
_ => throw new ArgumentException("Unknown shape") // [ERROR] Runtime error possible
};
Rust Algebraic Data Types (Enums)
// Rust - True algebraic data types with exhaustive pattern matching
#[derive(Debug, Clone)]
pub enum Result<T, E> {
Ok(T),
Err(E),
}
#[derive(Debug, Clone)]
pub enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
impl Shape {
pub fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => 0.5 * base * height,
// [OK] Compiler error if any variant is missing!
}
}
}
// Advanced: Enums can hold different types
#[derive(Debug)]
pub enum Value {
Integer(i64),
Float(f64),
Text(String),
Boolean(bool),
List(Vec<Value>), // Recursive types!
}
impl Value {
pub fn type_name(&self) -> &'static str {
match self {
Value::Integer(_) => "integer",
Value::Float(_) => "float",
Value::Text(_) => "text",
Value::Boolean(_) => "boolean",
Value::List(_) => "list",
}
}
}
graph TD
subgraph "C# Discriminated Unions (Workarounds)"
CS_ABSTRACT["abstract class Result"]
CS_SUCCESS["class Success : Result"]
CS_ERROR["class Error : Result"]
CS_MATCH["Manual Match method<br/>or switch expressions"]
CS_RUNTIME["[ERROR] Runtime exceptions<br/>for missing cases"]
CS_HEAP["[ERROR] Heap allocation<br/>for class inheritance"]
CS_ABSTRACT --> CS_SUCCESS
CS_ABSTRACT --> CS_ERROR
CS_SUCCESS --> CS_MATCH
CS_ERROR --> CS_MATCH
CS_MATCH --> CS_RUNTIME
CS_ABSTRACT --> CS_HEAP
end
subgraph "Rust Algebraic Data Types"
RUST_ENUM["enum Shape { ... }"]
RUST_VARIANTS["Circle { radius }<br/>Rectangle { width, height }<br/>Triangle { base, height }"]
RUST_MATCH["match shape { ... }"]
RUST_EXHAUSTIVE["[OK] Exhaustive checking<br/>Compile-time guarantee"]
RUST_STACK["[OK] Stack allocation<br/>Efficient memory use"]
RUST_ZERO["[OK] Zero-cost abstraction"]
RUST_ENUM --> RUST_VARIANTS
RUST_VARIANTS --> RUST_MATCH
RUST_MATCH --> RUST_EXHAUSTIVE
RUST_ENUM --> RUST_STACK
RUST_STACK --> RUST_ZERO
end
style CS_RUNTIME fill:#ffcdd2
style CS_HEAP fill:#fff3e0
style RUST_EXHAUSTIVE fill:#c8e6c9
style RUST_STACK fill:#c8e6c9
style RUST_ZERO fill:#c8e6c9
Exhaustive Pattern Matching: Compiler Guarantees vs Runtime Errors
C# Switch Expressions - Still Incomplete
// C# switch expressions look exhaustive but aren't guaranteed
public enum HttpStatus { Ok, NotFound, ServerError, Unauthorized }
public string HandleResponse(HttpStatus status) => status switch
{
HttpStatus.Ok => "Success",
HttpStatus.NotFound => "Resource not found",
HttpStatus.ServerError => "Internal error",
// Missing Unauthorized case - compiles fine!
// Runtime: System.InvalidOperationException at runtime
};
// Even with nullable warnings, this compiles:
public class User
{
public string Name { get; set; }
public bool IsActive { get; set; }
}
public string ProcessUser(User? user) => user switch
{
{ IsActive: true } => $"Active: {user.Name}",
{ IsActive: false } => $"Inactive: {user.Name}",
// Missing null case - warning only, not error
// Runtime: NullReferenceException possible
};
// Adding enum values breaks existing code silently
public enum HttpStatus
{
Ok,
NotFound,
ServerError,
Unauthorized,
Forbidden // Adding this doesn't break compilation of HandleResponse()!
}
Rust Pattern Matching - True Exhaustiveness
#[derive(Debug)]
enum HttpStatus {
Ok,
NotFound,
ServerError,
Unauthorized,
}
fn handle_response(status: HttpStatus) -> &'static str {
match status {
HttpStatus::Ok => "Success",
HttpStatus::NotFound => "Resource not found",
HttpStatus::ServerError => "Internal error",
HttpStatus::Unauthorized => "Authentication required",
// Compiler ERROR if any case is missing!
// This literally will not compile
}
}
// Adding a new variant breaks compilation everywhere it's used
#[derive(Debug)]
enum HttpStatus {
Ok,
NotFound,
ServerError,
Unauthorized,
Forbidden, // Adding this breaks compilation in handle_response()
}
// The compiler forces you to handle ALL cases
// Option<T> pattern matching is also exhaustive
fn process_optional_value(value: Option<i32>) -> String {
match value {
Some(n) => format!("Got value: {}", n),
None => "No value".to_string(),
// Forgetting either case = compilation error
}
}
graph TD
subgraph "C# Pattern Matching Limitations"
CS_SWITCH["switch expression"]
CS_WARNING["⚠️ Compiler warnings only"]
CS_COMPILE["✅ Compiles successfully"]
CS_RUNTIME["💥 Runtime exceptions"]
CS_DEPLOY["❌ Bugs reach production"]
CS_SILENT["😰 Silent failures on enum changes"]
CS_SWITCH --> CS_WARNING
CS_WARNING --> CS_COMPILE
CS_COMPILE --> CS_RUNTIME
CS_RUNTIME --> CS_DEPLOY
CS_SWITCH --> CS_SILENT
end
subgraph "Rust Exhaustive Matching"
RUST_MATCH["match expression"]
RUST_ERROR["🛑 Compilation fails"]
RUST_FIX["✅ Must handle all cases"]
RUST_SAFE["✅ Zero runtime surprises"]
RUST_EVOLUTION["🔄 Enum changes break compilation"]
RUST_REFACTOR["🛠️ Forced refactoring"]
RUST_MATCH --> RUST_ERROR
RUST_ERROR --> RUST_FIX
RUST_FIX --> RUST_SAFE
RUST_MATCH --> RUST_EVOLUTION
RUST_EVOLUTION --> RUST_REFACTOR
end
style CS_RUNTIME fill:#ffcdd2
style CS_DEPLOY fill:#ffcdd2
style CS_SILENT fill:#ffcdd2
style RUST_SAFE fill:#c8e6c9
style RUST_REFACTOR fill:#c8e6c9
True Immutability vs Record Illusions
C# Records - Immutability Theater
// C# records look immutable but have escape hatches
public record Person(string Name, int Age, List<string> Hobbies);
var person = new Person("John", 30, new List<string> { "reading" });
// These all "look" like they create new instances:
var older = person with { Age = 31 }; // New record
var renamed = person with { Name = "Jonathan" }; // New record
// But the reference types are still mutable!
person.Hobbies.Add("gaming"); // Mutates the original!
Console.WriteLine(older.Hobbies.Count); // 2 - older person affected!
Console.WriteLine(renamed.Hobbies.Count); // 2 - renamed person also affected!
// Init-only properties can still be set via reflection
typeof(Person).GetProperty("Age")?.SetValue(person, 25);
// Collection expressions help but don't solve the fundamental issue
public record BetterPerson(string Name, int Age, IReadOnlyList<string> Hobbies);
var betterPerson = new BetterPerson("Jane", 25, new List<string> { "painting" });
// Still mutable via casting:
((List<string>)betterPerson.Hobbies).Add("hacking the system");
// Even "immutable" collections aren't truly immutable
using System.Collections.Immutable;
public record SafePerson(string Name, int Age, ImmutableList<string> Hobbies);
// This is better, but requires discipline and has performance overhead
Rust - True Immutability by Default
#[derive(Debug, Clone)]
struct Person {
name: String,
age: u32,
hobbies: Vec<String>,
}
let person = Person {
name: "John".to_string(),
age: 30,
hobbies: vec!["reading".to_string()],
};
// This simply won't compile:
// person.age = 31; // ERROR: cannot assign to immutable field
// person.hobbies.push("gaming".to_string()); // ERROR: cannot borrow as mutable
// To modify, you must explicitly opt-in with 'mut':
let mut older_person = person.clone();
older_person.age = 31; // Now it's clear this is mutation
// Or use functional update patterns:
let renamed = Person {
name: "Jonathan".to_string(),
..person // Copies other fields (move semantics apply)
};
// The original is guaranteed unchanged (until moved):
println!("{:?}", person.hobbies); // Always ["reading"] - immutable
// Structural sharing with efficient immutable data structures
use std::rc::Rc;
#[derive(Debug, Clone)]
struct EfficientPerson {
name: String,
age: u32,
hobbies: Rc<Vec<String>>, // Shared, immutable reference
}
// Creating new versions shares data efficiently
let person1 = EfficientPerson {
name: "Alice".to_string(),
age: 30,
hobbies: Rc::new(vec!["reading".to_string(), "cycling".to_string()]),
};
let person2 = EfficientPerson {
name: "Bob".to_string(),
age: 25,
hobbies: Rc::clone(&person1.hobbies), // Shared reference, no deep copy
};
graph TD
subgraph "C# Records - Shallow Immutability"
CS_RECORD["record Person(...)"]
CS_WITH["with expressions"]
CS_SHALLOW["⚠️ Only top-level immutable"]
CS_REF_MUT["❌ Reference types still mutable"]
CS_REFLECTION["❌ Reflection can bypass"]
CS_RUNTIME["❌ Runtime surprises"]
CS_DISCIPLINE["😓 Requires team discipline"]
CS_RECORD --> CS_WITH
CS_WITH --> CS_SHALLOW
CS_SHALLOW --> CS_REF_MUT
CS_RECORD --> CS_REFLECTION
CS_REF_MUT --> CS_RUNTIME
CS_RUNTIME --> CS_DISCIPLINE
end
subgraph "Rust - True Immutability"
RUST_STRUCT["struct Person { ... }"]
RUST_DEFAULT["✅ Immutable by default"]
RUST_COMPILE["✅ Compile-time enforcement"]
RUST_MUT["🔒 Explicit 'mut' required"]
RUST_MOVE["🔄 Move semantics"]
RUST_ZERO["⚡ Zero runtime overhead"]
RUST_SAFE["🛡️ Memory safe"]
RUST_STRUCT --> RUST_DEFAULT
RUST_DEFAULT --> RUST_COMPILE
RUST_COMPILE --> RUST_MUT
RUST_MUT --> RUST_MOVE
RUST_MOVE --> RUST_ZERO
RUST_ZERO --> RUST_SAFE
end
style CS_REF_MUT fill:#ffcdd2
style CS_REFLECTION fill:#ffcdd2
style CS_RUNTIME fill:#ffcdd2
style RUST_COMPILE fill:#c8e6c9
style RUST_ZERO fill:#c8e6c9
style RUST_SAFE fill:#c8e6c9
Memory Safety: Runtime Checks vs Compile-Time Proofs
C# - Runtime Safety Net
// C# relies on runtime checks and GC
public class Buffer
{
private byte[] data;
public Buffer(int size)
{
data = new byte[size];
}
public void ProcessData(int index)
{
// Runtime bounds checking
if (index >= data.Length)
throw new IndexOutOfRangeException();
data[index] = 42; // Safe, but checked at runtime
}
// Memory leaks still possible with events/static references
public static event Action<string> GlobalEvent;
public void Subscribe()
{
GlobalEvent += HandleEvent; // Can create memory leaks
// Forgot to unsubscribe - object won't be collected
}
private void HandleEvent(string message) { /* ... */ }
// Null reference exceptions are still possible
public void ProcessUser(User user)
{
Console.WriteLine(user.Name.ToUpper()); // NullReferenceException if user.Name is null
}
// Array access can fail at runtime
public int GetValue(int[] array, int index)
{
return array[index]; // IndexOutOfRangeException possible
}
}
Rust - Compile-Time Guarantees
struct Buffer {
data: Vec<u8>,
}
impl Buffer {
fn new(size: usize) -> Self {
Buffer {
data: vec![0; size],
}
}
fn process_data(&mut self, index: usize) {
// Bounds checking can be optimized away by compiler when proven safe
if let Some(item) = self.data.get_mut(index) {
*item = 42; // Safe access, proven at compile time
}
// Or use indexing with explicit bounds check:
// self.data[index] = 42; // Panics in debug, but memory-safe
}
// Memory leaks impossible - ownership system prevents them
fn process_with_closure<F>(&mut self, processor: F)
where F: FnOnce(&mut Vec<u8>)
{
processor(&mut self.data);
// When processor goes out of scope, it's automatically cleaned up
// No way to create dangling references or memory leaks
}
// Null pointer dereferences impossible - no null pointers!
fn process_user(&self, user: &User) {
println!("{}", user.name.to_uppercase()); // user.name cannot be null
}
// Array access is bounds-checked or explicitly unsafe
fn get_value(array: &[i32], index: usize) -> Option<i32> {
array.get(index).copied() // Returns None if out of bounds
}
// Or explicitly unsafe if you know what you're doing:
/// # Safety
/// `index` must be less than `array.len()`.
unsafe fn get_value_unchecked(array: &[i32], index: usize) -> i32 {
*array.get_unchecked(index) // Fast but must prove bounds manually
}
}
struct User {
name: String, // String cannot be null in Rust
}
// Ownership prevents use-after-free
fn ownership_example() {
let data = vec![1, 2, 3, 4, 5];
let reference = &data[0]; // Borrow data
// drop(data); // ERROR: cannot drop while borrowed
println!("{}", reference); // This is guaranteed safe
}
// Borrowing prevents data races
fn borrowing_example(data: &mut Vec<i32>) {
let first = &data[0]; // Immutable borrow
// data.push(6); // ERROR: cannot mutably borrow while immutably borrowed
println!("{}", first); // Guaranteed no data race
}
graph TD
subgraph "C# Runtime Safety"
CS_RUNTIME["Runtime Checks"]
CS_GC["Garbage Collector"]
CS_EXCEPTIONS["Exception Handling"]
CS_BOUNDS["Runtime bounds checking"]
CS_NULL["Null reference exceptions"]
CS_LEAKS["Memory leaks possible"]
CS_OVERHEAD["Performance overhead"]
CS_RUNTIME --> CS_BOUNDS
CS_RUNTIME --> CS_NULL
CS_GC --> CS_LEAKS
CS_EXCEPTIONS --> CS_OVERHEAD
end
subgraph "Rust Compile-Time Safety"
RUST_OWNERSHIP["Ownership System"]
RUST_BORROWING["Borrow Checker"]
RUST_TYPES["Type System"]
RUST_ZERO_COST["Zero-cost abstractions"]
RUST_NO_NULL["No null pointers"]
RUST_NO_LEAKS["No memory leaks"]
RUST_FAST["Optimal performance"]
RUST_OWNERSHIP --> RUST_NO_LEAKS
RUST_BORROWING --> RUST_NO_NULL
RUST_TYPES --> RUST_ZERO_COST
RUST_ZERO_COST --> RUST_FAST
end
style CS_NULL fill:#ffcdd2
style CS_LEAKS fill:#ffcdd2
style CS_OVERHEAD fill:#fff3e0
style RUST_NO_NULL fill:#c8e6c9
style RUST_NO_LEAKS fill:#c8e6c9
style RUST_FAST fill:#c8e6c9
Inheritance vs Composition
// C# - Class-based inheritance
public abstract class Animal
{
public string Name { get; protected set; }
public abstract void MakeSound();
public virtual void Sleep()
{
Console.WriteLine($"{Name} is sleeping");
}
}
public class Dog : Animal
{
public Dog(string name) { Name = name; }
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
public void Fetch()
{
Console.WriteLine($"{Name} is fetching");
}
}
// Interface-based contracts
public interface IFlyable
{
void Fly();
}
public class Bird : Animal, IFlyable
{
public Bird(string name) { Name = name; }
public override void MakeSound()
{
Console.WriteLine("Tweet!");
}
public void Fly()
{
Console.WriteLine($"{Name} is flying");
}
}
Rust Composition Model
// Rust - Composition over inheritance with traits
pub trait Animal {
fn name(&self) -> &str;
fn make_sound(&self);
// Default implementation (like C# virtual methods)
fn sleep(&self) {
println!("{} is sleeping", self.name());
}
}
pub trait Flyable {
fn fly(&self);
}
// Separate data from behavior
#[derive(Debug)]
pub struct Dog {
name: String,
}
#[derive(Debug)]
pub struct Bird {
name: String,
wingspan: f64,
}
// Implement behaviors for types
impl Animal for Dog {
fn name(&self) -> &str {
&self.name
}
fn make_sound(&self) {
println!("Woof!");
}
}
impl Dog {
pub fn new(name: String) -> Self {
Dog { name }
}
pub fn fetch(&self) {
println!("{} is fetching", self.name);
}
}
impl Animal for Bird {
fn name(&self) -> &str {
&self.name
}
fn make_sound(&self) {
println!("Tweet!");
}
}
impl Flyable for Bird {
fn fly(&self) {
println!("{} is flying with {:.1}m wingspan", self.name, self.wingspan);
}
}
// Multiple trait bounds (like multiple interfaces)
fn make_flying_animal_sound<T>(animal: &T)
where
T: Animal + Flyable,
{
animal.make_sound();
animal.fly();
}
graph TD
subgraph "C# Inheritance Hierarchy"
CS_ANIMAL["Animal (abstract class)"]
CS_DOG["Dog : Animal"]
CS_BIRD["Bird : Animal, IFlyable"]
CS_VTABLE["Virtual method dispatch<br/>Runtime cost"]
CS_COUPLING["[ERROR] Tight coupling<br/>[ERROR] Diamond problem<br/>[ERROR] Deep hierarchies"]
CS_ANIMAL --> CS_DOG
CS_ANIMAL --> CS_BIRD
CS_DOG --> CS_VTABLE
CS_BIRD --> CS_VTABLE
CS_ANIMAL --> CS_COUPLING
end
subgraph "Rust Composition Model"
RUST_ANIMAL["trait Animal"]
RUST_FLYABLE["trait Flyable"]
RUST_DOG["struct Dog"]
RUST_BIRD["struct Bird"]
RUST_IMPL1["impl Animal for Dog"]
RUST_IMPL2["impl Animal for Bird"]
RUST_IMPL3["impl Flyable for Bird"]
RUST_STATIC["Static dispatch<br/>Zero cost"]
RUST_FLEXIBLE["[OK] Flexible composition<br/>[OK] No hierarchy limits<br/>[OK] Mix and match traits"]
RUST_DOG --> RUST_IMPL1
RUST_BIRD --> RUST_IMPL2
RUST_BIRD --> RUST_IMPL3
RUST_IMPL1 --> RUST_ANIMAL
RUST_IMPL2 --> RUST_ANIMAL
RUST_IMPL3 --> RUST_FLYABLE
RUST_IMPL1 --> RUST_STATIC
RUST_IMPL2 --> RUST_STATIC
RUST_IMPL3 --> RUST_STATIC
RUST_ANIMAL --> RUST_FLEXIBLE
RUST_FLYABLE --> RUST_FLEXIBLE
end
style CS_COUPLING fill:#ffcdd2
style RUST_FLEXIBLE fill:#c8e6c9
style CS_VTABLE fill:#fff3e0
style RUST_STATIC fill:#c8e6c9
Exceptions vs Result<T, E>
C# Exception-Based Error Handling
// C# - Exception-based error handling
public class UserService
{
public User GetUser(int userId)
{
if (userId <= 0)
{
throw new ArgumentException("User ID must be positive");
}
var user = database.FindUser(userId);
if (user == null)
{
throw new UserNotFoundException($"User {userId} not found");
}
return user;
}
public async Task<string> GetUserEmailAsync(int userId)
{
try
{
var user = GetUser(userId);
return user.Email ?? throw new InvalidOperationException("User has no email");
}
catch (UserNotFoundException ex)
{
logger.Warning("User not found: {UserId}", userId);
return "noreply@company.com";
}
catch (Exception ex)
{
logger.Error(ex, "Unexpected error getting user email");
throw; // Re-throw
}
}
}
Rust Result-Based Error Handling
use std::fmt;
#[derive(Debug)]
pub enum UserError {
InvalidId(i32),
NotFound(i32),
NoEmail,
DatabaseError(String),
}
impl fmt::Display for UserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UserError::InvalidId(id) => write!(f, "Invalid user ID: {}", id),
UserError::NotFound(id) => write!(f, "User {} not found", id),
UserError::NoEmail => write!(f, "User has no email address"),
UserError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
}
}
}
impl std::error::Error for UserError {}
#[derive(Debug, Clone)]
pub struct User {
pub name: String,
pub email: Option<String>,
}
pub struct UserService {
users: Vec<User>, // Simulated database
}
impl UserService {
fn database_find_user(&self, user_id: i32) -> Option<User> {
self.users.get(user_id as usize).cloned()
}
pub fn get_user(&self, user_id: i32) -> Result<User, UserError> {
if user_id <= 0 {
return Err(UserError::InvalidId(user_id));
}
// Simulate database lookup
self.database_find_user(user_id)
.ok_or(UserError::NotFound(user_id))
}
pub fn get_user_email(&self, user_id: i32) -> Result<String, UserError> {
let user = self.get_user(user_id)?; // ? operator propagates errors
user.email
.ok_or(UserError::NoEmail)
}
pub fn get_user_email_or_default(&self, user_id: i32) -> String {
match self.get_user_email(user_id) {
Ok(email) => email,
Err(UserError::NotFound(_)) => {
log::warn!("User not found: {}", user_id);
"noreply@company.com".to_string()
}
Err(err) => {
log::error!("Error getting user email: {}", err);
"error@company.com".to_string()
}
}
}
}
graph TD
subgraph "C# Exception Model"
CS_CALL["Method Call"]
CS_SUCCESS["Success Path"]
CS_EXCEPTION["throw Exception"]
CS_STACK["Stack unwinding<br/>(Runtime cost)"]
CS_CATCH["try/catch block"]
CS_HIDDEN["[ERROR] Hidden control flow<br/>[ERROR] Performance cost<br/>[ERROR] Easy to ignore"]
CS_CALL --> CS_SUCCESS
CS_CALL --> CS_EXCEPTION
CS_EXCEPTION --> CS_STACK
CS_STACK --> CS_CATCH
CS_EXCEPTION --> CS_HIDDEN
end
subgraph "Rust Result Model"
RUST_CALL["Function Call"]
RUST_OK["Ok(value)"]
RUST_ERR["Err(error)"]
RUST_MATCH["match result"]
RUST_QUESTION["? operator<br/>(early return)"]
RUST_EXPLICIT["[OK] Explicit error handling<br/>[OK] Zero runtime cost<br/>[OK] Cannot ignore errors"]
RUST_CALL --> RUST_OK
RUST_CALL --> RUST_ERR
RUST_OK --> RUST_MATCH
RUST_ERR --> RUST_MATCH
RUST_ERR --> RUST_QUESTION
RUST_MATCH --> RUST_EXPLICIT
RUST_QUESTION --> RUST_EXPLICIT
end
style CS_HIDDEN fill:#ffcdd2
style RUST_EXPLICIT fill:#c8e6c9
style CS_STACK fill:#fff3e0
style RUST_QUESTION fill:#c8e6c9
LINQ vs Rust Iterators
C# LINQ (Language Integrated Query)
// C# LINQ - Declarative data processing
var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var result = numbers
.Where(n => n % 2 == 0) // Filter even numbers
.Select(n => n * n) // Square them
.Where(n => n > 10) // Filter > 10
.OrderByDescending(n => n) // Sort descending
.Take(3) // Take first 3
.ToList(); // Materialize
// LINQ with complex objects
var users = GetUsers();
var activeAdults = users
.Where(u => u.IsActive && u.Age >= 18)
.GroupBy(u => u.Department)
.Select(g => new {
Department = g.Key,
Count = g.Count(),
AverageAge = g.Average(u => u.Age)
})
.OrderBy(x => x.Department)
.ToList();
// Async LINQ (with additional libraries)
var results = await users
.ToAsyncEnumerable()
.WhereAwait(async u => await IsActiveAsync(u.Id))
.SelectAwait(async u => await EnrichUserAsync(u))
.ToListAsync();
Rust Iterators
// Rust iterators - Lazy, zero-cost abstractions
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers
.iter()
.filter(|&&n| n % 2 == 0) // Filter even numbers
.map(|&n| n * n) // Square them
.filter(|&n| n > 10) // Filter > 10
.collect::<Vec<_>>() // Collect to Vec
.into_iter()
.rev() // Reverse (descending sort)
.take(3) // Take first 3
.collect(); // Materialize
// Complex iterator chains
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct User {
name: String,
age: u32,
department: String,
is_active: bool,
}
fn process_users(users: Vec<User>) -> HashMap<String, (usize, f64)> {
users
.into_iter()
.filter(|u| u.is_active && u.age >= 18)
.fold(HashMap::new(), |mut acc, user| {
let entry = acc.entry(user.department.clone()).or_insert((0, 0.0));
entry.0 += 1; // count
entry.1 += user.age as f64; // sum of ages
acc
})
.into_iter()
.map(|(dept, (count, sum))| (dept, (count, sum / count as f64))) // average
.collect()
}
// Parallel processing with rayon
use rayon::prelude::*;
fn parallel_processing(numbers: Vec<i32>) -> Vec<i32> {
numbers
.par_iter() // Parallel iterator
.filter(|&&n| n % 2 == 0)
.map(|&n| expensive_computation(n))
.collect()
}
fn expensive_computation(n: i32) -> i32 {
// Simulate heavy computation
(0..1000).fold(n, |acc, _| acc + 1)
}
graph TD
subgraph "C# LINQ Characteristics"
CS_LINQ["LINQ Expression"]
CS_EAGER["Often eager evaluation<br/>(ToList(), ToArray())"]
CS_REFLECTION["[ERROR] Some runtime reflection<br/>Expression trees"]
CS_ALLOCATIONS["[ERROR] Intermediate collections<br/>Garbage collection pressure"]
CS_ASYNC["[OK] Async support<br/>(with additional libraries)"]
CS_SQL["[OK] LINQ to SQL/EF integration"]
CS_LINQ --> CS_EAGER
CS_LINQ --> CS_REFLECTION
CS_LINQ --> CS_ALLOCATIONS
CS_LINQ --> CS_ASYNC
CS_LINQ --> CS_SQL
end
subgraph "Rust Iterator Characteristics"
RUST_ITER["Iterator Chain"]
RUST_LAZY["[OK] Lazy evaluation<br/>No work until .collect()"]
RUST_ZERO["[OK] Zero-cost abstractions<br/>Compiles to optimal loops"]
RUST_NO_ALLOC["[OK] No intermediate allocations<br/>Stack-based processing"]
RUST_PARALLEL["[OK] Easy parallelization<br/>(rayon crate)"]
RUST_FUNCTIONAL["[OK] Functional programming<br/>Immutable by default"]
RUST_ITER --> RUST_LAZY
RUST_ITER --> RUST_ZERO
RUST_ITER --> RUST_NO_ALLOC
RUST_ITER --> RUST_PARALLEL
RUST_ITER --> RUST_FUNCTIONAL
end
subgraph "Performance Comparison"
CS_PERF["C# LINQ Performance<br/>[ERROR] Allocation overhead<br/>[ERROR] Virtual dispatch<br/>[OK] Good enough for most cases"]
RUST_PERF["Rust Iterator Performance<br/>[OK] Hand-optimized speed<br/>[OK] No allocations<br/>[OK] Compile-time optimization"]
end
style CS_REFLECTION fill:#ffcdd2
style CS_ALLOCATIONS fill:#fff3e0
style RUST_ZERO fill:#c8e6c9
style RUST_LAZY fill:#c8e6c9
style RUST_NO_ALLOC fill:#c8e6c9
style CS_PERF fill:#fff3e0
style RUST_PERF fill:#c8e6c9
Generic Constraints: where vs trait bounds
C# Generic Constraints
// C# Generic constraints with where clause
public class Repository<T> where T : class, IEntity, new()
{
public T Create()
{
return new T(); // new() constraint allows parameterless constructor
}
public void Save(T entity)
{
if (entity.Id == 0) // IEntity constraint provides Id property
{
entity.Id = GenerateId();
}
// Save to database
}
}
// Multiple type parameters with constraints
public class Converter<TInput, TOutput>
where TInput : IConvertible
where TOutput : class, new()
{
public TOutput Convert(TInput input)
{
var output = new TOutput();
// Conversion logic using IConvertible
return output;
}
}
// Variance in generics
public interface IRepository<out T> where T : IEntity
{
IEnumerable<T> GetAll(); // Covariant - can return more derived types
}
public interface IWriter<in T> where T : IEntity
{
void Write(T entity); // Contravariant - can accept more base types
}
Rust Generic Constraints with Trait Bounds
use std::fmt::{Debug, Display};
use std::clone::Clone;
// Basic trait bounds
pub struct Repository<T>
where
T: Clone + Debug + Default,
{
items: Vec<T>,
}
impl<T> Repository<T>
where
T: Clone + Debug + Default,
{
pub fn new() -> Self {
Repository { items: Vec::new() }
}
pub fn create(&self) -> T {
T::default() // Default trait provides default value
}
pub fn add(&mut self, item: T) {
println!("Adding item: {:?}", item); // Debug trait for printing
self.items.push(item);
}
pub fn get_all(&self) -> Vec<T> {
self.items.clone() // Clone trait for duplication
}
}
// Multiple trait bounds with different syntaxes
pub fn process_data<T, U>(input: T) -> U
where
T: Display + Clone,
U: From<T> + Debug,
{
println!("Processing: {}", input); // Display trait
let cloned = input.clone(); // Clone trait
let output = U::from(cloned); // From trait for conversion
println!("Result: {:?}", output); // Debug trait
output
}
// Associated types (similar to C# generic constraints)
pub trait Iterator {
type Item; // Associated type instead of generic parameter
fn next(&mut self) -> Option<Self::Item>;
}
pub trait Collect<T> {
fn collect<I: Iterator<Item = T>>(iter: I) -> Self;
}
// Higher-ranked trait bounds (advanced)
fn apply_to_all<F>(items: &[String], f: F) -> Vec<String>
where
F: for<'a> Fn(&'a str) -> String, // Function works with any lifetime
{
items.iter().map(|s| f(s)).collect()
}
// Conditional trait implementations
impl<T> PartialEq for Repository<T>
where
T: PartialEq + Clone + Debug + Default,
{
fn eq(&self, other: &Self) -> bool {
self.items == other.items
}
}
graph TD
subgraph "C# Generic Constraints"
CS_WHERE["where T : class, IInterface, new()"]
CS_RUNTIME["[ERROR] Some runtime type checking<br/>Virtual method dispatch"]
CS_VARIANCE["[OK] Covariance/Contravariance<br/>in/out keywords"]
CS_REFLECTION["[ERROR] Runtime reflection possible<br/>typeof(T), is, as operators"]
CS_BOXING["[ERROR] Value type boxing<br/>for interface constraints"]
CS_WHERE --> CS_RUNTIME
CS_WHERE --> CS_VARIANCE
CS_WHERE --> CS_REFLECTION
CS_WHERE --> CS_BOXING
end
subgraph "Rust Trait Bounds"
RUST_WHERE["where T: Trait + Clone + Debug"]
RUST_COMPILE["[OK] Compile-time resolution<br/>Monomorphization"]
RUST_ZERO["[OK] Zero-cost abstractions<br/>No runtime overhead"]
RUST_ASSOCIATED["[OK] Associated types<br/>More flexible than generics"]
RUST_HKT["[OK] Higher-ranked trait bounds<br/>Advanced type relationships"]
RUST_WHERE --> RUST_COMPILE
RUST_WHERE --> RUST_ZERO
RUST_WHERE --> RUST_ASSOCIATED
RUST_WHERE --> RUST_HKT
end
subgraph "Flexibility Comparison"
CS_FLEX["C# Flexibility<br/>[OK] Variance<br/>[OK] Runtime type info<br/>[ERROR] Performance cost"]
RUST_FLEX["Rust Flexibility<br/>[OK] Zero cost<br/>[OK] Compile-time safety<br/>[ERROR] No variance (yet)"]
end
style CS_RUNTIME fill:#fff3e0
style CS_BOXING fill:#ffcdd2
style RUST_COMPILE fill:#c8e6c9
style RUST_ZERO fill:#c8e6c9
style CS_FLEX fill:#e3f2fd
style RUST_FLEX fill:#c8e6c9
Common C# Patterns in Rust
Repository Pattern
// C# Repository Pattern
public interface IRepository<T> where T : IEntity
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
public class UserRepository : IRepository<User>
{
private readonly DbContext _context;
public UserRepository(DbContext context)
{
_context = context;
}
public async Task<User> GetByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
// ... other implementations
}
// Rust Repository Pattern with traits and generics
use async_trait::async_trait;
use std::fmt::Debug;
#[async_trait]
pub trait Repository<T, E>
where
T: Clone + Debug + Send + Sync,
E: std::error::Error + Send + Sync,
{
async fn get_by_id(&self, id: u64) -> Result<Option<T>, E>;
async fn get_all(&self) -> Result<Vec<T>, E>;
async fn add(&self, entity: T) -> Result<T, E>;
async fn update(&self, entity: T) -> Result<T, E>;
async fn delete(&self, id: u64) -> Result<(), E>;
}
#[derive(Debug, Clone)]
pub struct User {
pub id: u64,
pub name: String,
pub email: String,
}
#[derive(Debug)]
pub enum RepositoryError {
NotFound(u64),
DatabaseError(String),
ValidationError(String),
}
impl std::fmt::Display for RepositoryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RepositoryError::NotFound(id) => write!(f, "Entity with id {} not found", id),
RepositoryError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
RepositoryError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
}
}
}
impl std::error::Error for RepositoryError {}
pub struct UserRepository {
// database connection pool, etc.
}
#[async_trait]
impl Repository<User, RepositoryError> for UserRepository {
async fn get_by_id(&self, id: u64) -> Result<Option<User>, RepositoryError> {
// Simulate database lookup
if id == 0 {
return Ok(None);
}
Ok(Some(User {
id,
name: format!("User {}", id),
email: format!("user{}@example.com", id),
}))
}
async fn get_all(&self) -> Result<Vec<User>, RepositoryError> {
// Implementation here
Ok(vec![])
}
async fn add(&self, entity: User) -> Result<User, RepositoryError> {
// Validation and database insertion
if entity.name.is_empty() {
return Err(RepositoryError::ValidationError("Name cannot be empty".to_string()));
}
Ok(entity)
}
async fn update(&self, entity: User) -> Result<User, RepositoryError> {
// Implementation here
Ok(entity)
}
async fn delete(&self, id: u64) -> Result<(), RepositoryError> {
// Implementation here
Ok(())
}
}
Builder Pattern
// C# Builder Pattern (fluent interface)
public class HttpClientBuilder
{
private TimeSpan? _timeout;
private string _baseAddress;
private Dictionary<string, string> _headers = new();
public HttpClientBuilder WithTimeout(TimeSpan timeout)
{
_timeout = timeout;
return this;
}
public HttpClientBuilder WithBaseAddress(string baseAddress)
{
_baseAddress = baseAddress;
return this;
}
public HttpClientBuilder WithHeader(string name, string value)
{
_headers[name] = value;
return this;
}
public HttpClient Build()
{
var client = new HttpClient();
if (_timeout.HasValue)
client.Timeout = _timeout.Value;
if (!string.IsNullOrEmpty(_baseAddress))
client.BaseAddress = new Uri(_baseAddress);
foreach (var header in _headers)
client.DefaultRequestHeaders.Add(header.Key, header.Value);
return client;
}
}
// Usage
var client = new HttpClientBuilder()
.WithTimeout(TimeSpan.FromSeconds(30))
.WithBaseAddress("https://api.example.com")
.WithHeader("Accept", "application/json")
.Build();
// Rust Builder Pattern (consuming builder)
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug)]
pub struct HttpClient {
timeout: Duration,
base_address: String,
headers: HashMap<String, String>,
}
pub struct HttpClientBuilder {
timeout: Option<Duration>,
base_address: Option<String>,
headers: HashMap<String, String>,
}
impl HttpClientBuilder {
pub fn new() -> Self {
HttpClientBuilder {
timeout: None,
base_address: None,
headers: HashMap::new(),
}
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn with_base_address<S: Into<String>>(mut self, base_address: S) -> Self {
self.base_address = Some(base_address.into());
self
}
pub fn with_header<K: Into<String>, V: Into<String>>(mut self, name: K, value: V) -> Self {
self.headers.insert(name.into(), value.into());
self
}
pub fn build(self) -> Result<HttpClient, String> {
let base_address = self.base_address.ok_or("Base address is required")?;
Ok(HttpClient {
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
base_address,
headers: self.headers,
})
}
}
// Usage
let client = HttpClientBuilder::new()
.with_timeout(Duration::from_secs(30))
.with_base_address("https://api.example.com")
.with_header("Accept", "application/json")
.build()?;
// Alternative: Using Default trait for common cases
impl Default for HttpClientBuilder {
fn default() -> Self {
Self::new()
}
}
Essential Crates for C# Developers
Core Functionality Equivalents
// Cargo.toml dependencies for C# developers
[dependencies]
# Serialization (like Newtonsoft.Json or System.Text.Json)
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# HTTP client (like HttpClient)
reqwest = { version = "0.11", features = ["json"] }
# Async runtime (like Task.Run, async/await)
tokio = { version = "1.0", features = ["full"] }
# Error handling (like custom exceptions)
thiserror = "1.0"
anyhow = "1.0"
# Logging (like ILogger, Serilog)
log = "0.4"
env_logger = "0.10"
# Date/time (like DateTime)
chrono = { version = "0.4", features = ["serde"] }
# UUID (like System.Guid)
uuid = { version = "1.0", features = ["v4", "serde"] }
# Collections (like List<T>, Dictionary<K,V>)
# Built into std, but for advanced collections:
indexmap = "2.0" # Ordered HashMap
# Configuration (like IConfiguration)
config = "0.13"
# Database (like Entity Framework)
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] }
# Testing (like xUnit, NUnit)
# Built into std, but for more features:
rstest = "0.18" # Parameterized tests
# Mocking (like Moq)
mockall = "0.11"
# Parallel processing (like Parallel.ForEach)
rayon = "1.7"
Example Usage Patterns
use serde::{Deserialize, Serialize};
use reqwest;
use tokio;
use thiserror::Error;
use chrono::{DateTime, Utc};
use uuid::Uuid;
// Data models (like C# POCOs with attributes)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: Uuid,
pub name: String,
pub email: String,
#[serde(with = "chrono::serde::ts_seconds")]
pub created_at: DateTime<Utc>,
}
// Custom error types (like custom exceptions)
#[derive(Error, Debug)]
pub enum ApiError {
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("Serialization failed: {0}")]
Serialization(#[from] serde_json::Error),
#[error("User not found: {id}")]
UserNotFound { id: Uuid },
#[error("Validation failed: {message}")]
Validation { message: String },
}
// Service class equivalent
pub struct UserService {
client: reqwest::Client,
base_url: String,
}
impl UserService {
pub fn new(base_url: String) -> Self {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client");
UserService { client, base_url }
}
// Async method (like C# async Task<User>)
pub async fn get_user(&self, id: Uuid) -> Result<User, ApiError> {
let url = format!("{}/users/{}", self.base_url, id);
let response = self.client
.get(&url)
.send()
.await?;
if response.status() == 404 {
return Err(ApiError::UserNotFound { id });
}
let user = response.json::<User>().await?;
Ok(user)
}
// Create user (like C# async Task<User>)
pub async fn create_user(&self, name: String, email: String) -> Result<User, ApiError> {
if name.trim().is_empty() {
return Err(ApiError::Validation {
message: "Name cannot be empty".to_string(),
});
}
let new_user = User {
id: Uuid::new_v4(),
name,
email,
created_at: Utc::now(),
};
let response = self.client
.post(&format!("{}/users", self.base_url))
.json(&new_user)
.send()
.await?;
let created_user = response.json::<User>().await?;
Ok(created_user)
}
}
// Usage example (like C# Main method)
#[tokio::main]
async fn main() -> Result<(), ApiError> {
// Initialize logging (like configuring ILogger)
env_logger::init();
let service = UserService::new("https://api.example.com".to_string());
// Create user
let user = service.create_user(
"John Doe".to_string(),
"john@example.com".to_string(),
).await?;
println!("Created user: {:?}", user);
// Get user
let retrieved_user = service.get_user(user.id).await?;
println!("Retrieved user: {:?}", retrieved_user);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test] // Like C# [Test] or [Fact]
async fn test_user_creation() {
let service = UserService::new("http://localhost:8080".to_string());
let result = service.create_user(
"Test User".to_string(),
"test@example.com".to_string(),
).await;
assert!(result.is_ok());
let user = result.unwrap();
assert_eq!(user.name, "Test User");
assert_eq!(user.email, "test@example.com");
}
#[test]
fn test_validation() {
// Synchronous test
let error = ApiError::Validation {
message: "Invalid input".to_string(),
};
assert_eq!(error.to_string(), "Validation failed: Invalid input");
}
}
Thread Safety: Convention vs Type System Guarantees
C# - Thread Safety by Convention
// C# collections aren't thread-safe by default
public class UserService
{
private readonly List<string> items = new();
private readonly Dictionary<int, User> cache = new();
// This can cause data races:
public void AddItem(string item)
{
items.Add(item); // Not thread-safe!
}
// Must use locks manually:
private readonly object lockObject = new();
public void SafeAddItem(string item)
{
lock (lockObject)
{
items.Add(item); // Safe, but runtime overhead
}
// Easy to forget the lock elsewhere
}
// ConcurrentCollection helps but limited:
private readonly ConcurrentBag<string> safeItems = new();
public void ConcurrentAdd(string item)
{
safeItems.Add(item); // Thread-safe but limited operations
}
// Complex shared state management
private readonly ConcurrentDictionary<int, User> threadSafeCache = new();
private volatile bool isShutdown = false;
public async Task ProcessUser(int userId)
{
if (isShutdown) return; // Race condition possible!
var user = await GetUser(userId);
threadSafeCache.TryAdd(userId, user); // Must remember which collections are safe
}
// Thread-local storage requires careful management
private static readonly ThreadLocal<Random> threadLocalRandom =
new ThreadLocal<Random>(() => new Random());
public int GetRandomNumber()
{
return threadLocalRandom.Value.Next(); // Safe but manual management
}
}
// Event handling with potential race conditions
public class EventProcessor
{
public event Action<string> DataReceived;
private readonly List<string> eventLog = new();
public void OnDataReceived(string data)
{
// Race condition - event might be null between check and invocation
if (DataReceived != null)
{
DataReceived(data);
}
// Another race condition - list not thread-safe
eventLog.Add($"Processed: {data}");
}
}
Rust - Thread Safety Guaranteed by Type System
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use std::collections::HashMap;
use tokio::sync::{mpsc, broadcast};
// Rust prevents data races at compile time
pub struct UserService {
items: Arc<Mutex<Vec<String>>>,
cache: Arc<RwLock<HashMap<i32, User>>>,
}
impl UserService {
pub fn new() -> Self {
UserService {
items: Arc::new(Mutex::new(Vec::new())),
cache: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn add_item(&self, item: String) {
let mut items = self.items.lock().unwrap();
items.push(item);
// Lock automatically released when `items` goes out of scope
}
// Multiple readers, single writer - automatically enforced
pub async fn get_user(&self, user_id: i32) -> Option<User> {
let cache = self.cache.read().unwrap();
cache.get(&user_id).cloned()
}
pub async fn cache_user(&self, user_id: i32, user: User) {
let mut cache = self.cache.write().unwrap();
cache.insert(user_id, user);
}
// Clone the Arc for thread sharing
pub fn process_in_background(&self) {
let items = Arc::clone(&self.items);
thread::spawn(move || {
let items = items.lock().unwrap();
for item in items.iter() {
println!("Processing: {}", item);
}
});
}
}
// Channel-based communication - no shared state needed
pub struct MessageProcessor {
sender: mpsc::UnboundedSender<String>,
}
impl MessageProcessor {
pub fn new() -> (Self, mpsc::UnboundedReceiver<String>) {
let (tx, rx) = mpsc::unbounded_channel();
(MessageProcessor { sender: tx }, rx)
}
pub fn send_message(&self, message: String) -> Result<(), mpsc::error::SendError<String>> {
self.sender.send(message)
}
}
// This won't compile - Rust prevents sharing mutable data unsafely:
fn impossible_data_race() {
let mut items = vec![1, 2, 3];
// This won't compile - cannot move `items` into multiple closures
/*
thread::spawn(move || {
items.push(4); // ERROR: use of moved value
});
thread::spawn(move || {
items.push(5); // ERROR: use of moved value
});
*/
}
// Safe concurrent data processing
use rayon::prelude::*;
fn parallel_processing() {
let data = vec![1, 2, 3, 4, 5];
// Parallel iteration - guaranteed thread-safe
let results: Vec<i32> = data
.par_iter()
.map(|&x| x * x)
.collect();
println!("{:?}", results);
}
// Async concurrency with message passing
async fn async_message_passing() {
let (tx, mut rx) = mpsc::channel(100);
// Producer task
let producer = tokio::spawn(async move {
for i in 0..10 {
if tx.send(i).await.is_err() {
break;
}
}
});
// Consumer task
let consumer = tokio::spawn(async move {
while let Some(value) = rx.recv().await {
println!("Received: {}", value);
}
});
// Wait for both tasks
let (producer_result, consumer_result) = tokio::join!(producer, consumer);
producer_result.unwrap();
consumer_result.unwrap();
}
#[derive(Clone)]
struct User {
id: i32,
name: String,
}
graph TD
subgraph "C# Thread Safety Challenges"
CS_MANUAL["Manual synchronization"]
CS_LOCKS["lock statements"]
CS_CONCURRENT["ConcurrentCollections"]
CS_VOLATILE["volatile fields"]
CS_FORGET["😰 Easy to forget locks"]
CS_DEADLOCK["💀 Deadlock possible"]
CS_RACE["🏃 Race conditions"]
CS_OVERHEAD["⚡ Runtime overhead"]
CS_MANUAL --> CS_LOCKS
CS_MANUAL --> CS_CONCURRENT
CS_MANUAL --> CS_VOLATILE
CS_LOCKS --> CS_FORGET
CS_LOCKS --> CS_DEADLOCK
CS_FORGET --> CS_RACE
CS_LOCKS --> CS_OVERHEAD
end
subgraph "Rust Type System Guarantees"
RUST_OWNERSHIP["Ownership system"]
RUST_BORROWING["Borrow checker"]
RUST_SEND["Send trait"]
RUST_SYNC["Sync trait"]
RUST_ARC["Arc<Mutex<T>>"]
RUST_CHANNELS["Message passing"]
RUST_SAFE["✅ Data races impossible"]
RUST_FAST["⚡ Zero-cost abstractions"]
RUST_OWNERSHIP --> RUST_BORROWING
RUST_BORROWING --> RUST_SEND
RUST_SEND --> RUST_SYNC
RUST_SYNC --> RUST_ARC
RUST_ARC --> RUST_CHANNELS
RUST_CHANNELS --> RUST_SAFE
RUST_SAFE --> RUST_FAST
end
style CS_FORGET fill:#ffcdd2
style CS_DEADLOCK fill:#ffcdd2
style CS_RACE fill:#ffcdd2
style RUST_SAFE fill:#c8e6c9
style RUST_FAST fill:#c8e6c9
Incremental Adoption Strategy
Phase 1: Learning and Experimentation (Weeks 1-4)
// Start with command-line tools and utilities
// Example: Log file analyzer
use std::fs;
use std::collections::HashMap;
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
file: String,
#[arg(short, long, default_value = "10")]
top: usize,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let content = fs::read_to_string(&args.file)?;
let mut word_count = HashMap::new();
for line in content.lines() {
for word in line.split_whitespace() {
let word = word.to_lowercase();
*word_count.entry(word).or_insert(0) += 1;
}
}
let mut sorted: Vec<_> = word_count.into_iter().collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1));
for (word, count) in sorted.into_iter().take(args.top) {
println!("{}: {}", word, count);
}
Ok(())
}
Phase 2: Replace Performance-Critical Components (Weeks 5-8)
// Replace CPU-intensive data processing
// Example: Image processing microservice
use image::{DynamicImage, ImageBuffer, Rgb};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use warp::Filter;
#[derive(Serialize, Deserialize)]
struct ProcessingRequest {
image_data: Vec<u8>,
operation: String,
parameters: serde_json::Value,
}
#[derive(Serialize)]
struct ProcessingResponse {
processed_image: Vec<u8>,
processing_time_ms: u64,
}
async fn process_image(request: ProcessingRequest) -> Result<ProcessingResponse, Box<dyn std::error::Error + Send + Sync>> {
let start = std::time::Instant::now();
let img = image::load_from_memory(&request.image_data)?;
let processed = match request.operation.as_str() {
"blur" => {
let radius = request.parameters["radius"].as_f64().unwrap_or(2.0) as f32;
img.blur(radius)
}
"grayscale" => img.grayscale(),
"resize" => {
let width = request.parameters["width"].as_u64().unwrap_or(100) as u32;
let height = request.parameters["height"].as_u64().unwrap_or(100) as u32;
img.resize(width, height, image::imageops::FilterType::Lanczos3)
}
_ => return Err("Unknown operation".into()),
};
let mut buffer = Vec::new();
processed.write_to(&mut std::io::Cursor::new(&mut buffer), image::ImageOutputFormat::Png)?;
Ok(ProcessingResponse {
processed_image: buffer,
processing_time_ms: start.elapsed().as_millis() as u64,
})
}
#[tokio::main]
async fn main() {
let process_route = warp::path("process")
.and(warp::post())
.and(warp::body::json())
.and_then(|req: ProcessingRequest| async move {
match process_image(req).await {
Ok(response) => Ok(warp::reply::json(&response)),
Err(e) => Err(warp::reject::custom(ProcessingError(e.to_string()))),
}
});
warp::serve(process_route)
.run(([127, 0, 0, 1], 3030))
.await;
}
#[derive(Debug)]
struct ProcessingError(String);
impl warp::reject::Reject for ProcessingError {}
Phase 3: New Microservices (Weeks 9-12)
// Build new services from scratch in Rust
// Example: Authentication service
use axum::{
extract::{Query, State},
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Postgres};
use uuid::Uuid;
use bcrypt::{hash, verify, DEFAULT_COST};
#[derive(Clone)]
struct AppState {
db: Pool<Postgres>,
jwt_secret: String,
}
#[derive(Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
#[derive(Deserialize)]
struct LoginRequest {
email: String,
password: String,
}
#[derive(Serialize)]
struct LoginResponse {
token: String,
user_id: Uuid,
}
async fn login(
State(state): State<AppState>,
Json(request): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> {
let user = sqlx::query!(
"SELECT id, password_hash FROM users WHERE email = $1",
request.email
)
.fetch_optional(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let user = user.ok_or(StatusCode::UNAUTHORIZED)?;
if !verify(&request.password, &user.password_hash)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
{
return Err(StatusCode::UNAUTHORIZED);
}
let claims = Claims {
sub: user.id.to_string(),
exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp() as usize,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(state.jwt_secret.as_ref()),
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(LoginResponse {
token,
user_id: user.id,
}))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let database_url = std::env::var("DATABASE_URL")?;
let jwt_secret = std::env::var("JWT_SECRET")?;
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(20)
.connect(&database_url)
.await?;
let app_state = AppState {
db: pool,
jwt_secret,
};
let app = Router::new()
.route("/login", post(login))
.with_state(app_state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
C# to Rust Concept Mapping
Dependency Injection → Constructor Injection + Traits
// C# with DI container
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserService, UserService>();
public class UserService
{
private readonly IUserRepository _repository;
public UserService(IUserRepository repository)
{
_repository = repository;
}
}
// Rust: Constructor injection with traits
pub trait UserRepository {
async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Error>;
async fn save(&self, user: &User) -> Result<(), Error>;
}
pub struct UserService<R>
where
R: UserRepository,
{
repository: R,
}
impl<R> UserService<R>
where
R: UserRepository,
{
pub fn new(repository: R) -> Self {
Self { repository }
}
pub async fn get_user(&self, id: Uuid) -> Result<Option<User>, Error> {
self.repository.find_by_id(id).await
}
}
// Usage
let repository = PostgresUserRepository::new(pool);
let service = UserService::new(repository);
LINQ → Iterator Chains
// C# LINQ
var result = users
.Where(u => u.Age > 18)
.Select(u => u.Name.ToUpper())
.OrderBy(name => name)
.Take(10)
.ToList();
// Rust: Iterator chains (zero-cost!)
let result: Vec<String> = users
.iter()
.filter(|u| u.age > 18)
.map(|u| u.name.to_uppercase())
.collect::<Vec<_>>()
.into_iter()
.sorted()
.take(10)
.collect();
// Or with itertools crate for more LINQ-like operations
use itertools::Itertools;
let result: Vec<String> = users
.iter()
.filter(|u| u.age > 18)
.map(|u| u.name.to_uppercase())
.sorted()
.take(10)
.collect();
Entity Framework → SQLx + Migrations
// C# Entity Framework
public class ApplicationDbContext : DbContext
{
public DbSet<User> Users { get; set; }
}
var user = await context.Users
.Where(u => u.Email == email)
.FirstOrDefaultAsync();
// Rust: SQLx with compile-time checked queries
use sqlx::{PgPool, FromRow};
#[derive(FromRow)]
struct User {
id: Uuid,
email: String,
name: String,
}
// Compile-time checked query
let user = sqlx::query_as!(
User,
"SELECT id, email, name FROM users WHERE email = $1",
email
)
.fetch_optional(&pool)
.await?;
// Or with dynamic queries
let user = sqlx::query_as::<_, User>(
"SELECT id, email, name FROM users WHERE email = $1"
)
.bind(email)
.fetch_optional(&pool)
.await?;
Configuration → Config Crates
// C# Configuration
public class AppSettings
{
public string DatabaseUrl { get; set; }
public int Port { get; set; }
}
var config = builder.Configuration.Get<AppSettings>();
// Rust: Config with serde
use config::{Config, ConfigError, Environment, File};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct AppSettings {
database_url: String,
port: u16,
}
impl AppSettings {
pub fn new() -> Result<Self, ConfigError> {
let s = Config::builder()
.add_source(File::with_name("config/default"))
.add_source(Environment::with_prefix("APP"))
.build()?;
s.try_deserialize()
}
}
// Usage
let settings = AppSettings::new()?;
Team Adoption Timeline
Month 1: Foundation
Week 1-2: Syntax and Ownership
- Basic syntax differences from C#
- Understanding ownership, borrowing, and lifetimes
- Small exercises: CLI tools, file processing
Week 3-4: Error Handling and Types
Result<T, E>vs exceptionsOption<T>vs nullable types- Pattern matching and exhaustive checking
Recommended exercises:
// Week 1-2: File processor
fn process_log_file(path: &str) -> Result<Vec<String>, std::io::Error> {
let content = std::fs::read_to_string(path)?;
let errors: Vec<String> = content
.lines()
.filter(|line| line.contains("ERROR"))
.map(|line| line.to_string())
.collect();
Ok(errors)
}
// Week 3-4: JSON processor with error handling
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)]
struct LogEntry {
timestamp: String,
level: String,
message: String,
}
fn parse_log_entries(json_str: &str) -> Result<Vec<LogEntry>, Box<dyn std::error::Error>> {
let entries: Vec<LogEntry> = serde_json::from_str(json_str)?;
Ok(entries)
}
Month 2: Practical Applications
Week 5-6: Traits and Generics
- Trait system vs interfaces
- Generic constraints and bounds
- Common patterns and idioms
Week 7-8: Async Programming and Concurrency
async/awaitsimilarities and differences- Channels for communication
- Thread safety guarantees
Recommended projects:
// Week 5-6: Generic data processor
trait DataProcessor<T> {
type Output;
type Error;
fn process(&self, data: T) -> Result<Self::Output, Self::Error>;
}
struct JsonProcessor;
impl DataProcessor<&str> for JsonProcessor {
type Output = serde_json::Value;
type Error = serde_json::Error;
fn process(&self, data: &str) -> Result<Self::Output, Self::Error> {
serde_json::from_str(data)
}
}
// Week 7-8: Async web client
async fn fetch_and_process_data(urls: Vec<&str>) -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let tasks: Vec<_> = urls
.into_iter()
.map(|url| {
let client = client.clone();
tokio::spawn(async move {
let response = client.get(url).send().await?;
let text = response.text().await?;
println!("Fetched {} bytes from {}", text.len(), url);
Ok::<(), reqwest::Error>(())
})
})
.collect();
for task in tasks {
task.await??;
}
Ok(())
}
Month 3+: Production Integration
Week 9-12: Real Project Work
- Choose a non-critical component to rewrite
- Implement comprehensive error handling
- Add logging, metrics, and testing
- Performance profiling and optimization
Ongoing: Team Review and Mentoring
- Code reviews focusing on Rust idioms
- Pair programming sessions
- Knowledge sharing sessions
Performance Comparison: Managed vs Native
Real-World Performance Characteristics
| Aspect | C# (.NET) | Rust | Performance Impact |
|---|---|---|---|
| Startup Time | 100-500ms (JIT compilation) | 1-10ms (native binary) | 🚀 50-500x faster |
| Memory Usage | +30-100% (GC overhead + metadata) | Baseline (minimal runtime) | 💾 30-50% less RAM |
| GC Pauses | 1-100ms periodic pauses | Never (no GC) | ⚡ Consistent latency |
| CPU Usage | +10-20% (GC + JIT overhead) | Baseline (direct execution) | 🔋 10-20% better efficiency |
| Binary Size | 30-200MB (with runtime) | 1-20MB (static binary) | 📦 10x smaller deployments |
| Memory Safety | Runtime checks | Compile-time proofs | 🛡️ Zero overhead safety |
| Concurrent Performance | Good (with careful synchronization) | Excellent (fearless concurrency) | 🏃 Superior scalability |
Benchmark Examples
// C# - JSON processing benchmark
public class JsonProcessor
{
public async Task<List<User>> ProcessJsonFile(string path)
{
var json = await File.ReadAllTextAsync(path);
var users = JsonSerializer.Deserialize<List<User>>(json);
return users.Where(u => u.Age > 18)
.OrderBy(u => u.Name)
.Take(1000)
.ToList();
}
}
// Typical performance: ~200ms for 100MB file
// Memory usage: ~500MB peak (GC overhead)
// Binary size: ~80MB (self-contained)
// Rust - Equivalent JSON processing
use serde::{Deserialize, Serialize};
use tokio::fs;
#[derive(Deserialize, Serialize)]
struct User {
name: String,
age: u32,
}
pub async fn process_json_file(path: &str) -> Result<Vec<User>, Box<dyn std::error::Error>> {
let json = fs::read_to_string(path).await?;
let mut users: Vec<User> = serde_json::from_str(&json)?;
users.retain(|u| u.age > 18);
users.sort_by(|a, b| a.name.cmp(&b.name));
users.truncate(1000);
Ok(users)
}
// Typical performance: ~120ms for same 100MB file
// Memory usage: ~200MB peak (no GC overhead)
// Binary size: ~8MB (static binary)
CPU-Intensive Workloads
// C# - Mathematical computation
public class Mandelbrot
{
public static int[,] Generate(int width, int height, int maxIterations)
{
var result = new int[height, width];
Parallel.For(0, height, y =>
{
for (int x = 0; x < width; x++)
{
var c = new Complex(
(x - width / 2.0) * 4.0 / width,
(y - height / 2.0) * 4.0 / height);
result[y, x] = CalculateIterations(c, maxIterations);
}
});
return result;
}
}
// Performance: ~2.3 seconds (8-core machine)
// Memory: ~500MB
// Rust - Same computation with Rayon
use rayon::prelude::*;
use num_complex::Complex;
pub fn generate_mandelbrot(width: usize, height: usize, max_iterations: u32) -> Vec<Vec<u32>> {
(0..height)
.into_par_iter()
.map(|y| {
(0..width)
.map(|x| {
let c = Complex::new(
(x as f64 - width as f64 / 2.0) * 4.0 / width as f64,
(y as f64 - height as f64 / 2.0) * 4.0 / height as f64,
);
calculate_iterations(c, max_iterations)
})
.collect()
})
.collect()
}
// Performance: ~1.1 seconds (same 8-core machine)
// Memory: ~200MB
// 2x faster with 60% less memory usage
When to Choose Each Language
Choose C# when:
- Rapid development is crucial - Rich tooling ecosystem
- Team expertise in .NET - Existing knowledge and skills
- Enterprise integration - Heavy use of Microsoft ecosystem
- Moderate performance requirements - Performance is adequate
- Rich UI applications - WPF, WinUI, Blazor applications
- Prototyping and MVPs - Fast time to market
Choose Rust when:
- Performance is critical - CPU/memory-intensive applications
- Resource constraints matter - Embedded, edge computing, serverless
- Long-running services - Web servers, databases, system services
- System-level programming - OS components, drivers, network tools
- High reliability requirements - Financial systems, safety-critical applications
- Concurrent/parallel workloads - High-throughput data processing
Migration Strategy Decision Tree
graph TD
START["Considering Rust?"]
PERFORMANCE["Is performance critical?"]
TEAM["Team has time to learn?"]
EXISTING["Large existing C# codebase?"]
NEW_PROJECT["New project or component?"]
INCREMENTAL["Incremental adoption:<br/>• CLI tools first<br/>• Performance-critical components<br/>• New microservices"]
FULL_RUST["Full Rust adoption:<br/>• Greenfield projects<br/>• System-level services<br/>• High-performance APIs"]
STAY_CSHARP["Stay with C#:<br/>• Optimize existing code<br/>• Use .NET performance features<br/>• Consider .NET Native"]
START --> PERFORMANCE
PERFORMANCE -->|Yes| TEAM
PERFORMANCE -->|No| STAY_CSHARP
TEAM -->|Yes| EXISTING
TEAM -->|No| STAY_CSHARP
EXISTING -->|Yes| NEW_PROJECT
EXISTING -->|No| FULL_RUST
NEW_PROJECT -->|New| FULL_RUST
NEW_PROJECT -->|Existing| INCREMENTAL
style FULL_RUST fill:#c8e6c9
style INCREMENTAL fill:#fff3e0
style STAY_CSHARP fill:#e3f2fd
Best Practices for C# Developers
1. Mindset Shifts
- From GC to Ownership: Think about who owns data and when it's freed
- From Exceptions to Results: Make error handling explicit and visible
- From Inheritance to Composition: Use traits to compose behavior
- From Null to Option: Make absence of values explicit in the type system
2. Code Organization
// Structure projects like C# solutions
src/
├── main.rs // Program.cs equivalent
├── lib.rs // Library entry point
├── models/ // Like Models/ folder in C#
│ ├── mod.rs
│ ├── user.rs
│ └── product.rs
├── services/ // Like Services/ folder
│ ├── mod.rs
│ ├── user_service.rs
│ └── product_service.rs
├── controllers/ // Like Controllers/ (for web apps)
├── repositories/ // Like Repositories/
└── utils/ // Like Utilities/
3. Error Handling Strategy
// Create a common Result type for your application
pub type AppResult<T> = Result<T, AppError>;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Validation error: {message}")]
Validation { message: String },
#[error("Business logic error: {message}")]
Business { message: String },
}
// Use throughout your application
pub async fn create_user(data: CreateUserRequest) -> AppResult<User> {
validate_user_data(&data)?; // Returns AppError::Validation
let user = repository.create_user(data).await?; // Returns AppError::Database
Ok(user)
}
4. Testing Patterns
// Structure tests like C# unit tests
#[cfg(test)]
mod tests {
use super::*;
use rstest::*; // For parameterized tests like C# [Theory]
#[test]
fn test_basic_functionality() {
// Arrange
let input = "test data";
// Act
let result = process_data(input);
// Assert
assert_eq!(result, "expected output");
}
#[rstest]
#[case(1, 2, 3)]
#[case(5, 5, 10)]
#[case(0, 0, 0)]
fn test_addition(#[case] a: i32, #[case] b: i32, #[case] expected: i32) {
assert_eq!(add(a, b), expected);
}
#[tokio::test] // For async tests
async fn test_async_functionality() {
let result = async_function().await;
assert!(result.is_ok());
}
}
5. Common Mistakes to Avoid
// [ERROR] Don't try to implement inheritance
// Instead of:
// struct Manager : Employee // This doesn't exist in Rust
// [OK] Use composition with traits
trait Employee {
fn get_salary(&self) -> u32;
}
trait Manager: Employee {
fn get_team_size(&self) -> usize;
}
// [ERROR] Don't use unwrap() everywhere (like ignoring exceptions)
let value = might_fail().unwrap(); // Can panic!
// [OK] Handle errors properly
let value = match might_fail() {
Ok(v) => v,
Err(e) => {
log::error!("Operation failed: {}", e);
return Err(e.into());
}
};
// [ERROR] Don't clone everything (like copying objects unnecessarily)
let data = expensive_data.clone(); // Expensive!
// [OK] Use borrowing when possible
let data = &expensive_data; // Just a reference
// [ERROR] Don't use RefCell everywhere (like making everything mutable)
struct Data {
value: RefCell<i32>, // Interior mutability - use sparingly
}
// [OK] Prefer owned or borrowed data
struct Data {
value: i32, // Simple and clear
}
This guide provides C# developers with a comprehensive understanding of how their existing knowledge translates to Rust, highlighting both the similarities and the fundamental differences in approach. The key is understanding that Rust's constraints (like ownership) are designed to prevent entire classes of bugs that are possible in C#, at the cost of some initial complexity.