事件也是方法。
定义一个事件成员意味着类型具有三种能力:
*类型的静态方法/实例方法可以订阅类型事件
*类型的静态方法/实例方法可以注销类型事件
*事件发生时通知已订阅事件的方法
.NET2.0的事件仍然是基于Win32的,只不过使用了Observer模式来实现,同时建立在Delegate机制之上。
事件的设计步骤如下(基本上是Observer的实现步骤):
10.1 设计一个对外提供事件的类型
1.定义EventArgs或子类,用于存放附加信息:
定义一个类,继承于EventArgs,以EventArgs结束,包含一组私有字段以及相应的只读公共属性。
public
class
NewMailEventArgs : EventArgs
{
private
string
from;
public
string
From
{
get
{
return
from; }
}
}
这里,EventArgs基类在FCL中是这个样子的:
[Serializable]
[ComVisible(
true
)]
public
class
EventArgs
{
//
Summary:
//
表示没有事件数据的事件。
public
static
readonly
EventArgs Empty;
public
EventArgs();
}
大多数事件没有附加数据,那么就不用定义任何私有字段和属性,直接使用EventArgs基类作为参数。
2.定义事件成员:
class
MailManager
{
public event EventHandler<NewMailEventArgs> NewMail;
}
这条语句等价于:
public
delegate
void
EventHandler
<
TVEventArgs
>
(Object sender, TVEventArgs e) where TVEventArgs: NewMailEventArgs;
所以方法原型相应为 void MethodName(
Object sender, NewMailEventArgs e)
这里,第一个参数sender类型是Object,因为要兼容所有类型,所以提供一个最广泛的基类型。
第二个参数名始终是e,而且派生于EventArgs,保持了对Observer模式的一致性,所有人(包括VS2005)都会调用这个e
事件方法要求都为void,即不允许有回调值,从而事件链易于操作。
3.定义引发事件的方法——负责通知订阅事件的对象:
这是一个protected的虚方法,并接受EventArgs或其子类的参数。
这个虚方法可以由派生类重写,以添加新的功能;不重写也可以,因为基本上已经可以使用了
class
MailManager
{
protected virtual void OnNewMail(NewMailEventArgs e)
{
EventHandler<NewMailEventArgs> temp = NewMail;
if (temp != null)
temp(this, e);
}
}
这里,使用临时变量temp,是为了防止可能存在的线程同步问题。
4.定义一个激发事件的方法
将输入转换成EventArgs或其子类的对象,然后激发事件
internal
class
MailManager
{
public void SimulateNewMail(String from, String to, String subject)
{
NewMailEventArgs e = new NewMailEventArgs(from, to, subject);
OnNewMail(e);
}
}
10.3 设计订阅者的类,使用事件
在ctor中订阅事件,绑定FaxMsg回调方法,在Unregister方法中注销事件
提供回调方法FaxMsg,当事件激发时自动调用
internal
sealed
class
Fax
{
public Fax(MailManager mm)
{
mm.NewMail += FaxMsg;
}
private void FaxMsg(Object sender, NewMailEventArgs e)
{
Console.WriteLine("Fax: {0}, {1}, {2}", e.From, e.To, e.Subject);
}
public void Unregister(MailManager mm)
{
mm.NewMail -= FaxMsg;
}
}
注意:使用+=和-=操作符,而不能显示使用add/remove方法
事件注销的意义:只要有一个对象还有一个方法仍然订阅事件,该对象就不会被垃圾收集
IDispose接口的Dispose方法,注销所有事件。
FaxMsg方法的sender参数为MailMessager对象,可以使用sender访问MailMessager的对象成员,
补充:在Main函数中实现:
public
static
void
Main()
{
MailManager mm = new MailManager();
//注册pager和fax
Fax fax = new Fax(mm);
Pager pager = new Pager(mm);
//通知pager和fax
mm.SimulateNewMail("Jeffrey", "Kristin", "I Love You!");
//注销fax,只剩下pager
fax.Unregister(mm);
//只通知pager
mm.SimulateNewMail("Jeffrey", "Mom & Dad", "Happy Birthday.");
}
10.2 事件机制
对于public event EventHandler<NewMailEventArgs> NewMail;
C#编译时,相应为
//
一个初始化为null的私有委托字段:
private
EventHandler
<
NewMailEventArgs
>
NewMail
=
null
;
//
一个订阅事件的公共方法:
[MethodImpl(MethodImplOptions.Synchronized)]
public
void
add_NewMail(EventHandler
<
NewMailEventArgs
>
value)
{
NewMail = (EventHandler<NewMailEventArgs>)Delegate.Combine(NewMail, value);
}
//
一个注销事件的公共方法:
[MethodImpl(MethodImplOptions.Synchronized)]
public
void
remove_NewMail(EventHandler
<
NewMailEventArgs
>
value)
{
NewMail = (EventHandler<NewMailEventArgs>)Delegate.Remove(NewMail, value);
}
注: 在IL中也是3个成员:一个私有字段,两个公有方法
如果将event声明为protected,则两个方法也相应为protected
event也可以是static或virtual,则两个方法也相应为static或virtual
10.4 事件与线程安全
在上面的实例中,
System.Runtime.CompilerServices命名空间下,自定义属性
[MethodImpl(MethodImplOptions.Synchronized)]保证了事件的线程同步。
但是这样的同步会有问题。
对于实例事件,CLR使用自身对象作为线程同步锁;
对于静态事件,CLR使用类型对象作为线程同步锁。
但是线程同步指导方针指出,方法永远不要在对象本身或类型对象上加锁,否则这个锁对外公开,会导致其它线程死锁
没有好的办法保证值类型的实例事件成员是线程安全的,因为C#不会为其add/remove生成
[MethodImpl(MethodImplOptions.Synchronized)];
值类型的静态事件成员肯定是线程安全的。
10.5 显示控制事件的订阅与注销
即显示的实现add和remove访问器方法:
建立一个临时委托变量m_NewMail与相应的属性,代替原先的事件成员NewMail,
新建一个作为线程同步锁的私有实例字段m_eventLock
主要改动如下:
class
MailManager
{
private EventHandler<NewMailEventArgs> m_NewMail;
public event EventHandler<NewMailEventArgs> NewMail
{
add
{
lock (m_eventLock)
{
m_NewMail += value;
}
}
remove
{
lock (m_eventLock)
{
m_NewMail -= value;
}
}
}
}
注意,C#不能分辨add/remove方法是由编译器自动创建的,还是程序员显示实现的,所以仍可以使用+=和-=这两个操作符处理事件。
10.6 多事件模型
System.Windows.Forms.Control类型有70多个事件,不可能用上述方法实现,会造成未使用事件对内存的浪费。
解决办法:使用注册工厂,建立事件池。具体见设计模式。