Hello World & Output
Hello, World
Console.WriteLine("Hello, World!"); println!("Hello, World!"); Rust's
println! is a macro — the ! signals that. Macros are not function calls; they expand at compile time. Execution starts at fn main(), which the runner supplies when a snippet does not include one.String interpolation / format
var name = "Rustacean";
var score = 42;
Console.WriteLine($"Hello, {name}! Score: {score}"); let name = "Rustacean";
let score = 42;
println!("Hello, {name}! Score: {score}"); Rust 2021 supports captured identifiers directly in the format string —
"{name}" pulls the variable from the local scope, exactly like C#'s $"{name}". For expressions use positional syntax: println!("{}", some_expr()).Debug-format output
var numbers = new List<int> { 1, 2, 3 };
Console.WriteLine(string.Join(", ", numbers)); let numbers = vec![1, 2, 3];
println!("{numbers:?}"); The
{:?} format specifier uses a type's Debug trait, printing a developer-readable representation. Most standard types implement it automatically. Use {:#?} for pretty-printed (indented) output. Display ({}) requires a hand-implemented fmt::Display trait.Variables & Types
Immutable by default
int count = 5;
count = 10; // C# vars are mutable by default
Console.WriteLine(count); let mut count = 5; // explicit mut to allow mutation
count = 10;
println!("{count}"); Rust bindings are immutable by default — omitting
mut produces a compile error if you attempt to reassign. This is the opposite of C#, where mutability is the default and readonly is the opt-out. The compiler enforces the constraint at zero cost.Type inference
var greeting = "hello"; // string
var count = 42; // int
var active = true; // bool
Console.WriteLine($"{greeting} {count} {active}"); let greeting = "hello"; // &str
let count = 42; // i32
let active = true; // bool
println!("{greeting} {count} {active}"); Both languages infer types from the right-hand side. Rust's inference is global — it can look forward in the function to determine a type, so you rarely need annotations. An integer literal defaults to
i32 and a float literal to f64 unless context demands otherwise.Explicit type annotations
int count = 42;
long big = 9_000_000_000L;
double ratio = 3.14;
Console.WriteLine($"{count} {big} {ratio}"); let count: i32 = 42;
let big: i64 = 9_000_000_000;
let ratio: f64 = 3.14;
println!("{count} {big} {ratio}"); Rust has a richer numeric type menu: signed integers
i8 through i128, unsigned u8 through u128, and isize/usize for pointer-sized integers. The C# int/long/double defaults map to i32/i64/f64.Variable shadowing
// C# allows shadowing only in inner scopes, not the same scope
var text = "42";
int count = int.Parse(text); // must use a new name or inner scope
Console.WriteLine(count); let text = "42";
let text = text.parse::<i32>().unwrap(); // re-declares text as i32
println!("{text}"); Rust's
let creates a new binding; it does not mutate. This means you can re-declare a variable with a different type in the same scope — the original is shadowed, not overwritten. This idiom is common for parsing-then-using the same logical value without inventing a second name.Constants
const double GRAVITY = 9.8;
const int MAX_PLAYERS = 8;
Console.WriteLine($"Gravity: {GRAVITY}, Max: {MAX_PLAYERS}"); const GRAVITY: f64 = 9.8;
const MAX_PLAYERS: u32 = 8;
println!("Gravity: {GRAVITY}, Max: {MAX_PLAYERS}"); Rust
const requires an explicit type annotation and is inlined at every use site — no stack slot, no address. Unlike C#'s const, Rust constants can appear at module level outside functions. static is the Rust equivalent of a global with a fixed address.Ownership & Borrowing
Move semantics (ownership transfer)
// C#: variables hold references; the GC manages the heap object.
// Both 'original' and 'alias' point to the same List.
var original = new List<int> { 1, 2, 3 };
var alias = original; // both see the same object
alias.Add(4);
Console.WriteLine(original.Count); // 4 — shared! let original = vec![1, 2, 3];
let moved = original; // ownership transferred — 'original' is gone
// println!("{:?}", original); // ERROR: value moved
println!("{:?}", moved); When a Rust value is assigned or passed to a function, ownership moves — the original binding becomes invalid. C# passes references (and the GC keeps the object alive); Rust has no GC, so the compiler tracks exactly one owner at a time and drops the value when the owner goes out of scope.
Borrowing with references
static void Print(List<int> items) {
Console.WriteLine(string.Join(", ", items));
}
var numbers = new List<int> { 1, 2, 3 };
Print(numbers); // passes reference (no move in C#)
Console.WriteLine(numbers.Count); // still accessible fn print_items(items: &Vec<i32>) { // borrow, not take ownership
println!("{:?}", items);
}
fn main() {
let numbers = vec![1, 2, 3];
print_items(&numbers); // lend with &
println!("{}", numbers.len()); // still accessible
} Passing
&value lends a reference without transferring ownership — the original binding remains usable. A shared reference (&T) allows many readers simultaneously; a mutable reference (&mut T) allows one writer and zero readers. This "borrow checker" rule prevents data races at compile time.Mutable borrowing
static void AppendItem(List<int> items, int value) {
items.Add(value); // C# always allows mutation via reference
}
var numbers = new List<int> { 1, 2, 3 };
AppendItem(numbers, 4);
Console.WriteLine(numbers.Count); // 4 fn append_item(items: &mut Vec<i32>, value: i32) {
items.push(value);
}
fn main() {
let mut numbers = vec![1, 2, 3];
append_item(&mut numbers, 4); // explicit mutable borrow
println!("{}", numbers.len()); // 4
} A mutable reference (
&mut T) is the only way to mutate a borrowed value. Rust enforces that exactly one mutable reference to a value exists at a time and no shared references exist simultaneously — making data races impossible at compile time. C# references are always mutable by default.Explicit cloning (deep copy)
var original = new List<int> { 1, 2, 3 };
var copy = new List<int>(original); // explicit copy
copy.Add(4);
Console.WriteLine(original.Count); // 3 — independent let original = vec![1, 2, 3];
let mut copy = original.clone(); // explicit deep copy
copy.push(4);
println!("{}", original.len()); // 3 — independent Rust's
.clone() is the explicit deep-copy mechanism — it is never implicit. For types that are cheap to copy (integers, booleans, small fixed-size arrays) Rust's Copy trait makes assignment copy automatically, like C# value types. Vec, String, and most heap types do not implement Copy.Strings
String types: <code>String</code> vs <code>&str</code>
// C# has one string type; all strings are heap-allocated objects.
string literal = "hello";
string owned = new string("hello");
Console.WriteLine(literal == owned); // true let literal: &str = "hello"; // borrowed string slice — lives in static memory
let owned: String = String::from("hello"); // heap-allocated, growable
println!("{}", literal == owned); // true — Rust compares contents Rust distinguishes
&str (a borrowed view into string data — often a string literal baked into the binary) from String (an owned, heap-allocated, growable buffer). Functions that only need to read a string take &str; functions that build or own a string use String. A String can be borrowed as &str with &my_string.Building strings
var builder = new StringBuilder();
builder.Append("Hello");
builder.Append(", ");
builder.Append("World!");
Console.WriteLine(builder.ToString()); let mut text = String::new();
text.push_str("Hello");
text.push_str(", ");
text.push_str("World!");
println!("{text}"); String::new() creates an empty, growable string buffer — the role of StringBuilder in C#. push_str(&str) appends a string slice and push(char) appends a single character. The format! macro builds a new String from a template without mutating anything.String methods
string message = "Hello, World!";
Console.WriteLine(message.ToUpper());
Console.WriteLine(message.Contains("World"));
Console.WriteLine(message.Replace("World", "Rust"));
Console.WriteLine(message.Length); let message = "Hello, World!";
println!("{}", message.to_uppercase());
println!("{}", message.contains("World"));
println!("{}", message.replace("World", "Rust"));
println!("{}", message.len()); Rust's string methods follow
snake_case naming. Note that .len() returns the byte count, not the character count — for Unicode use .chars().count(). String methods on &str return new Strings or &str slices, never mutating in place.Split and join
string csv = "alpha,beta,gamma";
string[] parts = csv.Split(',');
Console.WriteLine(string.Join(" | ", parts)); let csv = "alpha,beta,gamma";
let parts: Vec<&str> = csv.split(',').collect();
println!("{}", parts.join(" | ")); Rust's
.split() returns a lazy iterator, not an array — you call .collect() to materialize it into a Vec. The type annotation Vec<&str> tells the compiler what collection to build. Joining uses .join(separator) on a slice, equivalent to string.Join in C#.Trim and parse
string input = " 42 ";
int value = int.Parse(input.Trim());
Console.WriteLine(value + 1); let input = " 42 ";
let value: i32 = input.trim().parse().unwrap();
println!("{}", value + 1); .parse() returns a Result<T, E> — parsing can fail. .unwrap() extracts the value or panics on error. For production code, handle the error explicitly with match or the ? operator instead of unwrapping. .trim() on a &str returns a &str slice, not a new allocation.Numbers & Math
Integer arithmetic
int total = 10 + 3;
int difference = 10 - 3;
int product = 10 * 3;
int quotient = 10 / 3; // truncates toward zero: 3
int remainder = 10 % 3;
Console.WriteLine($"{total} {difference} {product} {quotient} {remainder}"); let total: i32 = 10 + 3;
let difference: i32 = 10 - 3;
let product: i32 = 10 * 3;
let quotient: i32 = 10 / 3; // truncates toward zero: 3
let remainder: i32 = 10 % 3;
println!("{total} {difference} {product} {quotient} {remainder}"); Integer division truncates toward zero in both languages. In Rust's debug builds, integer overflow panics rather than wrapping silently — a deliberate safety choice. In release builds it wraps. Use
checked_add(), saturating_add(), or wrapping_add() for explicit overflow semantics.Float arithmetic
double ratio = 98.6 / 2.0;
Console.WriteLine(ratio); let ratio: f64 = 98.6 / 2.0;
println!("{ratio}"); Rust's
f64 is the 64-bit double, equivalent to C#'s double. f32 maps to C#'s float. Unlike C#, Rust does not automatically promote integer expressions to floats — 10 / 3 is always integer division; write 10.0 / 3.0 for a float result.Math methods
Console.WriteLine(Math.Abs(-7));
Console.WriteLine(Math.Sqrt(16.0));
Console.WriteLine(Math.Min(3, 8));
Console.WriteLine(Math.Pow(2.0, 10.0)); println!("{}", (-7_i32).abs());
println!("{}", 16.0_f64.sqrt());
println!("{}", 3_i32.min(8));
println!("{}", 2.0_f64.powi(10)); In Rust, math operations are methods called directly on numeric values — no
Math. class prefix required. Integer methods: .abs(), .min(), .max(), .pow(). Float methods: .sqrt(), .sin(), .cos(), .powi() (integer exponent), .powf() (float exponent).Numeric type casting
double ratio = 3.9;
int truncated = (int)ratio; // explicit cast
long extended = truncated; // implicit widening
Console.WriteLine($"{truncated} {extended}"); let ratio: f64 = 3.9;
let truncated: i32 = ratio as i32; // as keyword — truncates toward zero
let extended: i64 = truncated as i64; // widening also uses 'as'
println!("{truncated} {extended}"); Rust uses the
as keyword for numeric casts — no implicit conversions exist at all, not even widening. f64 as i32 truncates toward zero (same as C#'s explicit cast), while saturating at the integer bounds in cases of infinity or out-of-range values (since Rust 1.45). Use .into() for safe, infallible widening.Vectors & Arrays
Vector creation
var numbers = new List<int> { 10, 20, 30 };
Console.WriteLine(numbers[0]);
Console.WriteLine(numbers.Count); let numbers = vec![10, 20, 30];
println!("{}", numbers[0]);
println!("{}", numbers.len()); Vec<T> is Rust's equivalent of List<T> — a heap-allocated, growable sequence. The vec![] macro is the idiomatic literal syntax. .len() returns the number of elements. Index access numbers[i] panics on out-of-bounds; use .get(i) to get an Option<&T> instead.Adding and removing elements
var items = new List<string> { "alpha", "beta" };
items.Add("gamma");
items.Remove("beta");
Console.WriteLine(items.Count); let mut items = vec!["alpha", "beta"];
items.push("gamma");
items.retain(|&item| item != "beta"); // keep elements that satisfy the predicate
println!("{}", items.len()); .push(value) appends to the end (like List.Add). .pop() removes and returns the last element as Option<T>. Rust has no direct .Remove(value) — use .retain(|item| item != &value) to keep all elements that satisfy the predicate, or .remove(index) to remove by position.Filter and map (LINQ vs iterators)
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var evens = numbers.Where(n => n % 2 == 0).ToList();
var doubled = numbers.Select(n => n * 2).ToList();
Console.WriteLine(string.Join(", ", evens));
Console.WriteLine(string.Join(", ", doubled)); let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<i32> = numbers.iter().filter(|&&n| n % 2 == 0).copied().collect();
let doubled: Vec<i32> = numbers.iter().map(|&n| n * 2).collect();
println!("{:?}", evens);
println!("{:?}", doubled); Rust's iterator methods (
.filter(), .map(), etc.) are lazy — they do no work until consumed by a terminating call like .collect(), .sum(), or .for_each(). This matches LINQ's deferred-execution model. .copied() converts &i32 references back to i32 values in the output.Fixed-size arrays
int[] scores = { 85, 92, 78, 95 };
Console.WriteLine(scores[0]);
Console.WriteLine(scores.Length); let scores: [i32; 4] = [85, 92, 78, 95];
println!("{}", scores[0]);
println!("{}", scores.len()); Rust arrays (
[T; N]) are stack-allocated with a compile-time fixed size — more like C# Span<T> on the stack than a List<T>. The size is part of the type: [i32; 4] and [i32; 5] are different types. Use Vec<T> when the size is dynamic or unknown at compile time.HashMaps
HashMap creation and access
var scores = new Dictionary<string, int> {
{ "Alice", 95 },
{ "Bob", 87 },
};
Console.WriteLine(scores["Alice"]); use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert("Alice", 95);
scores.insert("Bob", 87);
println!("{}", scores["Alice"]); Rust's
HashMap<K, V> lives in std::collections. There is no literal syntax — entries are added with .insert(key, value). Index access scores["Alice"] panics if the key is absent; use .get("Alice") to receive Option<&V> instead.Get with default value
var config = new Dictionary<string, string> { { "theme", "dark" } };
Console.WriteLine(config.GetValueOrDefault("theme", "light"));
Console.WriteLine(config.GetValueOrDefault("volume", "50")); use std::collections::HashMap;
let mut config = HashMap::new();
config.insert("theme", "dark");
println!("{}", config.get("theme").copied().unwrap_or("light"));
println!("{}", config.get("volume").copied().unwrap_or("50")); .get(key) returns Option<&V>. Chain .copied() (for Copy types like &str) and .unwrap_or(default) to replicate GetValueOrDefault. The entry API — config.entry("volume").or_insert("50") — inserts the default and returns a mutable reference in one step.Entry API (insert-or-update)
var wordCounts = new Dictionary<string, int>();
var words = new[] { "apple", "banana", "apple", "cherry", "banana", "apple" };
foreach (var word in words)
wordCounts[word] = wordCounts.GetValueOrDefault(word) + 1;
Console.WriteLine(wordCounts["apple"]); use std::collections::HashMap;
let words = ["apple", "banana", "apple", "cherry", "banana", "apple"];
let mut word_counts: HashMap<&str, i32> = HashMap::new();
for word in &words {
*word_counts.entry(word).or_insert(0) += 1;
}
println!("{}", word_counts["apple"]); The entry API is Rust's idiomatic way to insert-or-update in a single hash lookup.
.entry(key) returns an Entry enum; .or_insert(default) inserts the default if the key is absent and returns a mutable reference to the value, which can then be incremented with *= 1.Iterating entries
var population = new Dictionary<string, int> {
{ "Paris", 2_161_000 }, { "Tokyo", 13_960_000 }
};
foreach (var (city, count) in population)
Console.WriteLine($"{city}: {count}"); use std::collections::HashMap;
let mut population = HashMap::new();
population.insert("Paris", 2_161_000);
population.insert("Tokyo", 13_960_000);
for (city, count) in &population {
println!("{city}: {count}");
} Iterating a
HashMap with for (key, value) in &map yields (&K, &V) reference pairs. The iteration order is random (not insertion order) — Rust's default hasher trades predictability for resistance to hash-flooding attacks. Use BTreeMap for sorted-key iteration.Control Flow
if / else if / else
int score = 75;
if (score >= 90)
Console.WriteLine("A");
else if (score >= 70)
Console.WriteLine("B");
else
Console.WriteLine("C"); let score = 75;
if score >= 90 {
println!("A");
} else if score >= 70 {
println!("B");
} else {
println!("C");
} Rust omits parentheses around the condition and requires braces around the body — even for single-line branches. The condition must be a
bool; no implicit truthiness (non-zero integers are not truthy in Rust).<code>if</code> as an expression
int temperature = 22;
string label = temperature >= 20 ? "warm" : "cold";
Console.WriteLine(label); let temperature = 22;
let label = if temperature >= 20 { "warm" } else { "cold" };
println!("{label}"); Rust's
if is an expression that returns the value of its last expression in each branch — there is no separate ternary operator. Both branches must have the same type. The trailing semicolon is omitted from the last expression in a block when the block is used as a value.<code>if let</code> — conditional unwrapping
int? value = 42;
if (value.HasValue)
Console.WriteLine($"Got: {value.Value}");
else
Console.WriteLine("Nothing"); let value: Option<i32> = Some(42);
if let Some(number) = value {
println!("Got: {number}");
} else {
println!("Nothing");
} if let simultaneously pattern-matches and binds — it is shorthand for a match with one arm and an _ catch-all. C#'s value is int n pattern in an if condition serves the same role, but if let works with any pattern, including nested destructuring.<code>match</code> expression
int day = 3;
string name = day switch {
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
_ => "Other",
};
Console.WriteLine(name); let day = 3;
let name = match day {
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
_ => "Other",
};
println!("{name}"); Rust's
match is exhaustive — the compiler rejects code that doesn't cover every possible value. The wildcard arm _ satisfies exhaustiveness. Arms are separated by commas; multi-statement arms use curly braces. Unlike C# switch statements, there is no fall-through behaviour.Loops & Iterators
Numeric range loop
for (int index = 0; index < 5; index++)
Console.WriteLine(index); for index in 0..5 {
println!("{index}");
} Rust's
0..5 creates an exclusive range (0, 1, 2, 3, 4). Use 0..=5 for an inclusive range (includes 5). There is no C-style three-clause for loop; all iteration uses for item in iterator.Iterating over a collection
var fruits = new List<string> { "apple", "banana", "cherry" };
foreach (var fruit in fruits)
Console.WriteLine(fruit); let fruits = vec!["apple", "banana", "cherry"];
for fruit in &fruits {
println!("{fruit}");
} The
& in for fruit in &fruits borrows the vector — fruit is a reference (&str here) and fruits remains usable afterward. Iterating without & (for fruit in fruits) moves ownership of each element, which is often what you want for Vec<String> but prevents reusing the vector.<code>while</code> loop
int counter = 0;
while (counter < 5) {
Console.WriteLine(counter);
counter++;
} let mut counter = 0;
while counter < 5 {
println!("{counter}");
counter += 1;
} Rust has no
++ or -- operators — use += 1 and -= 1. The while loop is otherwise identical to C#'s. while let Some(value) = iterator.next() is a common Rust idiom for consuming an iterator inside a while loop.<code>loop</code> with a return value
int count = 0;
while (true) {
if (count >= 3) break;
count++;
}
Console.WriteLine(count); let mut count = 0;
let result = loop {
if count >= 3 { break count * 10; }
count += 1;
};
println!("{result}"); loop is Rust's infinite loop construct. Crucially, break can carry a value — the entire loop expression evaluates to that value. This idiom replaces retry patterns or state machines that would use a bool flag and extra variable in C#.Iterator chains
var numbers = Enumerable.Range(1, 10);
var result = numbers.Where(n => n % 2 == 0)
.Select(n => n * n)
.Sum();
Console.WriteLine(result); let result: i32 = (1..=10)
.filter(|n| n % 2 == 0)
.map(|n| n * n)
.sum();
println!("{result}"); Rust's iterator chain here is lazy —
.filter() and .map() build a pipeline that only runs when .sum() consumes it. No intermediate Vec is allocated. This matches LINQ's deferred execution. Terminal operations include .sum(), .count(), .collect(), .any(), .all(), and .find().Pattern Matching
Multiple patterns per arm
int code = 404;
string message = code switch {
200 or 201 or 204 => "Success",
400 or 422 => "Bad request",
404 => "Not found",
_ => "Unknown",
};
Console.WriteLine(message); let code = 404;
let message = match code {
200 | 201 | 204 => "Success",
400 | 422 => "Bad request",
404 => "Not found",
_ => "Unknown",
};
println!("{message}"); Rust uses the
| operator to group multiple patterns in one arm, where C# 9 uses the or keyword. Both achieve the same grouping. Rust's syntax mirrors the "or" in regular expressions, which many find more concise.Range patterns and guards
int score = 82;
string grade = score switch {
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
_ => "F",
};
Console.WriteLine(grade); let score = 82;
let grade = match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
_ => "F",
};
println!("{grade}"); Rust's
match supports inclusive range patterns (90..=100). For open-ended conditions use a match guard: score if score >= 90 => "A" — the guard is an additional boolean test after the pattern. C# 9 relational patterns (>= 90) express the same thing more directly.Tuple destructuring
var point = (3, 4);
var (x, y) = point;
Console.WriteLine($"x={x}, y={y}"); let point = (3, 4);
let (x, y) = point;
println!("x={x}, y={y}"); Rust and C# share almost identical tuple destructuring syntax. In Rust, destructuring can appear in
let bindings, function parameters, match arms, and for loops. Rust tuples are structural — they are not named types.Matching enum variants with data
// C# discriminated unions require a class hierarchy or OneOf pattern
object result = "success message";
if (result is string message)
Console.WriteLine($"Success: {message}");
else if (result is int code)
Console.WriteLine($"Code: {code}"); #[derive(Debug)]
enum ApiResult { Success(String), Error(u32) }
fn main() {
let result = ApiResult::Success(String::from("ok"));
match result {
ApiResult::Success(message) => println!("Success: {message}"),
ApiResult::Error(code) => println!("Code: {code}"),
}
} Rust enums are algebraic data types — each variant can carry different data. Matching on them destructures the payload in the same step. C# 9 discriminated unions are awkward to express; the closest alternatives are
sealed class hierarchies or third-party OneOf<A,B> types. Rust enums are the idiomatic, zero-overhead approach.Closures
Basic closure
Func<int, int> double_it = x => x * 2;
Console.WriteLine(double_it(5)); let double_it = |x: i32| x * 2;
println!("{}", double_it(5)); Rust closures use the
|params| body syntax. Type annotations on parameters are usually optional when the compiler can infer them. Single-expression bodies need no braces or return; multi-statement bodies use |x| { let y = x + 1; y * 2 }.Capturing the environment
int base_value = 10;
Func<int, int> add_base = x => x + base_value;
Console.WriteLine(add_base(5)); // 15 let base_value = 10;
let add_base = |x| x + base_value; // borrows base_value
println!("{}", add_base(5)); // 15 Rust closures capture variables by the least-powerful method that works — preferring borrowing, then mutable borrowing, then moving. Prefix with
move (move |x| x + base_value) to force the closure to take ownership, which is required when returning a closure from a function or spawning threads.Closures as function arguments
var numbers = new List<int> { 1, 2, 3, 4, 5 };
int sum = numbers.Aggregate(0, (acc, n) => acc + n);
Console.WriteLine(sum); let numbers = vec![1, 2, 3, 4, 5];
let sum: i32 = numbers.iter().fold(0, |acc, &n| acc + n);
println!("{sum}"); Rust functions that accept closures use
Fn, FnMut, or FnOnce trait bounds — the equivalent of C#'s Func<T>/Action<T> delegates. Fn allows many calls; FnMut allows mutation of captured variables; FnOnce allows exactly one call (consuming the closure).Common iterator adapters
var numbers = new List<int> { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers.Sum());
Console.WriteLine(numbers.Any(n => n > 4));
Console.WriteLine(numbers.All(n => n > 0));
Console.WriteLine(numbers.First(n => n % 2 == 0)); let numbers = vec![1, 2, 3, 4, 5];
println!("{}", numbers.iter().sum::<i32>());
println!("{}", numbers.iter().any(|&n| n > 4));
println!("{}", numbers.iter().all(|&n| n > 0));
println!("{:?}", numbers.iter().find(|&&n| n % 2 == 0)); Rust's iterator adapters mirror LINQ:
.sum(), .any(), .all(), .find(), .min(), .max(), .count(), .enumerate(), .zip(), and more. .find() returns Option<&T> rather than throwing like LINQ's .First() when nothing matches.Error Handling
<code>Option<T></code> — absence without null
string? name = null;
Console.WriteLine(name ?? "anonymous");
int? length = name?.Length;
Console.WriteLine(length); let name: Option<&str> = None;
println!("{}", name.unwrap_or("anonymous"));
let length: Option<usize> = name.map(|text| text.len());
println!("{length:?}"); Option<T> is Rust's replacement for null — a value is either Some(value) or None, and the compiler forces you to handle both. There is no null pointer in safe Rust; Option makes the possibility of absence visible in the type. .map() applies a function to Some and returns None unchanged — like C#'s null-conditional ?..<code>Result<T, E></code> — errors without exceptions
try {
int value = int.Parse("not a number");
Console.WriteLine(value);
} catch (FormatException exception) {
Console.WriteLine($"Parse error: {exception.Message}");
} let result: Result<i32, _> = "not a number".parse();
match result {
Ok(value) => println!("{value}"),
Err(error) => println!("Parse error: {error}"),
} Rust has no exceptions. Fallible operations return
Result<Ok, Err>. Matching on both arms is idiomatic. If you know a call cannot fail in context, .unwrap() extracts the value or panics; .expect("message") panics with a custom message. .unwrap_or(default) and .unwrap_or_else(|error| ...) handle the error quietly.<code>?</code> operator — early return on error
// C# equivalent: an exception thrown up the call stack automatically
static int ParseAndDouble(string input) {
int value = int.Parse(input); // throws FormatException on bad input
return value * 2;
}
Console.WriteLine(ParseAndDouble("5")); fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> {
let value: i32 = input.parse()?; // ? returns Err early if parse fails
Ok(value * 2)
}
fn main() {
println!("{:?}", parse_and_double("5"));
println!("{:?}", parse_and_double("bad"));
} The
? operator is shorthand for "return Err(e) immediately if this is an error, otherwise unwrap the Ok value." It is the Rust equivalent of how exceptions propagate in C# — but explicit in the function's return type. ? only works in functions returning Result or Option.Chaining with <code>map</code> and <code>and_then</code>
string input = "5";
// Chain: parse → double → stringify
string output = (int.TryParse(input, out int value)
? (value * 2).ToString()
: "error");
Console.WriteLine(output); let output: Result<String, _> = "5"
.parse::<i32>()
.map(|value| (value * 2).to_string());
println!("{:?}", output); // Ok("10") Result::map(f) applies f to the Ok value and leaves Err untouched — equivalent to null-conditional chaining in C#. Result::and_then(f) is for when f itself returns a Result (flatMap). These let you build a pipeline of fallible steps without nested match blocks.Panic — the last resort
// C# throws unchecked exceptions (ArgumentException, IndexOutOfRange, etc.)
var items = new List<int> { 1, 2, 3 };
Console.WriteLine(items[10]); // throws ArgumentOutOfRangeException let items = vec![1, 2, 3];
// items[10] // would panic: index out of bounds
// use .get(10) for safe access:
println!("{:?}", items.get(10)); // None A Rust panic is an unrecoverable error (like an uncaught C# exception) — it prints a message, unwinds the stack, and exits. Index out of bounds, integer overflow in debug, and explicit
panic!("message") all cause panics. For safe fallible access, prefer .get(index) which returns Option<&T>.Structs & Enums
Struct definition with methods
var point = new Point(3, 4);
Console.WriteLine(point.Distance());
record Point(int X, int Y) {
public double Distance() => Math.Sqrt(X * X + Y * Y);
} struct Point { x: i32, y: i32 }
impl Point {
fn distance(&self) -> f64 {
((self.x * self.x + self.y * self.y) as f64).sqrt()
}
}
fn main() {
let point = Point { x: 3, y: 4 };
println!("{}", point.distance());
} Rust structs hold data;
impl blocks add methods. &self is an immutable reference to the receiver (like this in C#); &mut self allows mutation; self (no reference) consumes the value. There is no class, no constructor — use struct literal syntax (Point { x: 3, y: 4 }) or a factory function.Basic enum
var heading = Direction.North;
Console.WriteLine(heading);
enum Direction { North, South, East, West } #[derive(Debug)]
enum Direction { North, South, East, West }
fn main() {
let heading = Direction::North;
println!("{heading:?}");
} Rust enums are accessed with the
:: path separator (Direction::North). The #[derive(Debug)] attribute auto-generates the debug-format implementation. Unlike C# enums, Rust enum variants are not integers by default — use #[repr(i32)] and explicit discriminants to opt into that.Enum with associated data (algebraic types)
// C# discriminated unions require a class hierarchy or OneOf pattern
Shape shape = new Circle { Radius = 5.0 };
double area = shape switch {
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.Width * r.Height,
_ => 0,
};
Console.WriteLine(area.ToString("F2"));
abstract class Shape {}
class Circle : Shape { public double Radius; }
class Rectangle : Shape { public double Width, Height; } #[derive(Debug)]
enum Shape {
Circle(f64),
Rectangle(f64, f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
let shape = Shape::Circle(5.0);
println!("{:.2}", area(&shape));
} Rust enums with associated data are algebraic data types — each variant can carry a different payload. The compiler guarantees exhaustive matching. C# requires a sealed class hierarchy plus pattern matching in a switch expression to approximate this. Rust's approach is zero-overhead: the enum is a tagged union on the stack.
Struct update syntax
var defaults = new Config("localhost", 8080, false);
var production = defaults with { Host = "example.com", Tls = true };
Console.WriteLine($"{production.Host}:{production.Port} TLS={production.Tls}");
record Config(string Host, int Port, bool Tls); #[derive(Debug)]
struct Config { host: &'static str, port: u16, tls: bool }
fn main() {
let defaults = Config { host: "localhost", port: 8080, tls: false };
let production = Config { host: "example.com", tls: true, ..defaults };
println!("{}:{} TLS={}", production.host, production.port, production.tls);
} Rust's
..defaults struct update syntax copies remaining fields from an existing value — like C#'s with on a record. It must appear last in the struct literal. The original struct is partially moved (or copied if all types implement Copy).Traits
Defining and implementing a trait
var greeter = new Greeter();
Console.WriteLine(greeter.Greet());
interface IGreet { string Greet(); }
class Greeter : IGreet {
public string Greet() => "Hello!";
} trait Greet {
fn greet(&self) -> String;
}
struct Greeter;
impl Greet for Greeter {
fn greet(&self) -> String {
String::from("Hello!")
}
}
fn main() {
let greeter = Greeter;
println!("{}", greeter.greet());
} A Rust
trait is like a C# interface: it declares method signatures a type must implement. The key difference is that impl Trait for Type is always written in a separate block — not inside the type definition — and you can implement traits for existing types (including standard library types) from third-party crates.Default method implementations
IAnimal dog = new Dog(); // typed as interface to access default method
Console.WriteLine(dog.Describe());
interface IAnimal {
string Name();
string Describe() => $"I am {Name()}"; // C# 8 default interface methods
}
class Dog : IAnimal {
public string Name() => "Rex";
} trait Animal {
fn name(&self) -> &str;
fn describe(&self) -> String { // default implementation
format!("I am {}", self.name())
}
}
struct Dog;
impl Animal for Dog {
fn name(&self) -> &str { "Rex" }
}
fn main() {
let dog = Dog;
println!("{}", dog.describe());
} Trait methods can have default implementations — any type implementing the trait gets the default for free and can override it. This is the same as C# 8's default interface methods. Default implementations can call other methods on the trait, enabling powerful composition.
<code>Display</code> trait (<code>ToString</code> equivalent)
var person = new Person("Alice", 30);
Console.WriteLine(person);
class Person {
string Name; int Age;
public Person(string name, int age) { Name = name; Age = age; }
public override string ToString() => $"{Name} ({Age})";
} use std::fmt;
struct Person { name: String, age: u32 }
impl fmt::Display for Person {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "{} ({})", self.name, self.age)
}
}
fn main() {
let person = Person { name: String::from("Alice"), age: 30 };
println!("{person}");
} Implementing
fmt::Display makes a type work with {} in println! — the equivalent of overriding ToString() in C#. Implementing fmt::Debug (usually via #[derive(Debug)]) makes it work with {:?}. The two traits serve different audiences: Display for end users, Debug for developers.Trait bounds on generic functions
static void Print<T>(T value) where T : System.IFormattable {
Console.WriteLine(value.ToString("G", null));
}
Print(3.14);
Print(42); use std::fmt::Display;
fn print_it<T: Display>(value: T) {
println!("{value}");
}
fn main() {
print_it(3.14);
print_it(42);
} T: Display is a trait bound — it constrains what types T can be. C#'s where T : IComparable is the direct equivalent. Multiple bounds use +: T: Display + Clone. The impl Trait shorthand also works: fn print_it(value: impl Display).Generics
Generic function
T First<T>(List<T> items) => items[0];
Console.WriteLine(First(new List<int> { 1, 2, 3 }));
Console.WriteLine(First(new List<string> { "a", "b" })); fn first<T: Clone>(items: &[T]) -> T {
items[0].clone()
}
fn main() {
println!("{}", first(&[1, 2, 3]));
println!("{}", first(&["a", "b"]));
} Generic functions in Rust are written identically to C#: the type parameter in angle brackets before the argument list. The
Clone bound is required here because returning a value from a reference requires copying it. Rust monomorphises generics at compile time (no runtime boxing), as does C# when structs are involved.Generic struct
class Stack<T> {
private List<T> items = new();
public void Push(T item) => items.Add(item);
public T Pop() => items[^1] is var item ? (items.RemoveAt(items.Count - 1), item).item : throw new InvalidOperationException();
public bool IsEmpty => items.Count == 0;
}
var stack = new Stack<int>();
stack.Push(1); stack.Push(2);
Console.WriteLine(stack.IsEmpty); struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self { Stack { items: Vec::new() } }
fn push(&mut self, item: T) { self.items.push(item); }
fn pop(&mut self) -> Option<T> { self.items.pop() }
fn is_empty(&self) -> bool { self.items.is_empty() }
}
fn main() {
let mut stack: Stack<i32> = Stack::new();
stack.push(1); stack.push(2);
println!("{}", stack.is_empty());
} Generic structs carry the type parameter on both the
struct declaration and the impl block: impl<T> Stack<T>. Self is an alias for the implementing type, useful in constructors. fn new() -> Self is Rust's idiomatic constructor pattern — there is no new keyword.Complex bounds with <code>where</code> clauses
T Max<T>(T first, T second) where T : IComparable<T>
=> first.CompareTo(second) >= 0 ? first : second;
Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana")); fn max_of<T>(first: T, second: T) -> T
where
T: PartialOrd,
{
if first >= second { first } else { second }
}
fn main() {
println!("{}", max_of(3, 7));
println!("{}", max_of("apple", "banana"));
} The
where clause keeps the function signature readable when there are many trait bounds. PartialOrd is Rust's equivalent of IComparable<T> for the <, >= operators. Ord is the total-ordering version (like IComparable without floating-point NaN). Use Ord for .min()/.max() on iterators.