扩展LinkedList,支持遍历同时删除节点

C#中自带的LinkedList,虽然提供了LinkedList.Remove(LinkedListNode)方法和LinkedListNode的操作来进行细致的增删操作,但大多数情况下都需要大量代码,费时费力

一开始遇到的情形是这样的,在我最近进行的编程练习中(https://github.com/zjtzyrcfd/simple-game-structure,尝试实现一个简单的游戏循环架构),有如下实现

游戏实例GameInstance保存了一个GameObject的容器,用于在循环时遍历更新。

显而易见,这个容器不需要随机存储(因为每个循环都会遍历),但是又应该支持高效增删操作(随时有GameObject会被删除)

最开始的时候,我使用了一个简单的数组式容器List,为了减少元素删除进行移动元素的开销,我额外创建了一个用于删除元素的循环,而独立于主循环。同时给这个循环设置了较大的循环间隔。

除了要在对容器进行操作时上锁,以及必须用朴素的for循环进行元素删除外(List中删除元素后迭代器会失效),代码的其他部分都相当简洁。

数组形式的代码(gameObjects的类型为List ):

void GCLoop(){
    lock (gameObjects){
        for(int i=gameObjects.Count-1;i>=0;i--){
            if (gameObjects[i].Deleted){
                gameObjects.RemoveAt(i);
            }
        }
    }
}  

之后,我试图将容器更换为LinkedListNode。在我想法中,类似的代码应该如下

foreach (var i in gameObjects)
{
    if (i.Deleted)
        ...//把i从gameObjects中移除
}

当然,大多数迭代器都会在容器发生修改之后失效(再进行迭代就会抛出异常),因此更安全(但不简洁)的代码如下

ArrayList<GameObject> temp;
foreach (var i in gameObjects)
{
    if (i.Deleted)
        ...//把i加入到temp中
}
foreach (var i in temp)
{
    ...//把i从gameObjects中移除
}

但是,在C#中,LinkedList想要达到O(1)复杂度的删除,你必须使用提供的LinkedListNode。(Remove(T)方法是O(N)的)

理由很简单,链表节点可以获得前后节点的信息,通过修改前后节点的next、prev,就可以把自身从链表中删除。

不尽人意的是,要用LinkedListNode来遍历链表,并没有foreach这样方便的方法。你必须用LinkedList.First获取第一个LinkedListNode,然后通过循环获取LinkedListNode的Next属性得到下一个结点,进行遍历。

可以预想到,遍历链表并且同时删除其中的某些元素,这个要求是合理的,并且会经常出现。因此我选择为LinkedList增加一个扩展方法,而不是直接在这里写一个for循环,之后要用时又到这儿复制。

为了达到目的(用foreach漂亮的遍历,并且可以删除节点),我们必须返回一个IEnumerable ,用它进行foreach遍历。

我们使用yield return方法来实现这个扩展方法。我们把扩展方法命名为GetNodes(),意义为获得链表结点的集合(而不是结点中的元素)。

public static class LinkedListUtil
{
    public static IEnumerable<LinkedListNode<T>> GetNodes<T>(this LinkedList<T> list){
        for(var i=list.First;i!=null;i=i.next){
            yield return i;
        }
    }
}

关于yield的用法,在此不再叙述

如此一来,便实现了对结点的遍历。

但还存在一个问题:如果在遍历中移去了某个节点(虽然在平常的迭代中,这是不允许的,但作为一个链表,你不觉得它应该允许吗?),比如如下用法

foreach (var i in gameObjects.GetNodes()){
    if (i.Value.Deleted){
        gameObjects.Remove(i);
    }
}

此时,扩展方法中的 i 就会失效(被移出链表了,尽管对象依然存在,但是它的Next和Prev已经被置为null),也无法通过i.Next获取下一个元素了。(而且不会抛出异常!)

解决办法是修改扩展方法为如下(最终代码)

public static class LinkedListUtil
{
    public static IEnumerable<LinkedListNode<T>> GetNodes<T>(this LinkedList<T> list){
        LinkedListNode<T> _n;
        for(var i=list.First;i!=null;i=_n){
            _n=i.Next;
            yield return i;
        }
    }
}

将下一个结点保存起来,再yield return当前节点,避免了在返回当前节点后还需要当前节点的信息。

这样一来,就可以实现边遍历边挑选出元素进行删除了!

我尚未验证多线程复杂环境下是否会出现bug,但至少这在单线程下没有问题

你可能感兴趣的:(扩展LinkedList,支持遍历同时删除节点)