Search code examples
algorithmautomatic-ref-countingtheory

How does ARC deal with circular linked lists when the external head reference is removed?


I've finally gotten around to learning about linked lists and all that. I understand that in a garbage collected environment, when a circular linked list head is dereferenced, the entire list is eventually GC'd.

I have quite a lot of experience with Objective-C and Swift which all use automatic reference counting instead of GC. As I understand it, this will create a strong reference cycle (as explained by Apple's ARC docs) since the head node will hold a strong reference to the back, which also holds a strong reference to the head. I've encountered strong retain cycles by accidentally doing exactly this.

This, in my mind, creates a major issue because it makes a circular linked list entirely unreleasable since under ARC, at least in Objective C and Swift, you cannot manually release objects.

Is the solution here to just to keep an extra weak reference property on each node for the "front" and "back" reference as they cannot use the common next reference (as these would need to be strong or else nodes in the middle of the list would be deallocated)? Or should I be pseudo-manually deallocating my list by breaking all references in the list before I remove the final reference to the head? All of these seem less than beautiful and more hacks than solutions.


Solution

  • Indeed, having a circular Linked List, where you got last node strongly referencing first node will result in first node always having at least 1 reference, and hence the list will always remain in the memory.

    Though ARC Wikipedia article suggests, that sometimes such cycles can be ignored, in this case it is unacceptable, because as a data structure designers, we do not control the size of the linked list — it can and should be able to grow endlessly by definition.

    My solution is to let the nodes know about their container (i.e. weakly reference the list itself). This way we could remove cyclic referencing, by making next a computed property of a node. When the actual next node is set, we return it as next. When it is nil, we return container's startNode, and so we are having the API, that presents itself as a Circular Linked List, but under the hood its just a usual one.

    Here's some code sample:

    class CircularLinkedList<T> {
        // MARK: - Internal structures
        class Node {
            let value: T
            private var _next: Node?
    
            weak fileprivate var container: CircularLinkedList<T>!
    
            var next: Node! {
                get {
                    if let next = _next {
                        return next
                    } else {
                        return container.startNode
                    }
                }
    
                set {
                    _next = newValue
                }
            }
    
            init(value: T) {
                self.value = value
            }
        }
    
        // MARK: - Properties
        var startNode: Node
    
        var endNode: Node
    
        // MARK: - Constructors
        init(initialValue: T) {
            startNode = Node(value: initialValue)
            endNode = startNode
    
            startNode.container = self
        }
    
        // MARK: - API
        func append(newValue: T) {
            let newNode = Node(value: newValue)
            newNode.container = self
    
            endNode.next = newNode
            endNode = newNode
        }
    }
    

    One might say, that letting the Node know about its container list is not a good idea architecture-wise. But making it fileprivate we're hiding it from the outside world, and inside the data structure we know that we should use it properly. It seems like a better solution than manually breaking the reference cycle during the list's lifetime.