在C#编程中,事件是一种常用的通知机制,允许对象通知其他对象发生了某些事情。然而,有时会遇到订阅事件时一切正常,但在尝试取消订阅时却遇到问题的情况。本文将探讨这一现象的原因,并提供相应的解决方案。
问题的核心在于C#编译器如何处理和转换匿名方法。在C#中,匿名方法可以用于创建事件处理器,但编译器在处理这些匿名方法时会创建一个封闭类来存储状态。这意味着每次调用匿名方法时,都会创建一个新的对象实例,这会导致取消订阅时出现问题。
以下是导致问题的示例代码:
public void Initialize()
{
control.KeyPressed += IfEnabledThenDo(control_KeyPressed);
control.MouseMoved += IfEnabledThenDo(control_MouseMoved);
}
public void Destroy()
{
control.KeyPressed -= IfEnabledThenDo(control_KeyPressed);
control.MouseMoved -= IfEnabledThenDo(control_MouseMoved);
}
public EventHandler IfEnabledThenDo(EventHandler actualAction)
{
return (sender, args) => {
if (args.Control.Enabled) actualAction(sender, args);
};
}
编译器将上述代码转换为:
public EventHandler IfEnabledThenDo(EventHandler actualAction)
{
<>c__DisplayClass1 CS$<>8__locals2 = new <>c__DisplayClass1();
CS$<>8__locals2.actualAction = actualAction;
return new EventHandler<Control.ControlEventArgs>(CS$<>8__locals2.<IfEnabledThenDo>b__0);
}
每次调用IfEnabledThenDo方法时,都会创建一个新的匿名类实例,因此取消订阅时无法正确移除事件处理器。
要解决这个问题,需要缓存IfEnabledThenDo方法返回的委托实例,并在订阅和取消订阅时使用相同的实例。以下是修改后的代码:
EventHandler keyPressed;
EventHandler mouseMoved;
public void Initialize()
{
keyPressed = IfEnabledThenDo(control_KeyPressed);
mouseMoved = IfEnabledThenDo(control_MouseMoved);
control.KeyPressed += keyPressed;
control.MouseMoved += mouseMoved;
}
public void Destroy()
{
control.KeyPressed -= keyPressed;
control.MouseMoved -= mouseMoved;
}
通过这种方式,确保了订阅和取消订阅使用的是相同的委托实例,从而避免了取消订阅时的问题。
深入理解编译器如何处理匿名方法对于解决这类问题非常有帮助。Raymond Chen在他的博客中详细解释了编译器为何要这样处理匿名方法,主要是为了在事件处理器执行时能够“持有”actualAction参数。
通过本文的探讨,了解到了C#中订阅与取消订阅机制的工作原理,以及如何避免在取消订阅时遇到的问题。通过缓存委托实例,可以确保订阅和取消订阅时使用的是相同的对象实例,从而避免潜在的错误。
实际上,对原始示例代码进行一个非常小的语法更改就可以立即解决问题。如果已经跟随本文到这里,应该能够理解为什么。
public void Initialize()
{
actualAction = control_KeyPressed;
control.KeyPressed += IfEnabledThenDo();
}
public void Destroy()
{
control.KeyPressed -= IfEnabledThenDo();
}
EventHandler actualAction;
public EventHandler IfEnabledThenDo()
{
return (sender, args) => {
if (args.Control.Enabled) actualAction(sender, args);
};
}