Rust - Weak References

Overview

Estimated time: 45–60 minutes

Master weak references in Rust to break reference cycles and manage memory efficiently. Learn when and how to use Weak<T> with Rc and Arc for preventing memory leaks in complex data structures.

Learning Objectives

Prerequisites

The Reference Cycle Problem

Reference cycles can cause memory leaks when using Rc<T>:

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell>>,
    children: RefCell>>,
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("Dropping node with value {}", self.value);
    }
}

fn create_cycle() {
    let parent = Rc::new(Node {
        value: 1,
        parent: RefCell::new(None),
        children: RefCell::new(Vec::new()),
    });
    
    let child = Rc::new(Node {
        value: 2,
        parent: RefCell::new(Some(Rc::clone(&parent))),
        children: RefCell::new(Vec::new()),
    });
    
    parent.children.borrow_mut().push(Rc::clone(&child));
    
    println!("Parent strong count: {}", Rc::strong_count(&parent));
    println!("Child strong count: {}", Rc::strong_count(&child));
    
    // This creates a cycle: parent -> child -> parent
    // Neither will be dropped when this function ends!
}

fn main() {
    println!("Creating reference cycle...");
    create_cycle();
    println!("Function ended - nodes may not be dropped due to cycle!");
    
    // Give some time to see if Drop is called
    std::thread::sleep(std::time::Duration::from_millis(100));
    println!("Program ending...");
}

Expected output:

Creating reference cycle...
Parent strong count: 2
Child strong count: 2
Function ended - nodes may not be dropped due to cycle!
Program ending...

Breaking Cycles with Weak References

Use Weak<T> to break reference cycles:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell>>, // Weak reference to parent
    children: RefCell>>,    // Strong references to children
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("Dropping node with value {}", self.value);
    }
}

fn create_tree() {
    let parent = Rc::new(Node {
        value: 1,
        parent: RefCell::new(None),
        children: RefCell::new(Vec::new()),
    });
    
    let child = Rc::new(Node {
        value: 2,
        parent: RefCell::new(Some(Rc::downgrade(&parent))), // Weak reference
        children: RefCell::new(Vec::new()),
    });
    
    parent.children.borrow_mut().push(Rc::clone(&child));
    
    println!("Parent strong count: {}", Rc::strong_count(&parent));
    println!("Parent weak count: {}", Rc::weak_count(&parent));
    println!("Child strong count: {}", Rc::strong_count(&child));
    
    // Access parent through weak reference
    if let Some(parent_ref) = child.parent.borrow().as_ref() {
        if let Some(parent_strong) = parent_ref.upgrade() {
            println!("Child's parent value: {}", parent_strong.value);
        }
    }
}

fn main() {
    println!("Creating tree with weak references...");
    create_tree();
    println!("Function ended - nodes should be properly dropped!");
    
    std::thread::sleep(std::time::Duration::from_millis(100));
    println!("Program ending...");
}

Expected output:

Creating tree with weak references...
Parent strong count: 1
Parent weak count: 1
Child strong count: 2
Child's parent value: 1
Function ended - nodes should be properly dropped!
Dropping node with value 2
Dropping node with value 1
Program ending...

Tree Data Structure with Weak References

Implement a proper tree using weak references for parent pointers:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

type TreeNode = Rc>;
type WeakNode = Weak>;

#[derive(Debug)]
struct Node {
    value: String,
    parent: Option,
    children: Vec,
}

impl Node {
    fn new(value: &str) -> TreeNode {
        Rc::new(RefCell::new(Node {
            value: value.to_string(),
            parent: None,
            children: Vec::new(),
        }))
    }
    
    fn add_child(parent: &TreeNode, child: TreeNode) {
        child.borrow_mut().parent = Some(Rc::downgrade(parent));
        parent.borrow_mut().children.push(child);
    }
    
    fn print_tree(node: &TreeNode, indent: usize) {
        let node_ref = node.borrow();
        println!("{:indent$}{}", "", node_ref.value, indent = indent);
        
        for child in &node_ref.children {
            Self::print_tree(child, indent + 2);
        }
    }
    
    fn get_parent_value(node: &TreeNode) -> Option {
        let node_ref = node.borrow();
        node_ref.parent.as_ref()
            .and_then(|weak_parent| weak_parent.upgrade())
            .map(|parent| parent.borrow().value.clone())
    }
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("Dropping node: {}", self.value);
    }
}

