C#事件与委托
事件
定义了事件成员的类型能提供以下功能:
- 方法能登记它对事件的关注
- 方法能注销它对事件的关注
- 事件发生时,登记了的方法将收到通知
CLR事件模型以委托为基础。委托是调用(Invoke)回调方法的一种类型安全的方式。
编译器如何实现事件
如:
public event EventHandler<NewMailEventArgs> NewMail;
C#编译器编译时把它转换为以下3个构造:
// 1. 一个被初始化为null的私有委托字段
private EventHandler<NewMailEventArgs> NewMail = null;
// 2. 一个公共add_Xxx方法(其中Xxx是事件名)
// 允许方法登记对事件的关注
public void add_NewMail(EventHandler<NewMailEventArgs> value)
{
// 通过循环和对CompareExchange的调用,可以以一种线程安全的方式向事件添加委托
EventHandler<NewMailEventArgs> prevHandler;
EventHandler<NewMailEventArgs> newMail = this.NewMail;
do
{
prevHandler = newMail;
EventHandler<NewMailEventArgs> newHandler = (EventHandler<NewMailEventArgs>) Delegate.Combine(prevHandler, value);
newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, newHandler, prevHandler);
} while (newMail != prevHandler);
}
// 3.一个公共remove_Xxx方法(其中Xxx是事件名)
// 允许方法注销对事件的关注
public void remove_NewMail(EventHandler<NewMailEventArgs> value)
{
// 通过循环和对CompareExchange的调用,可以以一种线程安全的方式向事件移除委托
EventHandler<NewMailEventArgs> prevHandler;
EventHandler<NewMailEventArgs> newMail = this.NewMail;
do
{
prevHandler = newMail;
EventHandler<NewMailEventArgs> newHandler = (EventHandler<NewMailEventArgs>) Delegate.Remove(prevHandler, value);
newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, newHandler, prevHandler);
} while (newMail != prevHandler);
}
- 第一个构造是具有恰当委托类型的字段。该字段是对一个委托列表的头部的引用。
- 第二个构造是一个方法,允许其他对象登记对事件的关注。生成的代码总是调用
System.Delegate
的静态Combine
方法,它将委托实例添加到委托列表中,返回新的列表头(地址),并将这个地址存回字段。 - 第三个构造也是方法,允许对象注销对事件的关注。方法中的代码总是调用
System.Delegate
的静态Remove
方法,将委托实例从委托列表中删除,返回新的列表头(地址),并将这个地址存回字段。
PS:
试图删除从未添加过的方法,
Delegate
的Remove
方法在内部不做任何事情。也就是说,不会抛出任何异常,也不会显示任何警告;事件的方法集合保持不变。在类型中定义事件时,事件的可访问性决定了什么代码能登记和注销对事件的关注。如上例中,因为源代码将事件声明为
public
,add
和remove
方法的可访问性都是public
。
除了生成上述3个构造,编译器还会在托管程序集的元数据中生成一个事件定义记录项。这个记录项包含了一些标志(flag)和基础委托类型(underlying delegate type),还引用了 add
和 remove
访问器方法。这些信息的作用就是建立”事件“的抽象概念和它的访问器方法之间的联系。
// 使用C#的+=操作符登记它对NewMail事件的关注
mm.NewMail += FaxMsg;
// C#编译器内建了对事件的支持,会将+=操作符翻译成以下代码来添加对象对事件的关注
mm.add_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg));
如果对象不再希望接收事件通知时,应注销对事件的关注,如果类型要实现 IDisposable
的 Dispose
方法,就应该在实现中注销对所有事件的关注。
// 使用C#的-=操作符注销它对NewMail事件的关注
mm.NewMail -= FaxMsg;
// C#编译器内建了对事件的支持,会将-=操作符翻译成以下代码来移除对象对事件的关注
mm.remove_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg));
委托
定义一些代码,随后用到:
internal delegate void Feedback(Int32 value);
private static void Counter(Int32 from, Int32 to, Feedback fb)
{
for (Int32 val = from; val <= to; val++)
{
// 如果指定了任何回调,就调用它们
if (fb != null)
fb(val);
}
}
private static void FeedbackToConsole(Int32 value)
{
Console.WriteLine("Item=" + value);
}
private static void FeedbackToMsgBox(Int32 value)
{
MessageBox.Show("Item=" + value);
}
private void FeedbackToFile(Int32 value)
{
using (StreamWriter sw = new StreamWriter("Status", true))
{
sw.WriteLine("Item=" + value);
}
}
用委托回调静态方法
private static void StaticDelegateDemo()
{
Console.WriteLine("----- Static Delegate Demo -----");
Counter(1, 3, null);
Counter(1, 3, new Feedback(FeedbackToConsole)); // 委托对象是方法的包装器,使方法能通过包装器来间接回调
Counter(1, 3, new Feedback(FeedbackToMsgBox));
Console.WriteLine();
}
将方法绑定到委托时,C#和CLR都允许引用类型的协变性(covariance)和逆变性(contravariance)。协变性是指方法能返回从委托的返回类型派生的一个类型,逆变性是指方法获取的参数可以是委托的参数类型的基类。注意,只有引用类型才支持协变性和逆变性,值类型或void不支持。
用委托回调实例方法
private static void InstanceDelegateDemo()
{
Console.WriteLine("----- Instance Delegate Demo -----");
Program p = new Program();
Counter(1, 3, new Feedback(p.FeedbackToFile)); // 导致委托包装对FeedbackToFile方法的引用,这是一个实例方法
Console.WriteLine();
}
委托揭秘
重新审视这一行代码:
internal delegate void Feedback(Int32 value);
编译器实际会像下面这样定义一个完整的类:
internal class Feedback : System.MulticaseDelegate
{
// 构造器
public Feedback(Object @object, IntPrt method);
// 这个方法的原型和源代码指定的一样
public virtual void Invoke(Int32 value);
// 以下方法实现对回调方法的异步回调
public virtual IAsyncResult BeginInvoke(Int32 value, AsyncCallback callback, Object @object);
public virtual void EndInvoke(IAsyncResult result);
}
注:
System.MulticaseDelegate
派生自System.Delegate
,后者又派生自System.Object
。
由于所有委托类型都派生自MulticaseDelegate
,所以它们继承了MulticaseDelegate
的字段、属性和方法。在所有这些成员中,有三个非公共字段是最重要的。
字段 | 类型 | 说明 |
---|---|---|
_target |
System.Object |
当委托对象包装一个静态方法时,这个字段为null。当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象。换言之,这个字段指出要传给实例方法的隐藏参数this的值。 |
_methodPtr |
System.IntPtr |
一个内部的整数值,CLR用它标识要回调的方法。 |
_invocationList |
System.Object |
该字段通常为null。构造委托链时它引用一个委托数组。 |
所有委托都有一个构造器,它获取两个参数:一个是对象引用,另一个是引用了回调方法的整数。
对象引用被传给构造器的object参数,标识了方法的一个特殊 IntPtr
值(从MethodDef
或MemberRef
元数据token获得)被传给构造器的method参数。对于静态方法,会为object参数传递null值。在构造器内部,这两个实参分别保存在_target
和_methodPtr
私有字段中。除此之外,构造器还将_invocationList
字段设为null。
例如,在执行了一下两行代码后:
Feedback fbStatic = new Feedback(Program.FeedbackToConsole);
Feedback fbInstance = new Feedback(new Program().FeedbackToFile);
fbStatic
和fbInstance
变量将引用两个独立的、初始化好的Feedback
委托对象,如图所示:
用委托回调多个方法(委托链)
private static void ChainDelegateDemo1(Program p)
{
Console.WriteLine("----- Chain Delegate Demo 1 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain = (Feedback) Delegate.Combine(fbChain, fb1);
fbChain = (Feedback) Delegate.Combine(fbChain, fb2);
fbChain = (Feedback) Delegate.Combine(fbChain, fb3);
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain = (Feedback) Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Counter(1, 2, fbChain);
}
在上述代码中,构造了三个委托对象并让变量 fb1, fb2 和 fb3 分别引用每个对象,如图所示:
在第一次 Delegate.Combine
时,发现试图合并的是 null 和 fb1,在内部,Combine 直接返回 fb1 中的值,所以 fbChain 变量现在引用 fb1 变量所引用的委托对象,如图所示:
添加第二个委托时,在内部 Combine 方法发现 fbChain 已引用了一个委托对象,所以 Combine 会构造一个新的委托对象。新委托对象对它的私有字段 _target
和 _methodPtr
进行初始化,同时 _invocationList
字段被初始化为引用一个委托对象数组。数组的第一个元素被初始化为引用包装了 FeedbackToConsole
方法的委托,第二个元素被初始化为引用包装了 FeedbackToMsgBox
方法的委托。最后 fbChain 被设为引用新建的委托对象,如图所示:
在添加第三个委托时,过程与添加第二个委托时类似,需要注意的是,之前新建的委托及其_invocationList
字段引用的数组现在可以进行垃圾回收了。最终状态如图所示:
在调用 Counter
方法时,会在 fbChain
引用的委托上调用 Invoke
方法,该委托发现私有字段_invocationList
不为 null ,所以会执行一个循环来遍历数组中的所有元素,并依次调用每个委托包装的方法。Feedback
的 Invoke
方法的伪代码大致如下:
// 无返回值
public void Invoke(Int32 value)
{
Delegate[] delegateSet = _invocationList as Delegate[];
if (delegateSet != null)
{
// 这个委托数组指定了应该调用的委托
foreach (Feedback d in delegateSet)
d(value); // 调用每个委托
}
else
{
// 否则不是委托链
// 在指定的目标对象上调用这个回调方法
_methodPtr.Invoke(_target, value);
}
}
// 有返回值
public Int32 Invoke(Int32 value)
{
Int32 result;
Delegate[] delegateSet = _invocationList as Delegate[];
if (delegateSet != null)
{
// 这个委托数组指定了应该调用的委托
foreach (Feedback d in delegateSet)
result = d(value); // 调用每个委托
// 循环完成后,result变量只包含调用的最后一个委托的结果
}
else
{
// 否则不是委托链
// 在指定的目标对象上调用这个回调方法
result = _methodPtr.Invoke(_target, value);
}
return result;
}
还可调用 Delegate 的公共静态方法 Remove 从链中删除委托。Remove方法被调用时,它扫描第一个实参所引用的那个委托对象内部维护的委托数组(从末尾向索引0扫描)。Remove查找的是其 _target
和 _methodPtr
字段与第二个实参中的字段匹配的委托。
- 如果找到匹配的委托,并且在删除之后数组中只剩余一个数据项,就返回那个数据项;
- 如果找到匹配的委托,并在数组中还剩余多个数据项,就新建一个委托对象——其中创建并初始化的
_invocationList
数组将引用原始数组中的所有数据项——并返回对这个新建委托对象的引用; - 如果从链中删除了仅有的一个元素,Remove会返回null;
注意:每次Remove方法调用只能从链中删除一个委托,它不会删除有匹配的
_target
和_methodPtr
字段的所有委托。
C#编译器自动为委托类型的实例重载了+=和-=操作符。这些操作符分别调用 Delegate.Combine
和 Delegate.Remove
。可用这些操作符简化委托链的构造,如下示例代码 ChainDelegateDemo1
和 ChainDelegateDemo2
方法生成的 IL 代码完全一样。
private static void ChainDelegateDemo2(Program p)
{
Console.WriteLine("----- Chain Delegate Demo 2 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2)
fbChain += fb3;
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain -= new Feedback(FeedbackToMsgBox);
Counter(1, 2, fbChain);
}
委托类型的 Invoke
方法包含了对数组中的所有项进行遍历的代码,足以应付很多情形,但也有它的局限性:
- 除了最后一个返回值,其他所有回调方法的返回值都会被丢弃;
- 如果被调用的委托中有一个抛出了异常或阻塞了相当长一段时间,链中后续的所有对象都调用不了;
所以 MulticaseDelegate
类提供了一个实例方法 GetInvocationList
,用于显示调用链中的每一个委托,并允许使用需要的任何算法:
public abstract class MulticaseDelegate : Delegate
{
// 创建一个委托数组,其中每个元素都引用链中的一个委托
public sealed override Delegate[] GetInvocationList();
}
在内部,GetInvocationList
构造并初始化一个数组,让它的每个元素都引用链中的一个委托,然后返回对该数组的引用。
委托定义不要太多(泛型委托)
.Net Framework 现在支持泛型,所以实际只需几个泛型委托(在System命名空间中定义)就能表示需要获取多达16个参数的方法:
public delegate void Action();
public delegate void Action<T>(T arg);
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
...
public delegate void Action<T1, ..., T16>(T1 arg1, ..., T16 arg16);
此外,还提供了17个 Func
函数,允许回调方法返回值:
public delegate TResult Func<TResult>();
public delegate TResult Func<T, TResult>(T arg);
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
...
public delegate TResult Func<T1, ..., T16, TResult>(T1 arg1, ..., T16 arg16);
然而,如需使用 ref
或 out
关键字以传引用的方式传递参数,就不得不定义自己的委托。
C#为委托提供的简化语法
button.Click += new EventHandler(button_Click);
上一行代码的思路是向按钮控件等级 button_Click
方法的地址,以便在按钮被单击时调用方法。构造 EventHandler
委托对象是CLR要求的,因为这个对象提供了一个包装器,可确保(被包装的)方法只能以类型安全的方式调用,这个包装器还支持调用实例方法和委托链。
简化语法1:不需要构造委托对象
C#允许指定回调方法的名称,不必构造委托对象包装器,如:
public static void CallbackWithoutNewingADelegateObject()
{
ThreadPool.QueueUserWorkItem(SomeAsyncTask, 5);
}
private static void SomeAsyncTask(Object o)
{
Console.WriteLine(o);
}
当然,当代码编译时,C#编译器还是会生成IL代码来新建委托对象。
简化语法2:不需要定义回调方法(lambda表达式)
前面的代码还可以这样重写:
public static void CallbackWithoutNewingADelegateObject()
{
ThreadPool.QueueUserWorkItem(obj => Console.WriteLine(obj), 5);
}
C#编译器会将这些代码改写为下面这样:
// 创建该私有字段是为了缓存委托对象
// 优点:CallbackWithoutNewingADelegateObject 不会每次调用都新建一个对象
// 缺点:缓存的对象永远不会被垃圾回收
[CompilerGenerated]
private static WaitCallback <>9__CacheAnonymousMethodDelegate1;
public static void CallbackWithoutNewingADelegateObject()
{
if (<>9__CacheAnonymousMethodDelegate1 == null)
{
// 第一次调用时,创建委托对象,并缓存它
<>9__CacheAnonymousMethodDelegate1 = new WaitCallback(<CallbackWithoutNewingADelegateObject>b__0);
}
ThreadPool.QueueUserWorkItem(<>9__CacheAnonymousMethodDelegate1, 5);
}
[CompilerGenerated]
private static void <CallbackWithoutNewingADelegateObject>b__0(Object obj)
{
Console.WriteLine(obj);
}
lambda表达式的优势在于,它从源代码中移除了一个“间接层”,或者说避免了迂回。如果只需在代码中引用这个主体一次,那么lambda表达式允许直接内联那些代码,不必为它分配名称,从而提高了变成效率。
简化语法3:局部变量不需要手动包装到类中即可传给回调方法
internal sealed class AClass
{
public static void UsingLocalVariablesInTheCallbackCode(Int32 numToDo)
{
// 一些局部变量
Int32[] squares = new Int32[numToDo];
AutoResetEvent done = new AutoResetEvent(false);
// 在其他线程上执行一系列任务
for (Int32 n = 0; n < squares.Length; n++)
{
ThreadPool.QueueUserWorkItem(
obj => {
Int32 num = (Int32) obj;
// 该任务通常更耗时
squares[num] = num * num;
// 如果是最后一个任务,就让主线程继续运行
if (Interlocked.Decrement(ref numToDo) == 0)
done.Set();
}, n);
}
// 等待其他所有线程结束运行
done.WaitOne();
// 显示结果
for (Int32 n = 0; n < squares.Length; n++)
{
Console.WriteLine("Index {0}, Square={1}", n, squares[n]);
}
}
}
方法定义了一个参数 numToDo
和两个局部变量 squares
和 done
,lambda表达式的主体引用了这些变量。
C#编译器实际是像下面这样重写了代码:
internal sealed class AClass
{
public static void UsingLocalVariablesInTheCallbackCode(Int32 numToDo)
{
// 一些局部变量
WaitCallback callback1 = null;
// 构造辅助类的实例
<>c__DisplayClass2 class1 = new <>c__DisplayClass2();
// 初始化辅助类的字段
class1.numToDo = numToDo;
class1.squares = new Int32[numToDo];
class1.done = new AutoResetEvent(false);
// 在其他线程上执行一系列任务
for (Int32 n = 0; n < squares.Length; n++)
{
if (callback1 == null)
{
// 新建的委托对象绑定到辅助对象及其匿名实例方法
callback1 = new WaitCallback(class1.<UsingLocalVariablesInTheCallbackCode>b__0);
}
ThreadPool.QueueUserWorkItem(callback1, n);
}
// 等待其他所有线程结束运行
done.WaitOne();
// 显示结果
for (Int32 n = 0; n < squares.Length; n++)
{
Console.WriteLine("Index {0}, Square={1}", n, squares[n]);
}
}
// 为避免冲突,辅助类被指定了一个奇怪的名称,而且被指定为私有,禁止从AClass类外部访问
[CompilerGenerated]
private sealed class <>c__DisplayClass2 : Object
{
// 回调代码要使用的每个局部变量都有一个对应的公共字段
public Int32 numToDo;
public Int32[] squares;
public AutoResetEvent done;
// 公共无参构造器
public <>c__DisplayClass2 { }
// 包含回调代码的公共实例方法
public void <UsingLocalVariablesInTheCallbackCode>b__0(Object obj)
{
Int32 num = (Int32)obj;
squares[num] = num * num;
if (Interlocked.Decrement(ref numToDo) == 0)
done.Set();
}
}
}