使用事件的常用步骤:
- 定义事件参数;
- 事件源类型中声明事件;
- 注册处理事件的方法,即监听;
- 再需要是,触发事件。
public class CustomEventArgs: System.EventArgs
{
public string EventData { get; private set; }
public CustomEventArgs(string eventData)
{
EventData = eventData;
}
}
public class CustomEventSource
{
public System.EventHandler<CustomEventArgs> CustomEvent;
public void DoWork()
{
// do other work
// 最初的写法
if(null != CustomEvent)
{
CustomEvent(this, new CustomEventArgs("event argument"));
}
// 有经验的同事会告诉我们,需要这样写
//var handler = CustomEvent;
//if(null != handler)
//{
// handler(this, new CustomEventArgs("event argument"));
//}
}
}
public class EventUsage
{
private CustomEventSource handlerDemo;
public void Init()
{
handlerDemo = new CustomEventSource();
handlerDemo.CustomEvent += HandleCustomEvent;
}
private void HandleCustomEvent(object sender, CustomEventArgs e)
{
Console.WriteLine($"event: {e.EventData}");
}
public void UnInit()
{
if(null != handlerDemo)
{
handlerDemo.CustomEvent -= HandleCustomEvent;
}
}
}
其实那会不是很理解为什么这么写,然后他们就告诉我,这样可以避免多线程使用时带来的bug,而且不好查。就是当前程序执行完if语句之后,会被另一个线程打断,并且另一个线程解除事件监听,也就是解除了事件订阅,这样事件处理程序成了null,这样就引发了空引用的异常。所以会先赋值一个,用赋值后的内容去处理事件。原理是:该赋值会对赋值符号右边的内容做浅拷贝(创建新引用,并令其指向原来的事件处理程序),当另一个线程注销事件处理程序的时候,只会修改类实例中的handlerDemo字段,而不会把该处理程序同时从局部变量handler里面移走,这样,handler中还保存着早前执行浅拷贝时所记录的事件订阅者,这样就不会出错了。
这样的处理是线程安全的,但是会复杂一些,C#引入的null条件运算符,可以解决这个问题,即:
CustomEvent?.Invoke(this, new CustomEventArgs("event argument"));
现在编译器也会智能提示我们这样修改,这样很方便,而且在语义上,与早期的if结构类似,但区别在于?.运算符左侧内容只会计算一次。现在我已经慢慢习惯这种使用方式了。