这更像是一个理论问题:在 C# 中是否可以通过任何方式创建一个真正不可变的双向链表?我看到的一个问题是两个相邻节点的相互依赖。
“真正”是指使用只读字段。
这更像是一个理论问题:在 C# 中是否可以通过任何方式创建一个真正不可变的双向链表?我看到的一个问题是两个相邻节点的相互依赖。
“真正”是指使用只读字段。
这可能与棘手的构造函数逻辑有关。例如
public sealed class Node<T> {
readonly T m_data;
readonly Node<T> m_prev;
readonly Node<T> m_next;
// Data, Next, Prev accessors omitted for brevity
public Node(T data, Node<T> prev, IEnumerator<T> rest) {
m_data = data;
m_prev = prev;
if (rest.MoveNext()) {
m_next = new Node(rest.Current, this, rest);
}
}
}
public static class Node {
public static Node<T> Create<T>(IEnumerable<T> enumerable) {
using (var enumerator = enumerable.GetEnumerator()) {
if (!enumerator.MoveNext()) {
return null;
}
return new Node(enumerator.Current, null, enumerator);
}
}
}
Node<string> list = Node.Create(new [] { "a", "b", "c", "d" });
你激起了我的好奇心。ReadOnlyNode 的类很简单,可以定义:
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;
}
}
readonly
双向链表的问题在于,对于每个节点,您必须在构造函数中指定该节点的前一个和下一个节点,因此如果它们是从构造函数外部传递的,它们必须已经存在。但是,当您调用构造函数时,节点 M 需要一个预先存在的节点 N 作为其“下一个”节点,但该节点 N 需要 M 作为其“上一个”节点才能被构造。这会产生“鸡和蛋”的情况,其中 N 和 M 都需要首先实例化另一个节点。
然而,给这只猫剥皮的方法不止一种。如果列表的每个节点都是从一个 ReadOnlyNode 的构造函数中递归实例化的会怎样?在每个构造函数完成之前,每个级别的属性仍然是可变的,并且对每个节点的引用将存在于其构造函数中,因此在所有内容都设置好之前没有设置所有内容并不重要。下面的代码编译,给定一个预先存在的 IEnumerable 将产生一个不可变的双向链表:
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)
{
}
}
//Usage - creates an immutable doubly-linked list of integers from 1 to 1000
var immutableList = new ReadOnlyNode<int>(Enumerable.Range(1,1000));
您可以将它与任何实现 IEnumerable 的集合一起使用(几乎所有内置集合都可以,并且您可以使用 OfType() 将非泛型 ICollections 和 IEnumerables 转换为泛型 IEnumerables)。唯一需要担心的是调用堆栈;您可以嵌套多少方法调用是有限制的,这可能会导致 SOE 出现在有限但很大的输入列表上。
编辑: JaredPar 提出了一个很好的观点;此解决方案使用 Count() 和 Any() ,它们必须考虑 Skip() 的结果,因此不能使用这些方法中内置的“快捷方式”,这些方法可以使用集合类的基数属性。这些调用变成线性的,这使算法的复杂性平方。如果您只使用 IEnumerable 的基本成员,这将变得更加高效:
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)
{
}
}
使用此解决方案,您将失去一些更优雅的错误检查,但如果 IEnumerable 为空,则无论如何都会引发异常。
是的,您可以创建一个用于设置链接的“link-setter”对象,将其发送到节点的构造函数中,或者拥有一个返回“link-setter”的静态创建方法。节点中的链接是私有的,只能通过“链接设置器”访问,当您使用它们设置列表时,您将它们丢弃。
然而,这是一个相当无用的练习。如果列表是不可变的,那么当简单数组效果更好时,使用双向链表是没有意义的。