我应该取消订阅事件吗?

57

我有3个关于事件的问题:

  1. 我是否应该总是取消已经订阅的事件?
  2. 如果我不这样做会发生什么?
  3. 在下面的示例中,您将如何取消已订阅的事件?

例如,我有以下代码:

Ctor:目的:用于数据库属性更新

this.PropertyChanged += (o, e) =>
{
    switch (e.PropertyName)
    {
        case "FirstName": break;
        case "LastName": break;
    }
};

还有这个:目的:为了将模型包装成视图模型以进行GUI绑定

ObservableCollection<Period> periods = _lpRepo.GetDailyLessonPlanner(data.DailyDate);
PeriodListViewModel = new ObservableCollection<PeriodViewModel>();

foreach (Period period in periods)
{
    PeriodViewModel periodViewModel = new PeriodViewModel(period,_lpRepo);
    foreach (DocumentListViewModel documentListViewModel in periodViewModel.DocumentViewModelList)
    {
        documentListViewModel.DeleteDocumentDelegate += new Action<List<Document>>(OnDeleteDocument);
        documentListViewModel.AddDocumentDelegate += new Action(OnAddDocument);
        documentListViewModel.OpenDocumentDelegate += new Action<int, string>(OnOpenDocument);
    }
    PeriodListViewModel.Add(periodViewModel);
}

https://dev59.com/vXNA5IYBdhLWcg3wL6oc - SwDevMan81
5个回答

73

首先,让我们回答最后一个问题。 如果您直接使用lambda表达式订阅事件,则无法可靠地取消订阅该事件。 您需要保留一个变量与委托一起使用(以便仍然可以使用lambda表达式),或者使用方法组转换。

现在,是否实际需要取消订阅取决于事件生产者和事件消费者之间的关系。 如果事件生产者应该比事件消费者存活更长时间,则应该取消订阅-否则,生产者将引用消费者,使其保持比应有的更长时间。只要生产者继续产生它,事件处理程序也将不断被调用。

现在,在许多情况下,这不是问题-例如,在窗体中,引发“Click”事件的按钮可能会像创建它的窗体一样存在约同的时间,订阅者通常在其中...因此不需要取消订阅。 这在GUI中非常典型。

同样,如果您仅出于单个异步请求的目的创建了一个WebClient,订阅相关事件并启动异步请求,那么当请求完成时,WebClient本身将有资格进行垃圾收集(假设您没有在其他地方保留引用)。

基本上,您应该始终考虑生产者和消费者之间的关系。 如果生产者将比您希望的消费者存活更长时间,或者它将在您对其不再感兴趣之后继续引发事件,则应该取消订阅。


事件生产者 = documentListViewModel?事件消费者 = 来自上面的OnXXX方法?你的意思是,如果我要删除documentListViewModel,仍然存在对OnXXX方法的引用?在ViewModel中应该从哪里取消订阅事件? - Elisabeth
请问您能否澄清一下您所提到的“事件生产者”、“事件消费者”这些术语的含义是什么? - bazsisz
2
@bazsisz:事件生产者是引发事件的东西,例如一个按钮说“我被点击了”。事件消费者是监听(或订阅)事件并希望对事件做出反应的东西。 - Jon Skeet

38

1)这要视情况而定。通常来说,这是一个好主意,但有一些典型情况下你不需要这么做。基本上,如果你确定订阅对象会比事件源存在更长的时间,那么你应该取消订阅,否则这将创建一个不必要的引用。

但是,如果你的对象正在订阅自己的事件,就像以下示例中的情况:

<Window Loaded="self_Loaded" ...>...</Window>

--那么你就不需要这么做。

2) 订阅事件会对订阅对象产生额外的引用。因此,如果您不取消订阅,则该引用可能会使您的对象保持活动状态,从而导致内存泄漏。通过取消订阅,您可以消除该引用。请注意,在自我订阅的情况下,这个问题不会出现。

3) 您可以这样做:

this.PropertyChanged += PropertyChangedHandler;
...
this.PropertyChanged -= PropertyChangedHandler;

在哪里

void PropertyChangedHandler(object o, PropertyChangedEventArgs e)
{
    switch (e.PropertyName)
    {
        case "FirstName": break;
        case "LastName": break;
    }
}

1
当我使用你的底部代码时,我无法编译?我在最后一个“}”后面加了一个“;”,但它仍然无法编译?另外,根据你提供的代码,我应该在哪里取消订阅? - Elisabeth
@Lisa:(1)你收到了什么错误信息?(2)当你不再需要事件时,应该取消订阅。通常这是在对象生命周期的末尾。如果你能告诉更多关于订阅事件的对象,那么就更容易理解何时取消订阅。 - Vlad
请检查我上面的代码,我订阅了3个事件OnXXX方法。PeriodViewModel不能删除DocumentListViewModel。 - Elisabeth
@Lisa:在这种情况下,你的订阅者(运行代码的对象)似乎比单个DocumentListViewModel存在时间更长,对吗?如果是这样的话,你就不需要取消订阅,因为DocumentListViewModel将会死亡,从而在你需要释放订阅者之前释放对其的引用。 - Vlad
5
你的第二句话似乎有些颠倒了。因为源对象保留了对订阅者的引用,所以如果订阅对象的寿命超过源对象并不是问题。源对象仍然可以被垃圾回收。 - BJ Myers

8

当订阅实例与被订阅实例具有相同的作用域时,您不必取消订阅事件。

假设您是一个表单,并且正在订阅控件,则这两个一起形成一个组。但是,如果您有一个管理表单的中央类,并且已经订阅了该表单的“Closed”事件,则它们不会一起形成一个组,您必须在表单关闭后取消订阅。

订阅事件会使订阅实例创建对被订阅实例的引用。这会防止垃圾回收。因此,当您有一个管理表单实例的中央类时,它将保留所有表单的内存。

WPF是一个例外,因为它具有弱事件模型,其中使用弱引用订阅事件,它不会保留表单的内存。但是,最好的做法仍然是在您不属于表单时取消订阅。


5
您可以查看MSDN上的这篇文章。引用如下:

如果要在事件被触发时防止调用事件处理程序,只需取消订阅该事件。为了避免资源泄漏,在处理订阅对象之前取消订阅事件非常重要。在取消订阅事件之前,发布对象中支持事件的多路广播委托将引用封装订阅者的事件处理程序的委托。只要发布对象持有该引用,您的订阅对象将无法进行垃圾回收。


1

1.) 我是否应该总是取消订阅已经订阅的事件?
通常情况下是的。唯一的例外是当你订阅的对象不再被引用并且很快就会被垃圾回收时。

2.) 如果我不这样做会发生什么?
你订阅的对象将持有对委托的引用,而委托又持有对其this指针的引用,因此你将得到一个内存泄漏。
或者如果处理程序是一个lambda表达式,它将保留绑定的任何本地变量,这些变量也不会被回收。


1
我想指出的是,如果您不取消订阅事件,那么在您完成使用对象之后,仍可能会调用处理程序;如果该对象是可处理的,则在对象被处理后。如果处理程序使用了已被处理释放的非托管资源,则可能会对处理程序代码产生影响。 - Steve Ellinger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接