Okay, so picture this: you’re cruising along in your nice, type-safe Rust code, and suddenly… your variable vanishes.
Not literally, of course. But the compiler throws a fit, and you’re left staring at an error that says something like:
“value borrowed here after move”.
Wait, move?
Welcome to move semantics, Rust’s very opinionated way of managing memory and keeping you from accidentally using things that don’t belong to you anymore.
Today’s post is a follow-up to yesterday’s crash course in ownership and this one might sting a little at first if you’re coming from the comfy world of .NET’s reference types. But stick with me. It’s about to make sense.
C# Land: References Rule Everything
In C#, most of our objects live on the heap and are accessed via references. When we assign a variable to another, we’re copying the reference, not the object.
var a = new Person("Alice"); var b = a; b.Name = "Bob"; Console.WriteLine(a.Name); // Bob
Both a
and b
point to the same object. It’s shared, and changes affect both.
Rust Land: Ownership Transfer by Default
Rust, on the other hand, doesn’t use reference semantics by default. Instead, it moves the value unless it’s a type that’s Copy
.
Here’s a head-scratcher from early on in my Rust journey:
fn main() { let name = String::from("Alice"); let other = name; println!("{}", name); // ERROR: value borrowed here after move }
Wait… what?
When we do let other = name;
, Rust moves the ownership of the String
from name
to other
. That means name
no longer owns the value and using it again is forbidden.
Think of it like this: the value didn’t get cloned or copied it got handed off.
But Why Move Instead of Copy?
Great question. In Rust, types like String
, Vec<T>
, and anything that allocates on the heap are not cheap to copy. Instead of silently cloning data and potentially causing performance issues, Rust makes moves explicit.
This design choice forces you to think about whether you truly need a clone or a reference. It’s a performance win wrapped in a safety blanket.
But Some Types Are Copyable
Rust distinguishes between types that are Copy (duplicated on assignment) and those that aren’t.
These scalar types are Copy
by default:
let x = 5; let y = x; // no move, just a copy println!("x: {}, y: {}", x, y); // totally fine
The same applies to bool
, char
, and simple numeric types.
So why isn’t String
Copy
?
Because String
owns heap memory. Copying it blindly would mean a deep copy, which Rust won’t do unless you ask explicitly.
Clone All the Things? Not Quite.
Want to keep using a value after moving it? Then you’ll need to clone()
it.
fn main() { let name = String::from("Alice"); let other = name.clone(); println!("name: {}, other: {}", name, other); }
Just like in C#, this creates two independent strings. But Rust makes sure you know that it’s not cheap you called .clone()
on purpose.
References: Your New Best Friend
Sometimes you don’t want to move or clone. You just want to borrow a value.
fn main() { let name = String::from("Alice"); greet(&name); println!("{}", name); // still usable } fn greet(person: &String) { println!("Hello, {}!", person); }
Passing a reference means you’re saying: “I don’t want to take this, just use it for a second.” Very much like ref
or in
in C#, but safer and enforced at compile time.
Final Thoughts: Rust Isn’t Trying to Confuse You
At first, move semantics feel harsh. Variables stop working. Ownership gets transferred. Errors fly everywhere.
But once you get the hang of it, it’s honestly brilliant. It forces you to be deliberate about memory and data flow and that makes your code faster and more predictable.
It’s like Rust is saying: “You want performance and safety? You’ve gotta earn it.”
Tomorrow, we’ll dive into borrowing and references in more depth, including mutable borrowing. It’s like ref
and out
in C#, but with training wheels and a seatbelt.