Skip to Content
Course content

144: Whiteboard Practice: Implementing a Linked List with Ownership Constraints

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

Implementing a linked list is a rite of passage in any language, but in Rust, it's less of a data structure exercise and more of a wrestling match with the borrow checker. When you're at a whiteboard, it's easy to draw arrows and think "this just points to that." In Rust, those arrows are ownership transfers, and if you don't account for them, the compiler will stop you dead in your tracks.

Here is a piece of code that looks perfectly logical to someone coming from Java or Python, but it's a disaster in Rust. I've written a basic LinkedList and a pop method that is supposed to remove the head of the list and return its value.


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

pub struct LinkedList {
    head: Option<Box<Node>>,
}

impl LinkedList {
    pub fn pop(&mut self) -> Option<i32> {
        // The logic: take the head, move the pointer to the next node, 
        // and return the value of the old head.
        if let Some(node) = self.head {
            self.head = node.next; // ERROR: cannot move out of borrowed content
            Some(node.value)
        } else {
            None
        }
    }
}

The "Move Out of Borrowed Content" Wall

If you try to compile this, you'll get a frustrating error: cannot move out of borrowed content. Why? Because self is a mutable reference (&mut self). When you do if let Some(node) = self.head, you aren't actually taking ownership of the node; you're just looking at it. However, the line self.head = node.next attempts to move the next pointer out of that borrowed node and assign it back into self.head.

Rust won't let you do this because, for a brief moment, you're trying to move a value out of a place that you don't own. If the compiler allowed this, you'd end up with a "hole" in your memory where the head used to be, which is exactly what Rust is designed to prevent.

Using .take() to Swap Ownership

To fix this, we need a way to tell Rust: "I want to take the value out of this Option and leave None in its place temporarily." This is exactly what the Option::take() method is for. It's the secret weapon for implementing linked lists and other recursive structures in Rust.

By using take(), we move the Box out of the head and leave None there. Now that we own the node, we can freely move the next pointer back into the head without fighting the borrow checker.


impl LinkedList {
    pub fn pop(&mut self) -> Option<i32> {
        // .take() replaces self.head with None and returns the original value
        self.head.take().map(|node| {
            // Now we own 'node', so we can move 'node.next' back into 'self.head'
            self.head = node.next;
            node.value
        })
    }
}


I used .map() here instead of an if let just to keep it concise, but the magic is in self.head.take(). We've effectively swapped the ownership of the node from the list to the local variable, allowing us to dismantle the node and re-attach the rest of the list to the head.

Keep in mind that this pattern—replacing a value with a default (like None) to satisfy the borrow checker—is something you'll do constantly when dealing with Box and Option. It's the idiomatic way to handle ownership transfers in mutable structures.




📋 Practical Task

Implementing an In-Place Linked List Reverser

Now it's your turn. You have a LinkedList implementation, but it's missing a crucial feature: the ability to reverse the list without allocating a new one. You must modify the list in-place by swapping the next pointers of the nodes.

Your Task: Implement the reverse method for the following LinkedList. You will need to use Option::take() to move the nodes as you iterate through the list, otherwise, you'll run into the same ownership issues we discussed above.


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

pub struct LinkedList {
    head: Option<Box<Node>>,
}

impl LinkedList {
    pub fn new() -> Self {
        LinkedList { head: None }
    }

    pub fn push(&mut self, value: i32) {
        let new_node = Box::new(Node {
            value,
            next: self.head.take(),
        });
        self.head = Some(new_node);
    }

    pub fn reverse(&mut self) {
        // TODO: Implement the reversal logic here.
        // Hint: You'll need a variable to track the 'previous' node 
        // and 'current' node as you traverse.
    }
}

fn main() {
    let mut list = LinkedList::new();
    list.push(1);
    list.push(2);
    list.push(3);
    
    list.reverse();
    
    // Verification logic: the head should now be 1, then 2, then 3.
    let mut current = list.head;
    while let Some(node) = current {
        println!("Value: {}", node.value);
        current = node.next;
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.