Rust for C# Developers
If you write C# for a living, good news: a lot of Rust will feel familiar. Rust has generics with constraints, async/await, powerful pattern matching, iterator chains that read a lot like LINQ, and one excellent command-line tool (Cargo) that does what dotnet does. You already know how to think in types, interfaces, and lambdas. That knowledge carries over.
The big shift is about memory and errors. Rust has no garbage collector. Instead, every value has one owner, and the compiler checks ownership and borrowing before your program runs. Rust also has no null (you use Option) and no exceptions (you use Result). These three changes are where most of your learning time will go. This page maps what you know from C# and .NET to Rust, so you can spend that time on the parts that are really new.
If you want a general comparison with C++, Go, and Python instead, see Rust vs Other Languages.
Language concepts: C# to Rust
Section titled “Language concepts: C# to Rust”Use this table as a phrase book. The last column links to the lesson on this site that explains the Rust side.
| C# | Rust | Learn more |
|---|---|---|
class with fields and methods |
struct for the data + an impl block for the methods |
Structs |
interface |
trait |
Traits |
| Abstract class, inheritance | No inheritance. Use composition (a struct inside a struct) + traits with default methods | Traits, Design Patterns |
List<IShape> of different classes |
Vec<Box<dyn Shape>> (a trait object) |
Trait Objects |
| Extension methods | Your own trait, implemented for a type you didn’t write (like String) |
Traits |
Generics with where T : IComparable<T> |
Generics with trait bounds: T: Ord or where T: Ord |
Generics |
null, Nullable<int>, nullable reference types |
Option<T>: either Some(value) or None |
Option |
?? and ?. |
.unwrap_or(...), .map(...), .and_then(...), and ? |
Option |
Exceptions, throw, try/catch |
Result<T, E> + the ? operator |
Result, Custom Errors |
An exception for a real bug (like InvalidOperationException) |
panic! (stops the thread) |
Panic |
IDisposable + using |
Drop, which runs automatically when the owner goes out of scope (RAII) |
Scope, Smart Pointers |
| Garbage collector | Ownership and borrowing, checked at compile time | Ownership, Borrowing |
record |
struct with #[derive(Debug, Clone, PartialEq)] |
Structs, Common Traits |
with expression on a record |
Struct update syntax: Order { total: 5, ..old } |
Structs |
enum (named integers only) |
enum where each variant can carry its own data |
Enums |
switch expressions, is patterns |
match (the compiler checks you covered every case) |
match, Pattern Matching |
Tuples (int, string) |
Tuples (i32, String) |
Tuples |
ref / out parameters |
&mut T, or return a tuple or an Option |
Borrowing |
Lambdas, Func<T>, Action<T>, delegates |
Closures, described by the traits Fn, FnMut, FnOnce |
Closures, Fn, FnMut & FnOnce |
event |
A channel, or a list of boxed closures (callbacks) | Channels, Fn, FnMut & FnOnce |
LINQ: Where, Select, Sum, OrderBy, ToList |
Iterator adapters: filter, map, sum, sort_by (on a Vec), collect |
Iterators, Sorting |
IEnumerable<T> and yield return |
impl Iterator<Item = T>, or your own type that implements Iterator |
Custom Iterators |
async Task<T> / await |
async fn returns a Future; you need a runtime like tokio to run it |
Async/Await |
Task.Run, Thread, lock |
std::thread::spawn or tokio::spawn; Mutex<T> wraps the data it protects |
Concurrency, Shared State |
Properties (get; set;) |
No properties. Use pub fields, or getter and setter methods |
Structs |
string and StringBuilder |
&str (a borrowed view of text) and String (owned, growable) |
Strings |
String interpolation $"Hi {name}" |
format!("Hi {name}") and println!("Hi {name}") |
Output |
var |
let (immutable by default; write let mut to allow changes) |
Variables |
const, readonly |
const, and plain let (already read-only) |
Variables |
static readonly field |
static, or LazyLock for values built at startup |
Variables |
namespace and using directives |
Modules (mod) and use |
Modules |
| Assembly / project | Crate (a library or a binary) | Cargo & Crates |
Attributes like [Serializable] |
Attributes like #[derive(Serialize)] and derive macros |
Macros, Serde & JSON |
| Reflection | Mostly none at runtime. Macros and derive generate code at compile time instead |
Macros |
unsafe and pointers |
unsafe blocks and raw pointers |
Unsafe Rust |
Span<T>, ReadOnlySpan<char> |
Slices: &[T] and &str |
Slices |
ToString() override, IFormattable |
Implement Display (then .to_string() works for free) |
Common Traits |
IEquatable<T>, IComparable<T>, GetHashCode |
PartialEq/Eq, PartialOrd/Ord, Hash (usually derived) |
Common Traits |
implicit / explicit conversion operators |
From/Into (and TryFrom when it can fail) |
Conversions |
Operator overloading (operator +) |
Implement traits from std::ops, like Add |
Advanced Traits |
checked / unchecked arithmetic |
checked_add, wrapping_add, saturating_add |
Integer Overflow |
decimal |
No built-in decimal type; use the rust_decimal crate, or store cents in an integer |
Data Types |
Collections
Section titled “Collections”The standard library covers almost everything in System.Collections.Generic. Names change, ideas don’t.
| C# | Rust | Learn more |
|---|---|---|
List<T> |
Vec<T> |
Vectors |
T[] (fixed length) |
[T; N] when the length is known at compile time, otherwise Vec<T> |
Arrays |
Dictionary<K, V> |
HashMap<K, V> |
HashMaps |
SortedDictionary<K, V> |
BTreeMap<K, V> (keys stay sorted) |
HashMaps |
HashSet<T> |
HashSet<T> |
HashSet |
SortedSet<T> |
BTreeSet<T> |
HashSet |
Queue<T> |
VecDeque<T> (push_back / pop_front) |
VecDeque & BinaryHeap |
Stack<T> |
Vec<T> with push / pop |
Vectors |
PriorityQueue<TElement, TPriority> |
BinaryHeap<T> (largest first; wrap in Reverse for smallest first) |
VecDeque & BinaryHeap |
LinkedList<T> |
LinkedList<T> exists, but you rarely need it; VecDeque is usually faster |
VecDeque & BinaryHeap |
ConcurrentDictionary<K, V> |
Mutex<HashMap<K, V>>, or the dashmap crate |
Shared State |
One thing to watch: like Dictionary, a Rust HashMap has no fixed order. If you print its contents, sort them first or use a BTreeMap.
Tooling: dotnet to cargo
Section titled “Tooling: dotnet to cargo”This is the part most C# developers enjoy right away. Cargo does the jobs of the dotnet CLI, MSBuild, and NuGet in one tool.
| .NET | Rust | Notes |
|---|---|---|
dotnet CLI |
cargo |
Builds, runs, tests, adds packages, publishes. See Cargo & Crates |
.csproj |
Cargo.toml |
Short and hand-editable |
packages.lock.json |
Cargo.lock |
Created for you |
.sln with several projects |
A Cargo workspace | One Cargo.toml at the top lists the member crates |
| NuGet / nuget.org | crates.io | Every published crate’s API docs are on docs.rs |
dotnet add package Serilog |
cargo add tracing |
|
dotnet new console |
cargo new my_app (cargo new --lib for a library) |
|
dotnet run / build / test |
cargo run / cargo build / cargo test |
|
-c Release |
--release |
Debug builds are much slower at runtime; benchmark in release |
dotnet watch |
cargo watch or bacon (installed with cargo install) |
|
dotnet tool install -g |
cargo install |
|
global.json (SDK version) |
rustup and a rust-toolchain.toml file |
|
| Roslyn analyzers | cargo clippy |
Hundreds of helpful lints, official |
dotnet format |
cargo fmt |
One standard style for everyone |
| Visual Studio / Rider | VS Code + rust-analyzer, or JetBrains RustRover | rust-analyzer gives completion, go-to-definition, and inline errors |
XML doc comments /// |
/// doc comments in Markdown + cargo doc |
Code examples in docs are run as tests. See Documentation |
| xUnit / NUnit / MSTest | Built-in #[test] and cargo test (cargo nextest is a popular faster runner) |
See Testing |
| Moq | mockall, or hand-written fakes that implement your trait |
|
| Coverlet (code coverage) | cargo llvm-cov or cargo tarpaulin |
|
| BenchmarkDotNet | criterion |
See Performance |
| dotnet-trace, profilers | cargo flamegraph, perf, or any native profiler |
Here is a small Cargo.toml, so you can compare it with a .csproj:
[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }Libraries and frameworks: “What do we use instead of…?”
Section titled “Libraries and frameworks: “What do we use instead of…?””This is the question every .NET team asks first. “We use Entity Framework. What do we have in Rust?” Here are the closest equivalents. Rust has no single company behind a “default stack” the way .NET has Microsoft, so you often pick between two or three good community crates.
Data access
Section titled “Data access”| .NET | Rust | Notes |
|---|---|---|
| Entity Framework Core | Diesel: ORM and query builder; your queries are type-checked at compile time. Migrations with diesel_cli |
Mature (2.x), synchronous by default (diesel-async exists) |
| Entity Framework Core (async, entity-style) | SeaORM: async ORM with entities, relations, and migrations (sea-orm-cli) |
Feels closest to EF; built on top of sqlx |
| Dapper | sqlx: you write plain SQL, and the query! macro checks it against your real database at compile time. Migrations with sqlx-cli |
Async, very popular, not an ORM |
| ADO.NET | sqlx, tokio-postgres, rusqlite (SQLite) |
Lower-level drivers |
| StackExchange.Redis | redis |
If you liked EF, try SeaORM first. If you liked Dapper and writing your own SQL, try sqlx. If you want the strongest compile-time guarantees, try Diesel. Rust Databases shows all three with working code.
Web and networking
Section titled “Web and networking”| .NET | Rust | Notes |
|---|---|---|
| ASP.NET Core Web API | axum (we build one in Project: Web API), actix-web, Rocket | axum and actix-web are widely used in production |
| Minimal APIs | axum: routes are plain async fn handlers |
The style is very close |
| Middleware pipeline | tower layers and the tower-http crate (CORS, compression, tracing, timeouts) |
axum uses tower directly |
| Built-in dependency injection | State in axum, plain constructors, and traits |
See the DI row below |
| Swashbuckle / OpenAPI | utoipa |
|
| SignalR | WebSockets in axum (the ws feature), or tokio-tungstenite |
No SignalR-style protocol layer built in |
| gRPC (Grpc.AspNetCore) | tonic (with prost for protobuf) |
|
HttpClient |
reqwest |
Async by default, with an optional blocking client |
| JWT bearer auth | jsonwebtoken |
Everyday building blocks
Section titled “Everyday building blocks”| .NET | Rust | Notes |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde + serde_json |
The standard; also does TOML, YAML, and more. See Serde & JSON |
ILogger / Serilog |
tracing (+ tracing-subscriber), or log + env_logger |
Structured logging with spans. See Logging |
IConfiguration / appsettings.json |
config or figment |
Layer files and environment variables, then deserialize with serde |
DI container (IServiceCollection) |
Usually nothing: pass dependencies into constructors, use traits for things you want to swap in tests | shaku exists, but few projects use a container |
| AutoMapper | Write From / Into implementations |
Explicit and checked by the compiler. See Conversions |
| FluentValidation / DataAnnotations | validator or garde (derive-based validation) |
|
| Polly (retries) | backon, or tower’s retry middleware |
|
| MediatR | No direct equivalent | Plain traits, or channels between tasks |
| Hangfire / background jobs | tokio::spawn for in-process tasks; apalis for persistent job queues |
|
CancellationToken |
Drop the future, or use CancellationToken from tokio-util |
Desktop, games, and more
Section titled “Desktop, games, and more”| .NET | Rust | Notes |
|---|---|---|
| Blazor WebAssembly | Leptos, Yew, Dioxus |
All before version 1.0; APIs still change. See WebAssembly |
| WPF / WinForms / MAUI | Tauri (web UI + Rust backend), Slint, egui, Iced |
Tauri 2 and Slint 1.x are the most stable. No first-party GUI toolkit |
| Unity | Bevy, or Rust inside Godot with the godot crate |
Bevy is pre-1.0 and breaks its API between releases |
| ML.NET | candle, burn, linfa |
Much younger than Python’s ML tools |
| Azure SDK for .NET | The official Azure SDK for Rust: azure_core, azure_identity, azure_storage_blob, … |
Covers fewer services than the .NET SDK |
Side by side: a class becomes a struct + impl
Section titled “Side by side: a class becomes a struct + impl”Let’s look at real code. Here is a small bank account in C#:
var account = new BankAccount("Ada", 100);account.Deposit(50);Console.WriteLine($"{account.Owner} has {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;}And in Rust:
struct BankAccount { owner: String, balance: i64,}
impl BankAccount { // `new` is just a convention, not a keyword. fn new(owner: &str, balance: i64) -> Self { BankAccount { owner: owner.to_string(), balance } }
// `&mut self`: this method changes the account. fn deposit(&mut self, amount: i64) { self.balance += amount; }
// `&self`: this method only reads. fn balance(&self) -> i64 { self.balance }}
fn main() { let mut account = BankAccount::new("Ada", 100); account.deposit(50); println!("{} has {}", account.owner, account.balance());}Output:
Ada has 150Data and behavior live in two separate blocks. There are no constructors, only functions that return Self. Every method says up front whether it reads (&self) or changes (&mut self) the value. And because deposit changes the account, the variable must be let mut. In C#, any method can change any field; in Rust, changes are visible in the signature.
Side by side: an interface becomes a trait
Section titled “Side by side: an interface becomes a trait”In C#, you might have an IShape interface with two classes and a 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, the interface is a trait, and each struct implements it in its own 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> plays the role of "a reference to some 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()); }}Output:
3.146.00In C#, every interface call goes through a virtual call. In Rust you choose: Box<dyn Shape> uses a virtual call (see Trait Objects), while a generic function like fn print_area<T: Shape>(s: &T) is compiled separately for each type, with no virtual call at all. A nice extra: you can implement your own trait for types you didn’t write, even String or i32. That’s how Rust does what extension methods do in C#.
Side by side: null becomes Option
Section titled “Side by side: null becomes Option”C# with nullable reference types warns you about null, but null still exists at runtime:
var email = FindEmail(2);if (email is not null) Console.WriteLine(email.ToUpper());else Console.WriteLine("no email on file");
Console.WriteLine(FindEmail(2) ?? "unknown");In Rust, a value that might be missing has the type Option<T>. You can’t use it as a plain value until you’ve handled the None case:
fn find_email(user_id: u32) -> Option<String> { if user_id == 1 { } else { None }}
fn main() { match find_email(1) { Some(email) => println!("{}", email.to_uppercase()), None => println!("no email on file"), }
// `if let` is the short form when you only care about one case. if let Some(email) = find_email(2) { println!("found {email}"); } else { println!("no email on file"); }
// unwrap_or works like C#'s ?? operator. let email = find_email(2).unwrap_or("unknown".to_string()); println!("{email}");}Output:
no email on fileunknownThere is no NullReferenceException in Rust, because there is no null to dereference. The compiler simply won’t let you call .to_uppercase() on an Option<String>. Learn the helper methods on the Option page, and the short forms on if let.
Side by side: try/catch becomes Result and ?
Section titled “Side by side: try/catch becomes Result and ?”In C#, parsing can throw, and you catch the exception somewhere up the call stack:
static int TotalSeats(string a, string b) => int.Parse(a) + int.Parse(b);
try{ Console.WriteLine(TotalSeats("12", "30")); Console.WriteLine(TotalSeats("12", "thirty"));}catch (FormatException e){ Console.WriteLine($"Error: {e.Message}");}In Rust, a function that can fail says so in its return type. The ? operator means “if this failed, return the error to my caller right now”. It does the job of letting an exception bubble up, but you can see it in the 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", "thirty")] { match total_seats(a, b) { Ok(total) => println!("Total: {total}"), Err(e) => println!("Error: {e}"), } }}Output:
Total: 42Error: invalid digit found in stringThe big difference: in C#, any method might throw, and the signature doesn’t tell you. In Rust, Result is part of the signature, so callers can’t forget to handle it. For your own error types, see Custom Errors. In applications, the anyhow crate gives you something close to “catch any exception” (see anyhow & thiserror).
Side by side: LINQ becomes an iterator chain
Section titled “Side by side: LINQ becomes an iterator chain”A typical LINQ query:
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($"Revenue: {orders.Sum(o => o.Total)}");
record Order(string Customer, int Total);The Rust version uses iterator adapters and 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(...): sorting happens on a Vec, not on the 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!("Revenue: {revenue}");}Output:
Linus, Ken, AdaRevenue: 550Rust iterators are lazy, just like LINQ’s deferred execution: nothing runs until you call something like collect(), sum(), or a for loop. collect() is your ToList(). Unlike LINQ, iterator chains are usually compiled down to the same machine code as a hand-written loop, so you don’t pay for the nice style. Sorting is covered on the Sorting page.
Side by side: using/IDisposable becomes Drop
Section titled “Side by side: using/IDisposable becomes Drop”In C#, you must remember using to release a resource on time:
using (var conn = new Connection("orders-db")){ Console.WriteLine($"querying {conn.Name}");}Console.WriteLine("after the block");
class Connection : IDisposable{ public string Name { get; } public Connection(string name) => Name = name; public void Dispose() => Console.WriteLine($"closing connection to {Name}");}In Rust, cleanup is automatic. When the owner of a value goes out of scope, Rust calls its Drop implementation. There is nothing to forget:
struct Connection { name: String,}
impl Drop for Connection { fn drop(&mut self) { println!("closing connection to {}", self.name); }}
fn main() { { let conn = Connection { name: "orders-db".to_string() }; println!("querying {}", conn.name); } // `conn` goes out of scope here, so drop() runs.
println!("after the block");}Output:
querying orders-dbclosing connection to orders-dbafter the blockThis is called RAII, and it’s how Rust handles everything: files, sockets, locks, and memory itself. A file closes when its File value is dropped. A Mutex unlocks when its guard is dropped. There is no finalizer and no GC pause; cleanup happens at a predictable moment.
Things that will trip you up as a C# developer
Section titled “Things that will trip you up as a C# developer”Everyone coming from C# hits the same bumps. Knowing them in advance helps.
-
Assignment moves, it doesn’t copy a reference. In C#,
var b = a;for a class gives you two references to one object. In Rust,let b = a;moves ownership of aStringorVecintob, andacan’t be used anymore:fn main() {let owner = String::from("Ada");let copy = owner;println!("{owner} and {copy}");}error[E0382]: borrow of moved value: `owner`--> src/main.rs:4:16|2 | let owner = String::from("Ada");| ----- move occurs because `owner` has type `String`, which does not implement the `Copy` trait3 | let copy = owner;| ----- value moved here4 | println!("{owner} and {copy}");| ^^^^^ value borrowed here after moveThe fix is to borrow (
let copy = &owner;) or to clone on purpose (owner.clone()). Read Ownership first; it explains this rule. -
The borrow checker. You can have many readers (
&T) or one writer (&mut T) at a time, never both. Patterns that are normal in C#, like objects that point back to their parent, or many objects changing one shared object, need smart pointers or a different design. Expect a few frustrating weeks. It gets much easier. -
Stringvs&str. C# has onestringtype. Rust has two:Stringowns its text,&strborrows it. Functions usually take&strand structs usually storeString. See Strings. -
No inheritance. You can’t write a base class with shared fields and override methods. Use traits (with default methods) for shared behavior, and put shared data in a struct that other structs contain.
-
No exceptions. Errors are values. You’ll write
?a lot, and yourmaincan returnResulttoo. Calling.unwrap()is fine while learning, but it’s the Rust version of “ignore the exception and crash”. -
No null. At first,
Optioneverywhere feels like extra work. After a while you’ll notice that you’ve stopped getting null-reference bugs. -
Explicit clones. Rust never copies a heap value behind your back. If you see
.clone(), a real copy is made. That’s a feature: the cost is visible. -
Compile times. A clean build of a project with many crates takes longer than a .NET build. Incremental builds and
cargo check(which skips code generation) are much faster; usecargo checkwhile you edit. -
Async needs a runtime. In .NET, the runtime and thread pool are always there. In Rust, an
async fndoes nothing until an executor runs it, so you add tokio and put#[tokio::main]onmain. See Async/Await.
A learning path for C# developers
Section titled “A learning path for C# developers”You don’t need to read this site in order. As a C# developer, this route skips what you already know and spends time where Rust is different:
- Get Started: install Rust and run
cargo new. - Variables and Data Types: skim these, mostly to learn
let mutand the number types. - Ownership, then Borrowing, then Slices: the most important pages for you. Take your time.
- Strings:
Stringvs&str. - Structs and Enums, then match and Pattern Matching.
- Option and Result, then Custom Errors.
- Traits, Generics, Common Traits, and Trait Objects.
- Closures and Iterators: your LINQ.
- Vectors, HashMaps, and VecDeque & BinaryHeap.
- Modules, Cargo & Crates, and Testing.
- Lifetimes and Smart Pointers.
- Concurrency, Shared State, and Async/Await.
- Serde & JSON and Logging, then build the Web API project.
Practice
Section titled “Practice”Both exercises give you a C# snippet. Port it to Rust.
1. Port a record and a LINQ query
Section titled “1. Port a record and a LINQ query”Port this C# code. It should print the names of products that are in stock and cost less than 20. Hint: make a struct with #[derive(Debug, Clone, PartialEq)], then use filter, map, and collect into a Vec<&str>. Vec<&str> has a .join(", ") method.
var products = new List<Product>{ new("Notebook", 5, true), new("Headphones", 45, true), new("Pen", 2, false), new("Mug", 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);Show solution
#[derive(Debug, Clone, PartialEq)]struct Product { name: String, price: u32, in_stock: bool,}
fn product(name: &str, price: u32, in_stock: bool) -> Product { Product { name: name.to_string(), price, in_stock }}
fn main() { let products = vec![ product("Notebook", 5, true), product("Headphones", 45, true), product("Pen", 2, false), product("Mug", 12, true), ];
let cheap: Vec<&str> = products .iter() .filter(|p| p.in_stock && p.price < 20) .map(|p| p.name.as_str()) .collect();
println!("{}", cheap.join(", "));}Output:
Notebook, MugA small helper function plays the role of the record’s positional constructor. The derives give you what a C# record gives you for free: printing with {:?}, copying with .clone(), and comparing with ==.
2. Port a Dictionary with exceptions
Section titled “2. Port a Dictionary with exceptions”This C# code takes one item out of a stock dictionary and throws when something is wrong. Port it so that take returns a Result<u32, String> instead of throwing. Hint: stock.get_mut(item) returns an Option<&mut u32>. Turn it into a Result with .ok_or(...), then use ?.
var stock = new Dictionary<string, int> { ["apples"] = 3, ["pears"] = 0 };
int Take(string item){ if (!stock.TryGetValue(item, out var count)) throw new KeyNotFoundException($"no item called {item}"); if (count == 0) throw new InvalidOperationException($"{item} is out of stock"); stock[item] = count - 1; return count - 1;}
foreach (var item in new[] { "apples", "pears", "kiwis" }){ try { Console.WriteLine($"took one {item}, {Take(item)} left"); } catch (Exception e) { Console.WriteLine($"error: {e.Message}"); }}Show solution
use std::collections::HashMap;
fn take(stock: &mut HashMap<String, u32>, item: &str) -> Result<u32, String> { let count = stock .get_mut(item) .ok_or(format!("no item called {item}"))?;
if *count == 0 { return Err(format!("{item} is out of stock")); }
*count -= 1; Ok(*count)}
fn main() { let mut stock = HashMap::new(); stock.insert("apples".to_string(), 3); stock.insert("pears".to_string(), 0);
for item in ["apples", "pears", "kiwis"] { match take(&mut stock, item) { Ok(left) => println!("took one {item}, {left} left"), Err(e) => println!("error: {e}"), } }}Output:
took one apples, 2 lefterror: pears is out of stockerror: no item called kiwisTryGetValue with an out parameter becomes get_mut, which returns an Option. The two throws become Err values, and the try/catch becomes a match. The &mut stock in the call makes it clear that take changes the dictionary.
- Much of C# carries over: generics, async/await, lambdas, pattern matching, and LINQ-style iterators all have close Rust equivalents.
- The big shifts are ownership instead of a garbage collector,
Optioninstead of null, andResult+?instead of exceptions. - Classes become a
struct+impl, interfaces become traits, and inheritance becomes composition plus traits.using/IDisposablebecomes automaticDrop. - Cargo replaces the
dotnetCLI, MSBuild, and NuGet; clippy and rustfmt replace analyzers anddotnet format; testing is built in. - For EF Core look at SeaORM or Diesel, for Dapper look at sqlx, and for ASP.NET Core look at axum or actix-web. GUI, games, and ML options are younger.
Quick check
Section titled “Quick check”Quick check
1. What does Rust use instead of null?
2. Your team uses Dapper and likes writing SQL by hand. Which Rust crate is the closest match?
3. What is the closest Rust equivalent of C#'s using block with IDisposable?
4. In C#, var b = a; with a class copies a reference. What does let b = a; do with a String in Rust?
Score: 0 / 4
Read more
Section titled “Read more”- Rust for C#/.NET Developers — Microsoft: Microsoft’s own guide, with many more side-by-side C# and Rust examples.
- The Rust Programming Language: the official book. Chapters 4 (ownership), 9 (errors), and 10 (generics and traits) matter most for C# developers.
- Getting Started with Diesel and SeaORM documentation: the two main ORMs, if you’re looking for Entity Framework.
- sqlx on docs.rs: the Dapper-style choice, with compile-time checked SQL.
- Are We Web Yet? and Are We GUI Yet?: community overviews of Rust’s web and desktop GUI libraries.