Skip to Content
Course content

76: Implementing a Linked List in Safe Rust

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you're coming from C or Java, you probably think a linked list is the "Hello World" of data structures. In Rust, it's more like the final boss of the ownership system. I've seen plenty of talented engineers hit a wall here because they try to treat Rust pointers like C pointers, only to have the borrow checker scream at them for twenty minutes.

Let's look at a common mistake. You're trying to implement a simple pop method for a singly linked list. You want to take the head node, move the pointer to the next node, and return the value of the old head. It looks intuitive, but it won't compile:

struct Node {
    value: i32,
    next: Option<Box<Node>>,
}

struct List {
    head: Option<Box<Node>>,
}

impl List {
    fn pop(&mut self) -> Option<i32> {
        // This is where the disaster happens
        if self.head.is_some() {
            let old_head = self.head.unwrap(); 
            self.head = old_head.next; 
            Some(old_head.value)
        } else {
            None
        }
    }
}

The "Cannot Move Out of Borrowed Content" Wall

If you try to compile this, Rust will throw a fit. The specific error is usually something like cannot move out of self.head which is behind a mutable reference.

Here is what's happening: self.head is inside the List struct, and the pop method takes &mut self. When you call self.head.unwrap(), you're trying to move the Box out of the List. But you don't own the List; you only have a mutable reference to it. Rust refuses to let you leave the List in an invalid state (where head is suddenly empty/undefined) while you're only borrowing the list.

Using Option::take to Swap Ownership

To fix this, we need a way to tell Rust: "I'm taking the value out of this spot, but I'm leaving something (None) in its place immediately." This is exactly what Option::take() is for. It replaces the value at the destination with None and returns the original value.

Here is the corrected pop implementation:

impl List {
    fn pop(&mut self) -> Option<i32> {
        // .take() pulls the Box out of self.head and leaves None behind
        self.head.take().map(|node| {
            // Now we own the 'node' Box. 
            // We put the next node back into the head of the list.
            self.head = node.next;
            node.value
        })
    }
}

By using take(), we satisfy the borrow checker. We aren't "stealing" the value and leaving a hole; we're performing a clean swap. Once we have ownership of the node, we can safely move node.next back into self.head before the node itself is dropped at the end of the closure.

Managing the Box Chain

When building a linked list in safe Rust, Box<T> is your best friend for singly linked lists because it provides a fixed size on the stack while the actual data lives on the heap. However, remember that Box implies unique ownership. This is why a doubly linked list is nearly impossible in safe Rust—you can't have two different nodes owning each other with Box.

If you want to implement a push method, you'll follow a similar pattern of ownership transfer. You create a new node, give it ownership of the current head, and then make the new node the new head.

impl List {
    fn push(&mut self, value: i32) {
        let new_node = Box::new(Node {
            value,
            // Move the current head into the next field of the new node
            next: self.head.take(),
        });
        
        // Set the new node as the head
        self.head = Some(new_node);
    }
}

I personally find it helpful to visualize this as a relay race. The "ownership" baton is being passed from the list to the node, and from the node to the next node. If you try to hold the baton in two places at once, the compiler will stop you.




📋 Practical Task

Exercise: Implementing a Singly Linked List Stack with Length Tracking

Build a Stack data structure based on a singly linked list. Your implementation must include the following:

  • A Node struct and a Stack struct.
  • The Stack must track its current length: usize.
  • A push(val: i32) method that adds an element to the top and increments the length.
  • A pop() -> Option<i32> method that removes the top element, decrements the length, and returns the value.
  • A peek() -> Option<&i32> method that returns a reference to the top value without removing it.

Requirement: You must use Option::take() in your pop method to handle ownership correctly. Test your stack by pushing three values and popping them all to ensure the length returns to zero.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.