fn main() {
    let root = Node::new("Root");
    let child1 = Node::new("Child 1");
    let child2 = Node::new("Child 2");
    let grandchild = Node::new("Grandchild");
    
    // Build the tree
    Node::add_child(&root, child1.clone());
    Node::add_child(&root, child2.clone());
    Node::add_child(&child1, grandchild.clone());
    
    println!("Tree structure:");
    Node::print_tree(&root, 0);
    
    // Access parent information
    println!("\nParent relationships:");
    if let Some(parent) = Node::get_parent_value(&child1) {
        println!("Child 1's parent: {}", parent);
    }
    
    if let Some(parent) = Node::get_parent_value(&grandchild) {
        println!("Grandchild's parent: {}", parent);
    }
    
    println!("\nReference counts:");
    println!("Root strong: {}, weak: {}", 
             Rc::strong_count(&root), Rc::weak_count(&root));
    println!("Child1 strong: {}, weak: {}", 
             Rc::strong_count(&child1), Rc::weak_count(&child1));
}

Expected output:

Tree structure:
Root
  Child 1
    Grandchild
  Child 2

Parent relationships:
Child 1's parent: Root
Grandchild's parent: Child 1

Reference counts:
Root strong: 1, weak: 2
Child1 strong: 2, weak: 1
Dropping node: Root
Dropping node: Child 1
Dropping node: Grandchild
Dropping node: Child 2

Observer Pattern with Weak References

Implement an observer pattern using weak references:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

trait Observer {
    fn notify(&self, message: &str);
}

struct Subject {
    observers: RefCell>>,
}

impl Subject {
    fn new() -> Self {
        Subject {
            observers: RefCell::new(Vec::new()),
        }
    }
    
    fn add_observer(&self, observer: Weak) {
        self.observers.borrow_mut().push(observer);
    }
    
    fn notify_all(&self, message: &str) {
        // Clean up dead weak references and notify living ones
        self.observers.borrow_mut().retain(|weak_observer| {
            if let Some(observer) = weak_observer.upgrade() {
                observer.notify(message);
                true // Keep this observer
            } else {
                false // Remove dead weak reference
            }
        });
    }
    
    fn observer_count(&self) -> usize {
        self.observers.borrow().len()
    }
}

struct ConcreteObserver {
    name: String,
}

impl ConcreteObserver {
    fn new(name: &str) -> Rc {
        Rc::new(ConcreteObserver {
            name: name.to_string(),
        })
    }
}

impl Observer for ConcreteObserver {
    fn notify(&self, message: &str) {
        println!("{} received: {}", self.name, message);
    }
}

impl Drop for ConcreteObserver {
    fn drop(&mut self) {
        println!("Observer {} is being dropped", self.name);
    }
}

fn main() {
    let subject = Subject::new();
    
    // Create observers
    let observer1 = ConcreteObserver::new("Observer1");
    let observer2 = ConcreteObserver::new("Observer2");
    let observer3 = ConcreteObserver::new("Observer3");
    
    // Add observers using weak references
    subject.add_observer(Rc::downgrade(&observer1));
    subject.add_observer(Rc::downgrade(&observer2));
    subject.add_observer(Rc::downgrade(&observer3));
    
    println!("Initial observer count: {}", subject.observer_count());
    
    // Notify all observers
    subject.notify_all("First message");
    
    // Drop one observer
    drop(observer2);
    println!("\nAfter dropping observer2:");
    
    // This will clean up the dead weak reference
    subject.notify_all("Second message");
    println!("Observer count after cleanup: {}", subject.observer_count());
    
    // Drop another observer
    drop(observer1);
    println!("\nAfter dropping observer1:");
    subject.notify_all("Third message");
    println!("Final observer count: {}", subject.observer_count());
}

Expected output:

Initial observer count: 3
Observer1 received: First message
Observer2 received: First message
Observer3 received: First message

Observer Observer2 is being dropped
After dropping observer2:
Observer1 received: Second message
Observer3 received: Second message
Observer count after cleanup: 2

Observer Observer1 is being dropped
After dropping observer1:
Observer3 received: Third message
Final observer count: 1
Observer Observer3 is being dropped

Weak References with Arc (Thread-Safe)

Use weak references in multi-threaded contexts:

use std::sync::{Arc, Weak, Mutex};
use std::thread;
use std::time::Duration;

struct SharedResource {
    id: usize,
    data: Mutex,
}

impl SharedResource {
    fn new(id: usize, data: &str) -> Arc {
        Arc::new(SharedResource {
            id,
            data: Mutex::new(data.to_string()),
        })
    }
    
