Smart Pointers
JavaScript references vs Rust smart pointers
Section titled “JavaScript references vs Rust smart pointers”In JavaScript and TypeScript, every object lives on the heap and the garbage collector manages its lifetime. You never think about who owns what — the GC figures it out. Rust has no GC. Instead, it gives you a toolkit of smart pointers that express ownership and aliasing rules explicitly, at zero runtime cost.
| Smart pointer | Purpose | JS/TS analogy |
|---|---|---|
Box<T> | Heap allocation, single owner | new MyClass() — one owner, dropped when out of scope |
Rc<T> | Reference-counted shared ownership (single-thread) | GC reference counting |
Arc<T> | Same as Rc<T> but thread-safe | Shared reference across async tasks |
RefCell<T> | Interior mutability (runtime borrow checking) | { mutable: T } wrapper — you control when to borrow |
Rc<RefCell<T>> | Shared + mutable (single-thread) | Mutable shared state across multiple holders |
Box<T> — heap allocation with single ownership
Section titled “Box<T> — heap allocation with single ownership”Box<T> is the simplest smart pointer: it puts a value on the heap and gives you a single-owner pointer to it. When the Box goes out of scope, the heap memory is freed automatically.
// TypeScript — every object is heap-allocated by defaultclass Node { constructor( public value: number, public next: Node | null = null ) {}}
const list = new Node(1, new Node(2, new Node(3)));console.log(list.value); // 1console.log(list.next?.value); // 2// Box<T>: explicit heap allocation in Rust// Required for recursive types (the compiler needs a fixed-size pointer)#[derive(Debug)]enum List { Cons(i32, Box<List>), Nil,}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("{:?}", list);
// Simple Box usage let b = Box::new(5); println!("b = {b}"); // b is freed here (end of scope)}Rc<T> — shared ownership with reference counting
Section titled “Rc<T> — shared ownership with reference counting”When multiple parts of your program need to read the same value and you cannot determine which will be the last to use it at compile time, use Rc<T>. Every Rc::clone increments a reference count; when the last Rc drops, the value is freed. This is what JavaScript’s GC does automatically — Rc makes it explicit.
// TypeScript — two variables can reference the same objectconst config = { maxRetries: 3 };const serviceA = { config };const serviceB = { config }; // same object, no copyconfig.maxRetries = 5;console.log(serviceA.config.maxRetries); // 5 — shared referenceuse std::rc::Rc;
fn main() { let config = Rc::new(String::from("max_retries=3"));
let service_a = Rc::clone(&config); // increments ref count let service_b = Rc::clone(&config); // increments ref count
println!("config = {config}"); println!("service_a = {service_a}"); println!("service_b = {service_b}"); println!("ref count = {}", Rc::strong_count(&config)); // 3
drop(service_a); // decrements ref count println!("after drop: ref count = {}", Rc::strong_count(&config)); // 2}RefCell<T> — interior mutability
Section titled “RefCell<T> — interior mutability”Rust’s borrow checker enforces that you either have one mutable reference OR many immutable references — never both at once. RefCell<T> moves this check to runtime: you call .borrow() for shared access and .borrow_mut() for exclusive access. If you violate the rule at runtime, it panics instead of refusing to compile.
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);{ let mut v = data.borrow_mut(); // exclusive borrow v.push(4);} // borrow released hereprintln!("{:?}", data.borrow()); // shared borrow: [1, 2, 3, 4]Rc<RefCell<T>> — shared mutable state
Section titled “Rc<RefCell<T>> — shared mutable state”Combine Rc (multiple owners) with RefCell (interior mutability) to get the closest equivalent to JavaScript’s mutable shared references.
Try it
Section titled “Try it”use std::rc::Rc;use std::cell::RefCell;
fn main() { // Box<T>: heap allocation, single owner let b = Box::new(5); println!("b = {b}");
// Rc<T>: reference-counted shared ownership let a = Rc::new(String::from("hello")); let b2 = Rc::clone(&a); println!("a = {a}, b2 = {b2}, count = {}", Rc::strong_count(&a));
// RefCell<T>: interior mutability - borrow checking at runtime let data = RefCell::new(vec![1, 2, 3]); { let mut v = data.borrow_mut(); v.push(4); } println!("data = {:?}", data.borrow());
// Rc<RefCell<T>>: shared mutable state let shared = Rc::new(RefCell::new(0)); let clone1 = Rc::clone(&shared); *clone1.borrow_mut() += 10; println!("shared = {}", shared.borrow());}Compiling…