Ownership
Introduction
Rust has no garbage collector. Memory safety is enforced by ownership: each value has a single owner. When the owner goes out of scope, the value is dropped (freed).
Three rules
- Each value has exactly one owner.
- When the owner goes out of scope, the value is dropped.
- Assigning or passing a value by default moves it (ownership transfers); the previous owner cannot use it.
Move semantics
let s1 = String::from("hello");
let s2 = s1; // s1 is moved into s2
// println!("{}", s1); // ERROR: use of moved value
println!("{}", s2); // OKIn C#: Reference types are copied by reference; both variables point to the same object. In Rust, String is not copied; ownership moves to s2, and s1 is no longer valid.
What a move actually does (memory)
A String is a small three-word handle on the stack (pointer, length, capacity β
24 bytes on a 64-bit machine) that points at a buffer on the heap:
let s1 = String::from("hi");
stack: s1 heap
βββββββββββββββββ βββββββ¬ββββββ
β ptr ββββββββββΌββββββββΆβ 'h' β 'i' β
β len = 2 β βββββββ΄ββββββ
β cap = 2 β
βββββββββββββββββlet s2 = s1; copies only those three words into s2 and marks s1 invalid β the
heap bytes are not copied, and only s2 will free the buffer (so there's no
double-free). That's why a move is cheap and s1 can no longer be used.
In C#: copying a string variable also just copies a pointer β but both variables
stay usable and the GC frees the object once nobody references it. Rust moves the handle
and statically assigns the single "free it" duty to the new owner.
Copy types
Simple types that implement Copy (e.g. i32, bool, char) are copied automatically; no move:
let x = 5;
let y = x; // x is copied
println!("{} {}", x, y); // both OKIn C#: Value types (int, bool) are copied by value; similar idea.
String vs &str
String: Owned, growable, heap-allocated. You create one from a string literal withString::from("hello"): that allocates a buffer on the heap and copies the text into it. Your code owns that buffer; when theStringis dropped, the buffer is freed.&str: Borrowed view of string data (a slice); immutable. A string literal like"world"is already a&str(it points into the binary). No allocation; you donβt own the storage.
let owned: String = String::from("hello"); // heap allocation, you own it
let slice: &str = "world"; // no allocation, just a viewIn C#: string is a reference type; βownershipβ is not explicit. In Rust, String is explicitly owned (one owner); &str is a non-owning view. See the separate file on String::from and owned types for more detail.
Common pitfalls
.clone()copies the data, not just the handle. It's the sanctioned way out of a move error, but for aString/Vecit allocates and deep-copies the heap buffer β fine occasionally, wasteful in a hot loop. Reach for a borrow (&) first; clone only when you truly need a second independent owner.- A move isn't a bug to "fix" with
.clone()reflexively. If the original is no longer needed, the move is exactly right; clone when you need both values to stay usable.
In C#: copying a reference variable is always cheap (no deep copy); Rust's .clone() is
closer to an explicit deep copy, so its cost is visible in the code.
Key takeaway
Rust has no GC; ownership and moves enforce who frees memory. Assigning or passing non-Copy values moves them. In C#, reference types share the object; in Rust, ownership is single.