Chapter 02 · the new idea

Ownership: a garbage collector that runs at compile time.

This is the chapter with no C# equivalent. Master it and the rest of Rust is just a nicer type system. The promise: memory and thread safety with no GC, no free(), and no runtime cost.

2.1How C# manages memory (so we can contrast)

In .NET, reference types live on the heap and the GC frees them whenever it likes. You never think about lifetime; you pay for it with GC pauses and the occasional NullReferenceException or unexpected shared-mutable-state bug. Rust gets the same safety by deciding ownership at compile time instead.

2.2The three rules

Every value has exactly one owner (a variable). When the owner goes out of scope, the value is dropped — its memory freed — deterministically, like Dispose() being called for you at the closing brace. That's rule one.

fn main() {
    let s = String::from("hi");   // s owns the heap buffer
}                              // s dropped here — memory freed, no GC

2.3Move semantics — the surprising part

Assigning or passing a non-Copy value moves ownership. The source variable becomes unusable. In C# this code is fine — both variables point at the same object. In Rust it won't compile.

C# — both valid

var a = new List<int>();
var b = a;        // alias
a.Add(1);        // fine
b.Add(2);        // same list

Rust — won't compile

let a = vec![1];
let b = a;        // a MOVED into b
println!("{:?}", a); // ERROR: use after move
Why

If both a and b owned the buffer, both would try to free it at scope end — a double-free. Rust's answer isn't a runtime check; it's to say only one owner exists, period. Want a real copy? Call .clone() explicitly. The cost is always visible.

2.4Borrowing — references without giving up ownership

Cloning everything would be wasteful, so you lend access with a borrow: &T (shared/read-only) or &mut T (exclusive/mutable). This is the everyday way to pass things to functions.

fn len(s: &String) -> usize { s.len() }  // borrows, doesn't take

let name = String::from("ada");
let n = len(&name);   // lend it
println!("{name} is {n}"); // still own it — fine

The borrow rules, enforced at compile time, are what make this work:

At any moment, you may haveCount
Shared references &T (read)any number
OR one mutable reference &mut T (write)exactly one
Both at oncenever
The payoff

"Many readers xor one writer" is exactly the invariant that prevents data races. Rust proves it at compile time, which is why fearless concurrency is a real thing: code that would race simply doesn't compile. The C# equivalent is hoping you remembered the right lock.

2.5Lifetimes — making borrows safe

A borrow must never outlive the thing it points to (no dangling references). Usually the compiler infers this. Occasionally you annotate it with a lifetime like 'a — a label that says "these references live at least as long as each other." It's not a runtime value; think of it as a generic parameter over scope.

// "the returned ref lives as long as both inputs"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

You'll read lifetimes long before you need to write them. When you do hit them, the compiler usually tells you precisely what to add.

2.6When you genuinely need shared ownership

Sometimes one owner isn't enough — a graph, a cache, shared state across threads. Rust has explicit tools, and choosing one is a deliberate decision rather than the silent default:

NeedReach forC# feeling
Heap allocation, single ownerBox<T>a plain reference type
Shared ownership, single threadRc<T>ref-counted handle
Shared ownership across threadsArc<T>thread-safe ref count
Shared mutable stateMutex<T> / RwLock<T>lock(){}, but the type forces it

The pattern Arc<Mutex<T>> — shared, lockable state — is the Rust workhorse you'll see constantly in servers like this one.

From a .NET codebase?

If your C# code leans on lock, ConcurrentDictionary, Interlocked, Lazy<T>, or SemaphoreSlim, chapter 10 maps each of those to its Rust counterpart, side by side.