    fn update_data(&self, new_data: &str) {
        let mut data = self.data.lock().unwrap();
        *data = new_data.to_string();
        println!("Resource {} updated to: {}", self.id, data);
    }
    
    fn get_data(&self) -> String {
        self.data.lock().unwrap().clone()
    }
}

impl Drop for SharedResource {
    fn drop(&mut self) {
        println!("Dropping resource {}", self.id);
    }
}

fn worker_thread(weak_resource: Weak, worker_id: usize) {
    for i in 0..5 {
        thread::sleep(Duration::from_millis(100));
        
        // Try to upgrade the weak reference
        if let Some(resource) = weak_resource.upgrade() {
            let data = resource.get_data();
            println!("Worker {} (iteration {}): Resource data = '{}'", 
                     worker_id, i, data);
        } else {
            println!("Worker {}: Resource has been dropped", worker_id);
            break;
        }
    }
}

fn main() {
    let resource = SharedResource::new(1, "Initial data");
    
    // Create weak references for worker threads
    let weak1 = Arc::downgrade(&resource);
    let weak2 = Arc::downgrade(&resource);
    
    // Spawn worker threads with weak references
    let handle1 = thread::spawn(move || worker_thread(weak1, 1));
    let handle2 = thread::spawn(move || worker_thread(weak2, 2));
    
    // Update resource data
    thread::sleep(Duration::from_millis(250));
    resource.update_data("Updated data");
    
    // Drop the main resource after a delay
    thread::sleep(Duration::from_millis(200));
    println!("Main thread dropping resource...");
    drop(resource);
    
    // Wait for worker threads to complete
    handle1.join().unwrap();
    handle2.join().unwrap();
    
    println!("All threads completed");
}

Expected output:

Worker 1 (iteration 0): Resource data = 'Initial data'
Worker 2 (iteration 0): Resource data = 'Initial data'
Worker 1 (iteration 1): Resource data = 'Initial data'
Worker 2 (iteration 1): Resource data = 'Initial data'
Resource 1 updated to: Updated data
Worker 1 (iteration 2): Resource data = 'Updated data'
Worker 2 (iteration 2): Resource data = 'Updated data'
Main thread dropping resource...
Dropping resource 1
Worker 1: Resource has been dropped
Worker 2: Resource has been dropped
All threads completed

Best Practices

1. When to Use Weak References

use std::rc::{Rc, Weak};
use std::cell::RefCell;

// Good: Parent-child relationships in trees
struct TreeNode {
    parent: Option>,  // Weak to parent
    children: Vec>,     // Strong to children
}

// Good: Observer pattern
struct Subject {
    observers: RefCell>>,
}

// Good: Cache-like structures
struct Cache {
    items: RefCell>>,
}

fn main() {
    println!("Use weak references for:");
    println!("1. Breaking reference cycles");
    println!("2. Parent pointers in trees");
    println!("3. Observer patterns");
    println!("4. Cache implementations");
    println!("5. Callback registrations");
}

2. Always Check if Weak References are Still Valid

use std::rc::{Rc, Weak};

fn safe_access(weak_ref: &Weak) -> Option {
    // Always use upgrade() and handle the None case
    weak_ref.upgrade().map(|strong| strong.clone())
}

fn unsafe_access(weak_ref: &Weak) -> String {
    // Don't do this - it will panic if the reference is dead
    // weak_ref.upgrade().unwrap().clone()
    
    // Do this instead
    weak_ref.upgrade()
        .map(|strong| strong.clone())
        .unwrap_or_else(|| "Reference is dead".to_string())
}

fn main() {
    let strong = Rc::new("Hello".to_string());
    let weak = Rc::downgrade(&strong);
    
    println!("Before drop: {:?}", safe_access(&weak));
    
    drop(strong);
    
    println!("After drop: {:?}", safe_access(&weak));
    println!("Safe fallback: {}", unsafe_access(&weak));
}

Checks for Understanding

Question 1: What happens when you try to upgrade() a dead weak reference?

Answer: upgrade() returns None when the referenced object has been dropped. Always handle this case to avoid panics.

Question 2: Why use weak references for parent pointers in tree structures?

Answer: Using strong references for both parent and child relationships creates reference cycles, preventing nodes from being dropped. Weak references to parents break the cycle while maintaining the relationship.

Question 3: Can you have multiple weak references to the same object?

Answer: Yes, you can create multiple weak references to the same object. Each weak reference can be upgraded independently, and the object is only dropped when all strong references are gone.