短暂的事件处理程序有什么问题吗?

5
例如,下面的内容有什么问题吗?
private void button1_Click(object sender, EventArgs e)
{
    Packet packet = new Packet();

    packet.DataNotSent += packet_DataNotSent;

    packet.Send();
}

private void packet_DataNotSent(object sender, EventArgs e)
{
    Console.WriteLine("Packet wasn't sent.");
}

class Packet
{
    public event EventHandler DataNotSent;

    public void Send()
    {
        if (DataNotSent != null)
        {
            DataNotSent(null, EventArgs.Empty);
        }
    }
}

如果只是一个简单的整数变量声明,那么当它超出范围并且稍后由垃圾收集器收集时,就没问题了,但事件有点...更长寿?我需要手动取消订阅或采取任何措施来确保这正常工作吗?
这感觉有点奇怪。不确定它是否正确。通常,当我添加到事件处理程序时,它会持续整个应用程序,所以我不确定在不断添加和删除事件处理程序时会发生什么。

你在哪里将事件处理程序从事件中分离?为什么每次单击按钮时都要附加相同的事件处理程序? - Ashley John
你知道可以将事件定义为一个动作,而不是一个事件处理程序吗? - Ritch Melton
http://stackoverflow.com/questions/298261/do-event-handlers-stop-garbage-collection-from-occuring http://stackoverflow.com/questions/506092-is-it-necessary-to-explicitly-remove-event-handlers-in-c - CharithJ
如果这是Packet类的用法,我会倾向于更改Send()方法以返回发送操作的结果并且摆脱事件处理程序。然而,我想你可能希望将Send()操作变成异步的,在这种情况下,您需要一个委托来执行回调。 - Igor Pashchuk
6个回答

6

从技术上讲,这段代码没有问题。由于在方法完成后不会有对packet的引用,因此该订阅也将被(最终)收集。

从风格上讲,我认为这样做非常不寻常。更好的解决方案可能是从Send()返回某种结果,并根据该结果进行下一步操作。顺序代码更容易理解。


你能详细说明一下你回答的第二部分吗?我总是乐于接受其他方案,而且我同意你对这种风格有点不太牢固的看法。你会像返回一个布尔值(或枚举等)来指示发送是否成功并在那里处理吗?返回值的问题在于我希望它是异步的,而且我觉得自己被限制在使用事件中了。 - Ryan Peschel
@RyanPeschel,如果你想异步执行任务(且不使用C# 5),那么使用事件或回调委托可能是最好的选择。如果需要指示失败,则应返回bool。如果需要更多数据,则可以返回自定义类型。 - svick
1
看一下新的任务模式。我认为这是处理同步和异步场景的一种清晰的方式。(http://www.codethinked.com/net-40-and-systemthreadingtasks) - Polity
@Polity:非常感谢您的精彩文章!它非常易读且写得很好。虽然我已经在使用Tasks,但我仍然发现它很有启发性。从任务中返回值的问题在于它会阻塞直到值返回(还不如使用同步代码!嗯,几乎一样)。 - Ryan Peschel
@RyanPeschel,关于任务,还有另一种选择 - 使用ContinueWith() - svick
1
@Ryan Peschel:那不是真的。任务仅意味着“一些工作”。工作如何或何时执行超出了任务的范围。执行任务可能是异步或同步的,这取决于任务内部的工作和您处理任务的方式。如果想要深入了解任务,请阅读以下文章:http://www.codeproject.com/KB/cs/TPL1.aspx - Polity

2
这就是事件驱动编程的美妙之处 -- 你不需要关心谁/什么/是否有人在监听,你只需要说出某个事件发生了,让已经订阅的人做出相应反应。
你不必手动取消订阅,但当对象超出范围时,你应该取消订阅。链接将保持订阅者的活力(来自 @pst)。

1
这有点误导——“link”会让订阅者保持连接。 - user166390

1

当发布事件的对象变得符合垃圾回收条件时,所有订阅该事件的对象也会变得符合垃圾回收条件(前提是没有其他引用指向它们)。

因此,在您的示例中:

  • packet超出范围时,它就变得符合GC条件了。
  • 因此,包含packet_DataNotSent的对象也变得符合GC条件了(除非被其他东西引用)。
  • 如果包含packet_DataNotSent的对象被其他东西引用,它当然不会被GC,但当packet被GC时,它仍会自动“取消订阅”。

1

我知道如果事件源即Packet类和事件接收器即事件处理程序的生命周期不匹配,就会出现内存泄漏问题。

在这种情况下,您创建的委托仅限于此函数的范围,因为事件接收器仅限于此函数的范围。

您可以在发送方法执行后调用packet.DataNotSent -= packet_DataNotSent;以确保委托被垃圾回收。请阅读此文


1

我猜你为了简洁而精简了示例,但在你的片段中,没有必要使用事件。 Packet.Send() 只需返回一个检查过的结果即可。只有在存在一些异步性(例如异步操作、调度未来执行等)时,才真正需要更复杂的方法,而且 Packet.Send() 没有立即返回。

就对象生命周期管理而言,你的事件订阅并不会造成问题,因为事件订阅将使处理程序保持活动状态,但反之则不然。也就是说,在仍有对它订阅的数据包的活动引用时,button1_Click 所属的类不会被垃圾回收。由于数据包的寿命很短,这不会是一个问题。

如果在实际应用中 Packet.Send() 无法返回结果,那么我会倾向于将委托传递给数据包,而不是订阅它的事件,假设只有一个对象需要收到失败通知。


0

事件应该用于一些场景,当我们不知道谁可能对某些事情发生或某些操作变得必要感兴趣时。在你的例子中,如果有人有兴趣知道数据包被发现无法投递的时间,调用构造函数的代码很可能知道那个使用者是谁。因此,让数据包的构造函数接受一个 Action<Packet, PacketResult> 委托会比发布一个事件更好。


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