Rust: Ownership - 1
NERDSSS. I was re-reading Rust ownership so I decided to write a blog poast about it. I’ll try to keep it simple and smooth as usual and for this one I’ve tried to take a game-like approach so it’d be like a lore-code.
This is Part 1 about Ownership in-general and we’ll focus on how Rust handles who “owns” data, especially stuff on the heap like dynamic strings.
Well here I decided to go with a Treasure Hunt… why? Because ownership is like claiming artifacts. Only one hunter can hold a treasure at a time, and if you drop it (out of scope), it’s gone forever. We’ll go step by step, hitting roadblocks that teach the concepts (hopefully).
First, make sure you have Rust toolchain up and running (rustup is your friend, btw.) and let’s code!!
Setting stuff up
Firstly we have to get started by making a Rust project. This’ll be a simple CLI game called “Rust Island Treasures”. The player starts with an empty backpack (inventory). As they explore, they find treasures represented as String
s because their descriptions can grow or change (like adding notes about curses or values). The goal here is to collect treasures without losing them to the island’s pitfalls (aka Rust’s compiler enforcing ownership rules).
so… to setup a project run:
$ cargo new rust-island-treasures
$ cd rust-island-treasures
now open your beloved text-editor (pls don’t fight over nvim, emacs, c*rsor etc.) just pick one.
Level 1: Discovering your First Treasure (Variables and Scope)
In our game, treasures appear when you enter a cave. Let’s create a treasure as a String
why? Just because. nah, I’m just kidding… Because string literals are baked into the binary, are immutable and fixed. Treasures might need to expand (e.g. “Golden Idol” becomes “Golden Idol - Cursed!”).
so… enough yapping time to get rusty
fn main() {
explore_cave();
}
fn explore_cave() {
let treasure = String::from("Golden Idol"); // claim ownership here
println!("You found {}", treasure);
} // treasure drops out of scope here. POOF!! it's freed!!
run this with cargo run
rust-island-treasures [ master][?][📦 v0.1.0][🦀 v1.88.0]
❯ cargo run
Compiling rust-island-treasures v0.1.0 (/home/syk/dev/rndm/rust-island-treasures)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.51s
Running `target/debug/rust-island-treasures`
You found Golden Idol
It works! But… inside explore_cave
, treasure
owns the data on the heap. When the function ends, Rust calls drop
automatically, freeing the memory. No manual cleanup, no garbage collector hunting it down later. That’s ownership rule #3 in action: when the owner goes out of scope, the value is dropped.
Now, what if we want to keep the treasure? Let’s try adding it to inventory outside the function.
fn main() {
let mut inventory = String::from("Empty backpack");
explore_cave(); // hmm... do we get the treasure out?
println!("Inventory: {}", inventory);
}
fn explore_cave() {
let treasure = String::from("Golden Idol");
// can't return it yet... let's pretend we add it here later
}
Stuck? The treasure is owned by explore_cave
and vanishes when the scope ends. We need to pass ownership back.
Level 2: Passing Ownership (Functions and Moves)
To “hand over” the treasure, we’ll return it from the function. This transfers ownership which follows rule #2: only one owner at a time.
Let’s update the code for this…
fn main() {
let mut inventory = String::from("Empty backpack");
let found_treasure = explore_cave(); // hmm... do we get the treasure out?
inventory.push_str(&format!(" + {}", found_treasure));
println!("Inventory: {}", inventory);
}
fn explore_cave() -> String {
let treasure = String::from("Golden Idol");
treasure // ownership moves to the caller
}
Well well well… let’s try cargo run
on this again…
rust-island-treasures [ master][?][📦 v0.1.0][🦀 v1.88.0]
❯ cargo run
Compiling rust-island-treasures v0.1.0 (/home/syk/dev/rndm/rust-island-treasures)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/rust-island-treasures`
Inventory: Empty backpack + Golden Idol
OH! It shows “Empty backpack + Golden Idol”. Looks like we managed to get it out of the cave! When we return treasure
, ownership moves to found_treasure
in main
. The original treasure
is invalidated; trying to use it after would make the compiler yell at us.
But… what about the integers or simple types? Let’s add a “score” to our game. Integers are stack-only so they copy instead of move.
READ ABOUT STACK-HEAP HERE IF YOU’RE A BOZO
fn main() {
let mut inventory = String::from("Empty backpack");
let found_treasure = explore_cave();
inventory.push_str(&format!(" + {}", found_treasure));
let score = calculate_score(10); // pass a stack value
println!("Inventory: {} | Score: {}", inventory, score);
}
fn explore_cave() -> String {
let treasure = String::from("Golden Idol");
treasure
}
fn calculate_score(base: i32) -> i32 {
base + 5 // copies base and original stays valid
}
Well what happened here… Integers implement Copy
, so passing them doesn’t move ownership… they just duplicate on the stack. Super fast, no heap drama. Strings? They move because they’re heap-allocated and don’t implement Copy
(they use Drop
for cleanup).
And if we run this code after adding score part we get
rust-island-treasures [ master][?][📦 v0.1.0][🦀 v1.88.0]
❯ cargo run
Compiling rust-island-treasures v0.1.0 (/home/syk/dev/rndm/rust-island-treasures)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/rust-island-treasures`
Inventory: Empty backpack + Golden Idol | Score: 15
We got our treasure and the score as well. We passed 10 and got 15.
Level 3: The Pitfall of Moves (Invalidating Variables)
Hmm let’s talk about the pitfalls of moves now. How about making this more game-like? Suppose we try to use a treasure after handing it off.
fn explore_cave() -> String {
let treasure = String::from("Golden Idol");
println!("Inspecting: {}", treasure); // used here
treasure // move ownership out
}
This’ll work perfectly fine but…
fn explore_cave() -> String {
let treasure = String::from("Golden Idol");
let moved = treasure; // move to `moved`
println!("Inspecting: {}", treasure); // Error! treasure is invalid now.
moved
}
This’ll make the compiler yell at us… about what?
borrow of moved value: `treasure`
value borrowed here after move
Hmm… Ownership moved to moved
, so treasure
is toast. This prevents double-free bugs or dangling pointers… Rust catches it at compile time.
In our game, this is like dropping a treasure into a pit: once moved, it’s gone from our hands.
Boss Level: Cloning to Keep Multiples (Deep Copies)
What if we want to duplicate a treasure for trading? We can use clone()
as it makes a deep copy of the heap data. Let’s see it in action using the following code:
fn main() {
let mut inventory = String::from("Empty backpack");
let found_treasure = explore_cave();
let duplicate = found_treasure.clone(); // expensive but keep original data
inventory.push_str(&format!(" + {} + Duplicate {}", found_treasure, duplicate));
let score = calculate_score(10);
println!("Inventory: {} | Score: {}", inventory, score);
}
fn explore_cave() -> String {
let treasure = String::from("Golden Idol");
treasure
}
fn calculate_score(base: i32) -> i32 {
base + 5
}
Now inventory has both but clone()
allocates a new heap space it’s not free. Use it sparingly, like in a game where duplicating costs “mana”.
Notice how stack vs heap matters? Stack pushes are instant (top of the stack), heap needs allocator searches. Ownership keeps heap tidy without GC pauses or manual frees.
Let’s try to run this updated code now
rust-island-treasures [ master][?][📦 v0.1.0][🦀 v1.88.0]
❯ cargo run
Compiling rust-island-treasures v0.1.0 (/home/syk/dev/rndm/rust-island-treasures)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/rust-island-treasures`
Inventory: Empty backpack + Golden Idol + Duplicate Golden Idol | Score: 15
Awesome! We got a duplicate Golden Idol now. Which explains how cloning makes the difference here. It made a new copy of found_treasure
into the heap.
And there you have it. A basic treasure hunt game teaching us ownership without feeling like a boring lecture (which I never experienced). Oh and what are the takeaways here?
- One Owner Rule: Prevents chaos like multiple claims on the same treasure.
- Moves: Transfer ownership, invalidate the old variable (great for heap safety).
- Scope Drops: Auto-cleanup when things go out of scope and no leaks.
- Copy vs Clone: Stack types copy cheaply; Heap needs explicit clones.
(If you’re following this then consider doing the homework below)
HOMEWORK: Run this full code. Tweak it. Add a user input for exploring more caves.
There’ll be a part 2 (if I got time) about references and borrowing: How to “borrow” a treasure without stealing it. Which is perfect for inspecting without losing ownership.