简体   繁体   English

如何在C#中创建真正不可变的双向链表?

[英]how can I create a truly immutable doubly linked list in C#?

It is more of a theoretical question: Is it possible by any means in C# to create a truly immutable doubly linked list? 这更像是一个理论问题:C#中是否有可能创建一个真正不可变的双向链表? A problem as I see it is in the mutual dependency of 2 adjacent nodes. 我看到的一个问题是2个相邻节点的相互依赖。

By "truly" I mean using readonly fields. “真正”我指的是使用只读字段。

This is possible to do with tricky constructor logic. 这可能与棘手的构造函数逻辑有关。 For example 例如

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" });

You piqued my curiousity. 你激起了我的好奇心。 The class for a ReadOnlyNode is simple enough to define: 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;
   }
}

The problem with readonly in a doubly-linked list is that, for each node, you have to specify that node's previous and next nodes in the constructor, so if they're passed from outside the constructor they must already exist. 双链表中readonly的问题在于,对于每个节点,您必须在构造函数中指定该节点的前一个节点和下一个节点,因此如果它们从构造函数外部传递,则它们必须已经存在。 But, Node M needs a pre-existing Node N as its "next" node when you call the constructor, but that node N needs M as its "previous" node in order to be constructed. 但是,当您调用构造函数时,Node M需要预先存在的Node N作为其“下一个”节点,但是该节点N需要M作为其“前一个”节点才能构造。 This creates a "chicken and egg" situation where both N and M need the other node to be instantiated first. 这会产生“鸡和蛋”情况,其中N和M都需要首先实例化另一个节点。

However, there's more than one way to skin this cat. 然而,这种猫皮肤的方法不止一种。 What if each node of a list was instantiated recursively from within the constructor of one ReadOnlyNode? 如果列表的每个节点都是从一个ReadOnlyNode的构造函数中递归实例化的,该怎么办? Until each constructor was complete, the properties at each level would still be mutable, and the reference to each Node would exist in its constructor, so it wouldn't matter that not everything had been set up until everything is set up. 在每个构造函数完成之前,每个级别的属性仍然是可变的,并且对每个节点的引用将存在于其构造函数中,因此在设置完所有内容之前并不是所有内容都已设置并不重要。 The following code compiles, and given a pre-existing IEnumerable will produce an immutable doubly-linked list: 下面的代码编译,并且给定一个预先存在的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));

You can use this with any collection that implements IEnumerable (pretty much all built-in collections do, and you can use OfType() to turn non-generic ICollections and IEnumerables into generic IEnumerables). 您可以将它与任何实现IEnumerable的集合一起使用(几乎所有内置集合都可以,并且您可以使用OfType()将非泛型ICollections和IEnumerables转换为通用IEnumerables)。 The only thing to worry about is the call stack; 唯一需要担心的是调用堆栈; there is a limit to how many method calls you can nest, which may cause an SOE on a finite but large list of inputs. 您可以嵌套多少个方法调用是有限制的,这可能会导致有限但很大的输入列表上的SOE。

EDIT: JaredPar brings up a very good point; 编辑: JaredPar提出了一个非常好的观点; this solution uses Count() and Any() which have to take the results of Skip() into account, and so cannot use the "shortcuts" built into these methods that can use the cardinality property of a collection class. 此解决方案使用Count()和Any(),它们必须考虑Skip()的结果,因此不能使用这些方法中内置的“快捷方式”,这些方法可以使用集合类的基数属性。 Those calls become linear, which squares the complexity of the algorithm. 这些调用变为线性,这使算法的复杂性变得平方。 If you just use the basic members of IEnumerable instead, this becomes much more performant: 如果你只是使用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)
    {
    }
}

With this solution, you lose a little of the more elegant error-checking, but if the IEnumerable is null an exception would have been thrown anyway. 使用此解决方案,您将失去一些更优雅的错误检查,但如果IEnumerable为null,则无论如何都会抛出异常。

Yes, you can make a "link-setter" object used for setting the links, that you send into the constructor of the node, or have a static create method that returns the "link-setter". 是的,您可以创建一个“link-setter”对象,用于设置链接,发送到节点的构造函数,或者使用静态create方法返回“link-setter”。 The links in the node are private, and can only be accessed through the "link-setter", and when you have used them to set up the list, you throw them away. 节点中的链接是私有的,只能通过“link-setter”访问,当您使用它们设置列表时,您将它们丢弃。

However, that's a pretty useless exercise. 然而,这是一个非常无用的练习。 If the list is immutable, it's pointless to use a doubly linked list when a simple array works better. 如果列表是不可变的,当简单数组更好地工作时,使用双向链表是没有意义的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM