Breaking cycles in graph structures
Weak is a non-owning reference to Rc or Arc. It doesn't increase the reference count, so it doesn't prevent the value from being dropped.
The Problem: Reference Cycles
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
next: Option<Rc<RefCell<Node>>>,
}
// Create cycle: A โ B โ A
let a = Rc::new(RefCell::new(Node { next: None }));
let b = Rc::new(RefCell::new(Node { next: Some(Rc::clone(&a)) }));
a.borrow_mut().next = Some(Rc::clone(&b));
// Memory leak! Both have ref count 2, neither drops
The Solution: Weak References
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
next: Option<Rc<RefCell<Node>>>,
prev: Weak<RefCell<Node>>, // Weak doesn't prevent drop
}
use std::rc::{Rc, Weak};
use std::cell::RefCell;
/// DOM-like tree with parent-child relationships
pub struct TreeNode {
pub value: String,
pub parent: RefCell<Weak<TreeNode>>, // Weak to parent
pub children: RefCell<Vec<Rc<TreeNode>>>, // Strong to children
}
impl TreeNode {
pub fn new(value: impl Into<String>) -> Rc<Self> {
Rc::new(TreeNode {
value: value.into(),
parent: RefCell::new(Weak::new()),
children: RefCell::new(Vec::new()),
})
}
pub fn add_child(parent: &Rc<TreeNode>, child: Rc<TreeNode>) {
// Set child's parent (weak reference)
*child.parent.borrow_mut() = Rc::downgrade(parent);
// Add child to parent's children (strong reference)
parent.children.borrow_mut().push(child);
}
pub fn get_parent(&self) -> Option<Rc<TreeNode>> {
// Upgrade weak to strong (may fail if parent was dropped)
self.parent.borrow().upgrade()
}
pub fn print_path_to_root(&self) {
print!("{}", self.value);
if let Some(parent) = self.get_parent() {
print!(" โ ");
parent.print_path_to_root();
} else {
println!(" (root)");
}
}
}
// Usage
fn tree_example() {
let root = TreeNode::new("root");
let child1 = TreeNode::new("child1");
let grandchild = TreeNode::new("grandchild");
TreeNode::add_child(&root, Rc::clone(&child1));
TreeNode::add_child(&child1, Rc::clone(&grandchild));
// Navigate up the tree
grandchild.print_path_to_root();
// Check reference counts
println!("Root strong count: {}", Rc::strong_count(&root)); // 1
println!("Child1 strong count: {}", Rc::strong_count(&child1)); // 2
println!("Root weak count: {}", Rc::weak_count(&root)); // 1 (child1's parent ref)
}
use std::rc::{Rc, Weak};
use std::cell::RefCell;
/// Event subject that notifies observers
pub struct Subject {
observers: RefCell<Vec<Weak<dyn Observer>>>,
state: RefCell<i32>,
}
pub trait Observer {
fn update(&self, state: i32);
}
impl Subject {
pub fn new() -> Rc<Self> {
Rc::new(Subject {
observers: RefCell::new(Vec::new()),
state: RefCell::new(0),
})
}
/// Register observer (weak reference)
pub fn attach(&self, observer: Weak<dyn Observer>) {
self.observers.borrow_mut().push(observer);
}
/// Update state and notify all observers
pub fn set_state(&self, new_state: i32) {
*self.state.borrow_mut() = new_state;
self.notify();
}
fn notify(&self) {
// Clean up dead observers while notifying
self.observers.borrow_mut().retain(|weak_obs| {
if let Some(observer) = weak_obs.upgrade() {
observer.update(*self.state.borrow());
true // Keep observer
} else {
false // Remove dead observer
}
});
}
}
/// Concrete observer
struct ConcreteObserver {
id: usize,
}
impl Observer for ConcreteObserver {
fn update(&self, state: i32) {
println!("Observer {} received update: state = {}", self.id, state);
}
}
// Usage
fn observer_example() {
let subject = Subject::new();
{
let obs1 = Rc::new(ConcreteObserver { id: 1 });
let obs2 = Rc::new(ConcreteObserver { id: 2 });
subject.attach(Rc::downgrade(&obs1));
subject.attach(Rc::downgrade(&obs2));
subject.set_state(10); // Both observers notified
// obs2 goes out of scope here
}
// obs2 dropped, only obs1's weak ref remains (but it's also out of scope)
subject.set_state(20); // No observers (auto-cleaned)
}
use std::rc::{Rc, Weak};
use std::cell::RefCell;
use std::collections::HashMap;
/// Cache entry that can be evicted
pub struct CachedValue {
pub data: String,
pub size: usize,
}
/// Cache that holds weak references to allow eviction
pub struct WeakCache {
entries: RefCell<HashMap<String, Weak<CachedValue>>>,
total_size: RefCell<usize>,
max_size: usize,
}
impl WeakCache {
pub fn new(max_size: usize) -> Self {
WeakCache {
entries: RefCell::new(HashMap::new()),
total_size: RefCell::new(0),
max_size,
}
}
/// Get value from cache
pub fn get(&self, key: &str) -> Option<Rc<CachedValue>> {
self.entries.borrow().get(key)?.upgrade()
}
/// Insert value into cache (stores weak reference)
pub fn insert(&self, key: String, value: Rc<CachedValue>) {
let weak = Rc::downgrade(&value);
self.entries.borrow_mut().insert(key, weak);
*self.total_size.borrow_mut() += value.size;
}
/// Cleanup dead entries
pub fn cleanup(&self) {
let mut entries = self.entries.borrow_mut();
let mut total_size = self.total_size.borrow_mut();
entries.retain(|_, weak| {
if let Some(value) = weak.upgrade() {
true // Still alive
} else {
// Entry died, update size
false
}
});
// Recalculate total size
*total_size = entries.values()
.filter_map(|w| w.upgrade())
.map(|v| v.size)
.sum();
println!("Cache cleanup: {} entries, {} bytes", entries.len(), *total_size);
}
}
// Usage
fn cache_example() {
let cache = WeakCache::new(1000);
{
let value1 = Rc::new(CachedValue {
data: "data1".to_string(),
size: 100,
});
cache.insert("key1".to_string(), Rc::clone(&value1));
// value1 in scope, cache hit
assert!(cache.get("key1").is_some());
}
// value1 dropped, weak reference becomes invalid
assert!(cache.get("key1").is_none());
cache.cleanup(); // Removes dead entry
}
use std::rc::{Rc, Weak};
let strong = Rc::new(42);
let weak = Rc::downgrade(&strong);
// Try to upgrade weak to strong
match weak.upgrade() {
Some(value) => println!("Value still alive: {}", value),
None => println!("Value was dropped"),
}
drop(strong); // Drop the last strong reference
// Now upgrade fails
assert!(weak.upgrade().is_none());
upgrade() is O(1) - just checks counter and increments if non-zero
let strong = Rc::new(42);
let weak1 = Rc::downgrade(&strong);
let weak2 = Rc::downgrade(&strong);
println!("Strong count: {}", Rc::strong_count(&strong)); // 1
println!("Weak count: {}", Rc::weak_count(&strong)); // 2
// Value dropped when strong count reaches 0
// Weak references just become invalid
use std::sync::{Arc, Weak};
use std::thread;
let data = Arc::new(vec![1, 2, 3]);
let weak = Arc::downgrade(&data);
thread::spawn(move || {
if let Some(data) = weak.upgrade() {
println!("Data still alive: {:?}", data);
} else {
println!("Data was dropped");
}
}).join().unwrap();
use std::rc::{Rc, Weak};
use std::cell::RefCell;
pub struct Graph {
nodes: Vec<Rc<RefCell<GraphNode>>>,
}
pub struct GraphNode {
pub id: usize,
pub neighbors: Vec<Weak<RefCell<GraphNode>>>, // Weak to prevent cycles
}
impl Graph {
pub fn new() -> Self {
Graph { nodes: Vec::new() }
}
pub fn add_node(&mut self, id: usize) -> Rc<RefCell<GraphNode>> {
let node = Rc::new(RefCell::new(GraphNode {
id,
neighbors: Vec::new(),
}));
self.nodes.push(Rc::clone(&node));
node
}
pub fn add_edge(
&self,
from: &Rc<RefCell<GraphNode>>,
to: &Rc<RefCell<GraphNode>>,
) {
from.borrow_mut().neighbors.push(Rc::downgrade(to));
}
pub fn traverse(&self, start_id: usize) {
if let Some(node) = self.nodes.iter().find(|n| n.borrow().id == start_id) {
self.visit(node, &mut std::collections::HashSet::new());
}
}
fn visit(&self, node: &Rc<RefCell<GraphNode>>, visited: &mut std::collections::HashSet<usize>) {
let node_ref = node.borrow();
if visited.contains(&node_ref.id) {
return;
}
println!("Visiting node {}", node_ref.id);
visited.insert(node_ref.id);
for weak_neighbor in &node_ref.neighbors {
if let Some(neighbor) = weak_neighbor.upgrade() {
drop(node_ref); // Release borrow before recursion
self.visit(&neighbor, visited);
}
}
}
}
Strong Rc:
Stack: Heap:
โโโโโโโ โโโโโโโโโโโโโโโโ
โRc<T>โโโโโโโโโโโบ โstrong: 2 โ
โโโโโโโ โweak: 1 โ
โvalue: T โ
โโโโโโโโโโโโโโโโ
Weak Rc:
Stack: (same heap)
โโโโโโโโ โโโโโโโโโโโโโโโโ
โWeak<T>โโโโโโโโโโบ โstrong: 2 โ
โโโโโโโโ โweak: 1 โ
โvalue: T โ
โโโโโโโโโโโโโโโโ
Key Points:
// BAD: Unwrapping upgrade without checking
let weak: Weak<Data> = ...;
let strong = weak.upgrade().unwrap(); // PANIC if value dropped!
// GOOD: Handle None case
if let Some(strong) = weak.upgrade() {
// Use strong
} else {
// Handle case where value is gone
}
// BAD: Creates cycle
struct Node {
parent: Option<Rc<Node>>, // Strong reference
children: Vec<Rc<Node>>, // Strong references
}
// Parent keeps children alive, children keep parent alive = LEAK
// GOOD: Break cycle with Weak
struct Node {
parent: Weak<Node>, // Weak to parent
children: Vec<Rc<Node>>, // Strong to children
}
// BAD: Accumulates dead weak references
struct Subject {
observers: Vec<Weak<Observer>>,
}
impl Subject {
fn notify(&self) {
for weak in &self.observers {
if let Some(obs) = weak.upgrade() {
obs.update();
}
// Dead observers accumulate!
}
}
}
// GOOD: Clean up during iteration
fn notify(&mut self) {
self.observers.retain(|weak| {
if let Some(obs) = weak.upgrade() {
obs.update();
true // Keep
} else {
false // Remove dead observer
}
});
}
| Operation | Cost | Notes |
|-----------|------|-------|
| Downgrade | ~2 cycles | Increment weak_count |
| Upgrade | ~5-10 cycles | Check + increment strong_count |
| Drop Weak | ~2 cycles | Decrement weak_count |
| Size | 8 bytes | One pointer |
Implement a doubly-linked list using Rc + Weak.
Hints:Build an event bus where subscribers use weak references.
Hints:Implement undo/redo with weak references to commands.
Hints:Uses Weak for widget parent references to prevent cycles.
View on GitHubBrowser engine uses Weak for DOM parent references.
View on GitHubUses Weak for task waker references.
View on GitHubRun this code in the official Rust Playground