54

我有一个处理来自 WinForms 控件的事件的类。根据用户正在做的事情,我正在推迟该类的一个实例并创建一个新实例来处理相同的事件。我需要先从事件中取消订阅旧实例 - 很简单。如果可能的话,我想以非专有的方式来做这件事,这似乎是 IDisposable 的工作。但是,大多数文档仅在使用非托管资源时才建议使用 IDisposable,这在此处不适用。

如果我实现 IDisposable 并取消订阅 Dispose() 中的事件,我是否会歪曲它的意图?我应该提供一个 Unsubscribe() 函数并调用它吗?


编辑:这是一些显示我在做什么的虚拟代码(使用 IDisposable)。我的实际实现与一些专有数据绑定有关(长话短说)。

class EventListener : IDisposable
{
    private TextBox m_textBox;

    public EventListener(TextBox textBox)
    {
        m_textBox = textBox;
        textBox.TextChanged += new EventHandler(textBox_TextChanged);
    }

    void textBox_TextChanged(object sender, EventArgs e)
    {
        // do something
    }

    public void Dispose()
    {
        m_textBox.TextChanged -= new EventHandler(textBox_TextChanged);
    }
}

class MyClass
{
    EventListener m_eventListener = null;
    TextBox m_textBox = new TextBox();

    void SetEventListener()
    {
        if (m_eventListener != null) m_eventListener.Dispose();
        m_eventListener = new EventListener(m_textBox);
    }
}

在实际代码中,“EventListener”类涉及的比较多,每个实例都具有唯一意义。我在一个集合中使用它们,并在用户单击时创建/销毁它们。


结论

我接受gbjbaanb 的回答,至少现在是这样。我觉得使用熟悉的界面的好处超过了在不涉及非托管代码的情况下使用它的任何可能的缺点(这个对象的用户怎么会知道这一点?)。

如果有人不同意 - 请发布/评论/编辑。如果可以针对 IDisposable 提出更好的论据,那么我将更改已接受的答案。

4

9 回答 9

46

是的,去吧。尽管有些人认为 IDisposable 仅适用于非托管资源,但事实并非如此 - 非托管资源恰好是最大的胜利,也是实施它的最明显理由。我认为它获得了这个想法是因为人们想不出任何其他理由来使用它。它不像终结器,这是一个性能问题,GC 也不容易处理好。

将任何整理代码放入您的 dispose 方法中。与试图记住撤消您的引用相比,它会更清晰,更清洁并且更有可能防止内存泄漏和该死的视线更容易正确使用。

IDisposable 的目的是让您的代码更好地工作,而无需您进行大量手动工作。利用它的力量对你有利,克服一些人为的“设计意图”胡说八道。

我记得当 .NET 刚问世时,要说服微软相信确定性终结的有用性已经够难的了——我们赢得了战斗并说服他们添加它(即使当时它只是一种设计模式),使用它!

于 2009-01-16T22:43:51.247 回答
15

我个人的投票是有一个 Unsubscribe 方法,以便从事件中删除该类。IDisposable 是一种用于确定性释放非托管资源的模式。在这种情况下,您不管理任何非托管资源,因此不应实施 IDisposable。

IDisposable 可用于管理事件订阅,但可能不应该。例如,我将您指向 WPF。这是一个充斥着事件和事件处理程序的库。然而几乎没有 WPF 中的类实现 IDisposable。我认为这表明事件应该以另一种方式管理。

于 2009-01-16T22:34:12.280 回答
8

使用模式取消订阅事件让我感到困扰的一件事IDisposable是完成问题。

Dispose()函数 inIDisposable应该由开发人员调用,但是,如果开发人员未调用它,则可以理解 GC 将调用此函数(IDisposable至少按照标准模式)。但是,在您的情况下,如果您不调用Dispose任何其他人,则该事件仍然存在,并且强引用使 GC 无法调用终结器。

Dispose在我看来,GC 不会自动调用 ()的事实足以在这种情况下不使用 IDisposable 。也许它需要一个新的应用程序特定接口,该接口表明这种类型的对象必须具有调用的Cleanup函数以由 GC 处理。

于 2010-10-26T10:16:43.887 回答
5

我认为一次性适用于 GC 无法自动处理的任何事情,并且事件引用在我的书中很重要。这是我想出的一个助手类。

public class DisposableEvent<T> : IDisposable
    {

        EventHandler<EventArgs<T>> Target { get; set; }
        public T Args { get; set; }
        bool fired = false;

        public DisposableEvent(EventHandler<EventArgs<T>> target)
        {
            Target = target;
            Target += new EventHandler<EventArgs<T>>(subscriber);
        }

        public bool Wait(int howLongSeconds)
        {
            DateTime start = DateTime.Now;
            while (!fired && (DateTime.Now - start).TotalSeconds < howLongSeconds)
            {
                Thread.Sleep(100);
            }
            return fired;
        }

        void subscriber(object sender, EventArgs<T> e)
        {
            Args = e.Value;
            fired = true;
        }

        public void Dispose()
        {
            Target -= subscriber;
            Target = null;
        }

    }

