Rust Ownership: Stop Memorizing! Play with Smart Pointers as “Toys” - The Big Three (Box, Rc, RefCell) Will Make Memory Management Crystal Clear

Hey there, friend! Still getting dizzy with Rust’s ownership system? All that borrowing and lifetime stuff feeling like studying an incomprehensible legal code?

Don’t worry! Today we’re not going to talk about those big theories. Let’s imagine the code world as a playground, data as beloved toys, and see how Rust makes memory management both safe and fun through several “smart” “toy boxes” (smart pointers).

Toy Box #1: Box<T> — “This toy is too big, it won’t fit in my house!”

Imagine you bought an awesome LEGO Millennium Falcon, but it’s too big for your small room (Stack) to hold. What do you do?

Your mom (the compiler) says: “Silly child, if it doesn’t fit, just rent an external storage locker (Heap)!”

Box<T> is that “universal box” that helps you rent a storage locker and put your toy inside.

It’s simple, it does one thing: moves your data from the stack to the heap. And what do you hold? Just a “key” (pointer) pointing to the storage locker. This “key” itself is small, so your room (stack) can definitely hold it.

// Can't fit 1 million integers in the room (stack)
// let a = [0; 1000000]; // This might crash the program

// Use Box to put it in the external storage locker (heap)
let b = Box::new([0; 1000000]); // Easy peasy!

Most importantly, this storage locker key can only have one owner at a time. If you give the key to your friend, you can’t use it anymore. This is Box<T>’s single ownership. When “you” (the variable holding the Box) leave the playground (scope), Rust automatically helps you return the storage locker, and the toys inside are destroyed, clean and tidy, never forgetful.

When to use it?

  1. When your “toy” is too big and won’t fit on the stack.
  2. When you need a “toy list” but each toy in the list has a different size (trait objects Box<dyn Trait>).
  3. When you create a recursive toy that “gives birth to itself” (like a linked list).

Toy Box #2: Rc<T> — “My toy, everyone can look at it together!”

Now, you have a limited edition comic book (a piece of data), and several of your friends (different parts of the code) want to read it.

If you use Box, you give the comic book (ownership) to friend A, and friends B and C can’t read it. This obviously won’t work - the ship of friendship will capsize.

So, Rc<T> (Reference Counting) makes its grand entrance! It’s like a “librarian”.

Rc<T> puts your comic book in a public reading room (still on the heap), then gives each friend who wants to read it a “library card” (clones an Rc pointer). It has an internal counter that records how many “library cards” have been issued.

use std::rc::Rc;

// Manage the comic book "Rust from Beginner to Giving Up" with Rc
let book = Rc::new(String::from("Rust from Beginner to Giving Up"));

println!("Current readers: {}", Rc::strong_count(&book)); // Output 1, just yourself

// Friend A borrowed it
let friend_a = Rc::clone(&book);
println!("Current readers: {}", Rc::strong_count(&book)); // Output 2

// Friend B also borrowed it
let friend_b = Rc::clone(&book);
println!("Current readers: {}", Rc::strong_count(&book)); // Output 3

When a friend finishes reading and destroys their “library card” (variable leaves scope), the counter decreases by one. When the counter reaches zero, it means no one is reading this comic book anymore, and the librarian Rc will dispose of the book and reclaim memory.

But remember! Toys managed by Rc<T> can only be looked at, not modified! If anyone dares to scribble on them (modify data), the compiler will be the first to spank your bottom. This is for safety, to prevent data races.

Toy Box #3: RefCell<T> — “Secretly, I have a way to modify it while everyone is looking”

“Only look, no modify? That’s too inflexible!” you might complain.

Don’t worry, Rust provides a “cheat device”, a real black magic — RefCell<T>.

RefCell<T> is like a transparent display case with a runtime lock. Under normal circumstances, it’s like Rc, allowing multiple people to “observe” (shared references). But it secretly provides a special key that lets you apply for “temporary modification rights” at runtime (when the program is running).

It bypasses the compiler’s static borrowing checks and postpones the checking work to runtime.

use std::cell::RefCell;

let shared_data = RefCell::new(5);

// Everyone can look
println!("Original data: {:?}", shared_data.borrow());

// I want to modify it, apply for a mutable borrow
let mut mutable_borrow = shared_data.borrow_mut();
*mutable_borrow += 1;

// After modification, others see the new data
println!("Modified data: {:?}", shared_data.borrow()); // Output 6

Sounds great? But black magic always has a price!

The cost of RefCell<T> is: if you don’t follow the rules, it won’t tell you at compile time, but will directly crash your program (panic) at runtime!

Its rules are:

  • At any time, you can only have one “mutable borrow” (borrow_mut).
  • When you have a “mutable borrow”, you can’t have any “immutable borrows” (borrow).

If you apply for two borrow_mut at the same time, or try to borrow while borrow_mut still exists, the program will shout “You violated the rules!” and explode on the spot.

Ultimate Combination: Rc<RefCell<T>> — Shared Ownership, Plus Internal Modification!

Alright, now let’s combine toy boxes #2 and #3, and we get the ultimate artifact Rc<RefCell<T>>.

Rc is responsible for letting multiple owners share this “transparent display case”. RefCell is responsible for letting people with permissions modify the things inside the case at runtime.

This is your perfect solution when you need “multiple ownership” and “mutability”. For example, in a graph structure, a node might be referenced by multiple other nodes, and its own state also needs to be modified.

Summary: How to choose?

  • Want to put big things on the heap, and there’s only one owner? Use Box<T>.
  • Want data to be shared read-only by multiple parts? Use Rc<T>.
  • Want to temporarily “break” borrowing rules in single-threaded code, implementing interior mutability? Use RefCell<T>.
  • Want data to be shared by multiple parts, and also modifiable? Use Rc<RefCell<T>>.
  • (Secret preview) If you’re playing with sharing and modification in a multi-threaded environment? Then you’ll need atomic reference counting Arc and mutex locks Mutex, we’ll cover that next time!

See, through these vivid “toy boxes”, Rust’s ownership and memory management suddenly becomes crystal clear, doesn’t it?

Follow Dream Beast Programming WeChat Official Account, unlock more black tech, let’s make programming bloom with creativity!