Chapter 02 · the new idea
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.
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.
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
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.
var a = new List<int>();
var b = a; // alias
a.Add(1); // fine
b.Add(2); // same list
let a = vec![1];
let b = a; // a MOVED into b
println!("{:?}", a); // ERROR: use after move
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.
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 have | Count |
|---|---|
Shared references &T (read) | any number |
OR one mutable reference &mut T (write) | exactly one |
| Both at once | never |
"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.
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.
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:
| Need | Reach for | C# feeling |
|---|---|---|
| Heap allocation, single owner | Box<T> | a plain reference type |
| Shared ownership, single thread | Rc<T> | ref-counted handle |
| Shared ownership across threads | Arc<T> | thread-safe ref count |
| Shared mutable state | Mutex<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.
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.