You struck my curiosity. The class for ReadOnlyNode is simple enough to define:
public class ReadOnlyNode<T> { public readonly T Value; public readonly ReadOnlyNode<T> Next; public readonly ReadOnlyNode<T> Prev; public Node(T value, ReadOnlyNode<T> next, ReadOnlyNode<T> prev) { Value = value; Next = next; Prev = prev; } }
The problem with readonly
in a doubly linked list is that for each node you need to indicate that the node is the previous and next nodes in the constructor, so if they are passed from outside the constructor, they must already exist. But, node M needs the pre-existing node N as the βnextβ node when calling the constructor, but node N requires M as its βpreviousβ node in order to be built. This creates a chicken and egg situation, where both N and M need the second node to be created first.
However, there is more than one way to trick this cat. What if each list node was recursively created from the constructor of a single ReadOnlyNode? Until each constructor is complete, the properties at each level will still be mutable, and a link to each node will exist in its constructor, so it does not matter that not everything was configured until everything was configured. The following code compiles and, taking into account the pre-existing IEnumerable, creates an immutable doubly linked list:
public class ReadOnlyNode<T> { public readonly T Value; public readonly ReadOnlyNode<T> Next; public readonly ReadOnlyNode<T> Prev; private ReadOnlyNode(IEnumerable<T> elements, ReadOnlyNode<T> prev) { if(elements == null || !elements.Any()) throw new ArgumentException( "Enumerable must not be null and must have at least one element"); Next = elements.Count() == 1 ? null : new ReadOnlyNode<T>(elements.Skip(1), this); Value = elements.First(); Prev = prev; } public ReadOnlyNode(IEnumerable<T> elements) : this(elements, null) { } }
You can use this with any collection that implements IEnumerable (almost all built-in collections do, and you can use OfType () to turn non-common ICollections and IEnumerables into common IEnumerables). The only thing to worry about is the call stack; there is a limit to the number of call calls you can nest, which can cause SOE in a finite but large list of entries.
EDIT: JaredPar raises a very good point; this solution uses Count () and Any (), which should take into account the results of Skip (), and therefore cannot use the "shortcuts" built into these methods, which can use the power property of the collection class. These calls become linear, which complicates the complexity of the algorithm. If you simply use the basic elements of IEnumerable, this becomes much more efficient:
public class ReadOnlyNode<T> { public readonly T Value; public readonly ReadOnlyNode<T> Next; public readonly ReadOnlyNode<T> Prev; private ReadOnlyNode(IEnumerator<T> elements, ReadOnlyNode<T> prev, bool first) { if (elements == null) throw new ArgumentNullException("elements"); var empty = false; if (first) empty = elements.MoveNext(); if(!empty) { Value = elements.Current; Next = elements.MoveNext() ? new ReadOnlyNode<T>(elements, this, false) : null; Prev = prev; } } public ReadOnlyNode(IEnumerable<T> elements) : this(elements.GetEnumerator(), null, true) { } }
With this solution, you will lose a slightly more elegant error checking, but if IEnumerable is null, an exception will still be thrown.