这使您可以编写此代码:

Class1 class1 = new Class1();
            using (var x = new DisposableEvent<object>(class1.Test))
            {
                if (x.Wait(30))
                {
                    var result = x.Args;
                }
            }

一个副作用是,您不能在事件上使用 event 关键字,因为这会阻止将它们作为参数传递给辅助构造函数,但是,这似乎没有任何不良影响。

于 2009-08-05T15:16:04.730 回答
4

另一种选择是使用弱委托WPF 弱事件之类的东西,而不必显式取消订阅。

PS [OT] 我认为只提供强大的代表的决定是 .NET 平台的一个最昂贵的设计错误。

于 2009-01-17T08:49:33.940 回答
4

从我读到的关于一次性用品的所有内容中,我认为它们实际上主要是为了解决一个问题而发明的:及时释放非托管系统资源。但是我发现的所有示例仍然不仅集中在非托管资源的主题上,而且还有另一个共同点: 调用 Dispose 只是为了加快一个进程,否则稍后会自动发生(GC -> 终结器 ->处置)

然而,调用取消订阅事件的 dispose 方法永远不会自动发生,即使您将添加一个会调用您的 dispose 的终结器。(至少只要事件拥有对象存在 - 如果它被调用,您将不会从取消订阅中受益,因为事件拥有对象也会消失)

所以主要区别在于事件以某种方式构建了一个无法收集的对象图,因为事件处理对象突然被您只想引用/使用的服务引用。您突然被迫调用 Dispose - 不可能进行自动处理。因此,Dispose 将获得一个微妙的其他含义,而不是在所有示例中发现的含义,其中 Dispose 调用 - 在肮脏的理论中;) - 不是必需的,因为它会被自动调用(在某些时候)......

反正。由于一次性模式已经非常复杂(处理难以正确处理的终结器和许多准则/合同),更重要的是,在大多数情况下与事件反向引用主题无关,我会说它会是通过不使用该隐喻来表示可以称为“从对象图取消根”/“停止”/“关闭”的东西,更容易在我们的脑海中分离出来。

我们想要实现的是禁用/停止某些行为(通过取消订阅事件)。如果有一个像 IStoppable 这样带有 Stop() 方法的标准接口,那就太好了,根据合同,它只专注于

  • 让对象(+它自己的所有可停止对象)与它不是自己创建的任何对象的事件断开连接
  • 这样它就不会再以隐式事件样式的方式被调用(因此可以被视为已停止)
  • 一旦对该对象的任何传统引用消失,就可以收集

让我们调用唯一执行取消订阅的接口方法“Stop()”。您会知道停止的对象处于可接受的状态,但只是停止了。也许拥有一个简单的属性“已停止”也是一个不错的选择。

如果您只是想暂停将来肯定会再次需要的某个行为,或者存储一个已删除的历史中的模型对象以供以后撤消恢复。

毕竟写作我不得不承认刚刚在这里看到了一个 IDisposable 的例子:http: //msdn.microsoft.com/en-us/library/dd783449%28v=VS.100%29.aspx 但无论如何,直到我了解 IObservable 的每一个细节和最初的动机我会说它不是最好的用例示例

  • 因为它是一个非常复杂的系统,我们这里只有一个小问题
  • 并且可能是整个新系统的动机之一是首先摆脱事件,这将导致与原始问题有关的某种堆栈溢出

但似乎他们走在了正确的轨道上。无论如何:他们应该使用我的界面“Istoppable”;)因为我坚信在

  • 处置:“您应该调用该方法,否则如果GC 发生迟到,可能会泄漏” ....

  • 停止:“你必须调用这个方法来停止某种行为
于 2011-01-05T01:23:54.563 回答
3

IDisposable 坚定地关乎资源,我认为问题的根源在于不会进一步搅浑水。

我也在为您自己的界面上的取消订阅方法投票。

于 2009-01-16T22:39:03.190 回答
3

一种选择可能是根本不取消订阅——只是为了改变订阅的含义。如果事件处理程序可以变得足够聪明,可以根据上下文知道它的含义,那么您首先不需要取消订阅。

在您的特定情况下,这可能是一个好主意,也可能不是一个好主意——我认为我们真的没有足够的信息——但值得考虑。

于 2009-01-16T23:20:32.140 回答
1

不,您并没有阻止 IDisposable 的意图。IDisposable 旨在作为一种通用方式,以确保在您使用完一个对象后,您可以主动清理与该对象相关的所有内容。它不必只是非托管资源,它也可以包括托管资源。事件订阅只是另一个托管资源!

实践中经常出现的类似情况是,您将在您的类型上实现 IDisposable,纯粹是为了确保您可以在另一个托管对象上调用 Dispose()。这也不是变态,它只是整洁的资源管理!

于 2013-08-21T17:19:57.413 回答