在遍历事件处理程序集合时,如何安全地从回调中移除处理程序?

Posted

技术标签:

【中文标题】在遍历事件处理程序集合时,如何安全地从回调中移除处理程序?【英文标题】:While iterating over a collection of event handlers, how do you safely remove a handler from *within* a callback? 【发布时间】:2011-07-09 04:42:45 【问题描述】:

我有点困惑。 Java 的文档告诉我们,在使用 Iterator 对象迭代该集合时从集合中删除项目时没有定义的行为,唯一安全的方法是使用 Iterator.remove()。

那么,如果在遍历列表的过程中,其中一个处理程序决定是时候将自己作为侦听器删除,那么您将如何安全地从 ArrayList 中删除事件处理程序?

// in public class Dispatcher

public void dispatchEvent()
    Iterator<IEventHandler> iterator = mHandlers.iterator();
    IEventHandler handler = null;
    while(iterator.hasNext())
        handler = iterator.next();
        handler.onCallbackEvent();
    


public void insertHandler(IEventHandler h)
    mHandlers.add(h);


public void removeHandler(IEventHandler h)
    mHandlers.remove(h);

同时,处理程序是这样实例化的......

final Dispatcher d = new Dispatcher();
d.insertHandler(new IEventHandler()
    @Override
    public void onCallbackEvent()
        Log.i(" callback happened ");
        d.removeHandler(this);
    
);

看到潜在的问题了吗?由于在该特定处理程序中声明的 onCallbackEvent(),您正在从 ArrayList 中删除处理程序,而您仍在使用迭代器进行迭代

这是一个棘手的问题吗?处理这种情况的安全方法是什么?

【问题讨论】:

【参考方案1】:

这是实现事件系统时非常常见的问题。唯一的解决方案是在更改时复制处理程序列表。您可以在 insertHandler/removeHandler 方法中自己执行此操作,也可以仅使用 CopyOnWriteArrayList。

【讨论】:

感谢您的建议。这确实很不幸。如果 Iterator 不能提供一种在迭代时修改底层集合的安全方法,我不得不想知道它的意义何在。 **注意:是的,我知道迭代器设计模式。不过,这种安全性似乎是迭代器应该为你买的东西,如果你不小心使用它的话。 为了给您正在寻找的迭代安全性,迭代器必须复制或执行同样计算密集型的操作。你能自己想出另一种方法来实现一个安全的迭代器吗?您必须在更改时复制或在迭代时复制。在大多数情况下,更改时复制更合适,因为更改的频率低于迭代。默认集合不提供迭代安全性,因为并非所有情况都需要它。这就是为什么有 ArrayList 和 CopyOnWriteArrayList。 "copy the handlers on change":你的意思是复制removeHandler中的数组,然后在dispatchEvent结束时用修改后的副本替换原来的列表? (使用 CopyOnWriteArrayList 似乎要简单得多。) “在变化时复制处理程序列表”是指在调用 addListener/removeListener 方法期间复制列表,而不是在事件调度期间。【参考方案2】:

您可以重新实现 removeHandler 来存储计划删除的处理程序。

public void removeHandler(IEventHandler h)
    mHandlersToRemove.add(h);

然后在您进行任何调度之前将其删除。

public void dispatchEvent()
    mHandlers.removeAll(mHandlersToRemove);
    mHandlersToRemove.clear();
    ...

您也可以在 dispatchEvent 的末尾删除,但您只能从 within 处理程序中删除。 (否则,您可能会分派给已删除的处理程序。)


如果您对此问题的理论解决方案感兴趣,可以查看 C++ 如何实现迭代器。在 stl 向量中,迭代器有一个 erase method,它返回下一个有效的迭代器。

看起来像这样:

for (itr = listA.begin(); itr != listA.end(); )

    if ( shouldRemove(*itr) ) 
        itr = listA.erase(itr);
    
    else 
      ++itr;
    

当然,此示例不适用于您的问题,因为它都在 C++ 中,并且将新迭代器传播到顶层循环(或在您的调用中添加返回值以“删除“ 健康)状况)。但也许那里有类似的java实现:)

【讨论】:

以上是关于在遍历事件处理程序集合时,如何安全地从回调中移除处理程序?的主要内容,如果未能解决你的问题,请参考以下文章

html元素移除回调

从 DOM 中移除的元素中取消绑定事件

如何在拖动 React 组件的上下文中移除事件监听器

JDA:如何检测成员是如何从公会中移除的?

怎样在.net项目中移除一个重复的引用?

从集合向量中移除集合