【问题标题】:How to iterate over list while removing items at the same time?如何在删除项目的同时迭代列表?
【发布时间】:2014-09-08 13:49:43
【问题描述】:

我正在尝试找到一种优雅的方式来遍历列表,同时删除项目。
I know this solution。但我的条件更难:

  • 全部单线程在这里
  • 迭代必须向前
  • 每个项目都必须只处理一次
  • 多个和随机项目可以在处理 1 个项目时移除。
  • 项目是复杂而智能的对象。他们执行一个自定义方法,它可以决定删除一些项目(从 0 到全部)。
  • (添加和插入也可以发生,但现在这并不重要,如果有办法同时处理,那就太好了)

问题:这可能吗?如果是,如何?


我有将对象标记已移除/非活动的想法。当我稍后再次迭代时,我将删除它们而不调用它们做事。迭代会经常重复,这就是为什么每个对象在每次迭代中都必须恰好有 1 圈。那行得通吗?


这就是我现在处理事情的方式。它并不完美,但会给你我希望的提示。

伪代码:

class Foo
{
    public void DoStuff()
    {
        // do other stuff

        if (condition)
            Kill(x); // should result in list.RemoveAt(x) somehow
    }
}

class Program
{
    [STAThread]
    static void Main(string[] args)
    {
        List<Foo> list = new List<Foo>();
        for (int i = 0; i < 15; i++)
            list.Add(new Foo());

        for (int i = 0; i < list.Count; i++)
            list[i].DoStuff();

        Console.ReadKey();
    }
}


(这不是 XY 问题。我敢肯定。多年来我一直在想这个问题,我决定最终找到一个可靠的解决方案。我正在使用 C# 来解决这个问题。这不是恶作剧. 如果它看起来像它,我很抱歉。)

感谢您的帮助!

【问题讨论】:

  • 这听起来像是在说疯话,究竟是什么在处理它们时删除它们?在不同的线程上运行的东西?
  • 所以它是删除可能导致在其他位置修改列表的项目的行为吗?还是问题从其他线程访问列表?
  • 这是真的还是只是个谜?其中一些限制很奇怪。
  • 为了能够回答问题,您需要提供更多有关应用程序所需行为的信息。例如,如果您正在迭代列表,并且您在第二个元素上调用某个方法,该方法从列表中删除元素 1 和 3,您是否希望元素 3 仍然被访问,即使它已被删除?是否存在限制您创建列表副本的限制?它们是真正的限制,还是只是一个思想问题?
  • @Alex 想象一下foreach 循环体中的代码从它正在迭代的集合中删除项目。只涉及一个线程,但从迭代器的角度来看,它在尝试迭代集合时从“其他地方”发生了变异。这很容易创建:foreach(var item in list) list.RemoveAt(random.Next(list.Count));

标签: c# list iteration


【解决方案1】:

您可以在此处使用ObservableCollection,以便迭代集合的代码能够检测集合在迭代时何时以及如何发生变异。通过使用ObservableCollection,迭代代码可以在当前索引之前添加项目时增加索引,或者当从当前索引之前删除项目时减少索引。

public static IEnumerable<T> IterateWhileMutating<T>(
    this ObservableCollection<T> list)
{
    int i = 0;
    NotifyCollectionChangedEventHandler handler = (_, args) =>
    {
        switch (args.Action)
        {
            case NotifyCollectionChangedAction.Add:
                if (args.NewStartingIndex <= i)
                    i++;
                break;
            case NotifyCollectionChangedAction.Move:
                if (args.NewStartingIndex <= i)
                    i++;
                if (args.OldStartingIndex <= i) //note *not* else if
                    i--;
                break;
            case NotifyCollectionChangedAction.Remove:
                if (args.OldStartingIndex <= i)
                    i--;
                break;
            case NotifyCollectionChangedAction.Reset:
                i = int.MaxValue;//end the sequence
                break;
            default:
                //do nothing
                break;
        }
    };
    try
    {
        list.CollectionChanged += handler;
        for (i = 0; i < list.Count; i++)
        {
            yield return list[i];
        }
    }
    finally
    {
        list.CollectionChanged -= handler;
    }
}

代码取自this other answer of mine。它包含有关在对其进行变异时迭代序列的后果的附加切线信息,以及有关此代码及其设计决策的含义的一些附加说明。

【讨论】:

  • 令我惊讶的是有多少种方法可以解决一个问题。很棒的代码。
  • 您不应该在迭代后也删除委托(最好在finally 内)吗?否则,我认为这是内存泄漏。
  • (我不在 C# 的那个角落) 如果我不使用它作为扩展并将事件侦听器添加到我的可观察列表中(如 list.CollectionChanged += ...;) ,那么,当一切都正确完成后,它是否具有相同的解决效果?
  • @Eve 如果您愿意,您可以使用所有这些代码,但不要产生项目,只需执行您在foreach 循环体中所做的任何事情,是的。这使得该解决方案可重用,并允许您抽象出具有迭代器的抽象概念,该迭代器可以有效地迭代从在迭代时实际改变集合的代码中变异的集合。即使我只使用过此代码一次,我也会发现将这些关注点分开是对代码的改进。
  • @Eve 将其视为相对加速/减速并没有真正的意义。如果您在循环本身内部执行的操作非常快,那么减慢迭代列表的开销会对循环产生有意义的相对影响,但是如果循环本身做了很多工作,则会增加相当多的开销循环本身变得不那么有意义了。
【解决方案2】:

我有将对象标记为已删除/非活动状态的想法。

是的,我认为这样的做法是合理的。我要做的是首先收集所有要删除的项目,然后一次将它们全部删除。在代码中,它可能看起来像:

var toRemove = new HashSet<Item>();

foreach (var item in list)
{
    toRemove.UnionWith(item.GetItemsToRemove());
}

list.RemoveAll(item => toRemove.Contains(item));

这种方法的好处是它应该很快 (O(n)),因为虽然从 List&lt;T&gt; 中删除单个项目是 O(n),但同时从中删除多个项目是 也 O(n)。

【讨论】:

    猜你喜欢
    • 2022-05-17
    • 2020-05-27
    • 2014-10-26
    • 1970-01-01
    • 1970-01-01
    • 2011-11-26
    • 2016-04-25
    • 1970-01-01
    相关资源
    最近更新 更多