Chapter 03

structs, real enums, and the end of null.

Rust has no classes and no inheritance. Data is structs and enums; behavior is traits (next chapter). The enum does a lot of the work here: it's a discriminated union, and it's how Rust does away with null.

3.1structs ≈ classes-without-the-methods-attached

A struct holds data. It's not the C# struct value-type distinction you're thinking of — Rust decides stack vs heap by other means. Methods go in a separate impl block, which keeps data and behavior visually distinct.

C#

class Point {
  public double X, Y;
  public double Dist() =>
    Math.Sqrt(X*X + Y*Y);
}

Rust

struct Point { x: f64, y: f64 }

impl Point {
  fn dist(&self) -> f64 {
    (self.x*self.x + self.y*self.y).sqrt()
  }
}

&self is just this, but borrowed — the method reads the struct without taking ownership, exactly the borrowing from chapter 2.

3.2enums are sum types, not just named constants

A C# enum is a glorified integer. A Rust enum is a discriminated union: each variant can carry its own differently-typed payload. It's one of the types you'll reach for most.

enum Shape {
    Circle { r: f64 },              // struct-like variant
    Rect { w: f64, h: f64 },
    Point,                          // no payload
}
Closest C#

You'd model this with a class hierarchy plus pattern matching, or the newer records + sealed interface "DU emulation." Rust bakes it in, and crucially the compiler checks you've handled every variant.

3.3match — an exhaustive switch

match is C#'s switch expression, but the compiler refuses to compile if you miss a case. Add a new enum variant and every match that forgot it lights up red — which makes large refactors considerably safer.

fn area(s: &Shape) -> f64 {
    match s {
        Shape::Circle { r }      => 3.14159 * r * r,
        Shape::Rect { w, h }   => w * h,
        Shape::Point          => 0.0,
        // omit one arm → compile error
    }
}

3.4Option<T> — there is no null

Rust has no null. Instead, a value that might be missing gets the type Option<T> — an enum with exactly two variants: Some(T) wraps a real value, and None means there's nothing there. Picture a box that either holds something or is empty; the type forces you to open it before you can use what's inside, so the NullReferenceException as a category does not exist.

// looking up a user's age — they might not exist
let found:   Option<u32> = Some(34);  // a box with 34 inside
let missing: Option<u32> = None;      // an empty box

So a function that may or may not find something returns an Option, and the caller has to deal with both cases. Compare a lookup in each language:

C# nullable

int? age = FindAge(id);
if (age is not null)
    Console.WriteLine($"{age} yrs");
// nothing FORCES the check

Rust Option

let age: Option<u32> = find_age(id);
match age {
    Some(years) => println!("{years} yrs"),
    None        => println!("no such user"),
}

The nullable-reference-types feature in C# 8+ is Microsoft retrofitting exactly this idea — but it's opt-in and warns rather than blocks. Rust had it from day one and it's a hard error.

Ergonomics

You won't write match for every option. Combinators like .map(), .unwrap_or(0), .and_then(), and if let Some(years) = find_age(id) make it terse — much like null-coalescing (??) and null-conditional (?.) chains, but type-checked end to end.

3.5Quick type map

C#Rust
int / longi32 / i64 (size is explicit)
stringString (owned) / &str (borrowed slice)
List<T>Vec<T>
Dictionary<K,V>HashMap<K,V>
T? (nullable)Option<T>
tuple (int, string)(i32, String)
varlet (immutable by default; let mut to opt in)

Note that last row: bindings are immutable unless you write mut. The opposite default from C#, and it nudges you toward safer code.