Long time without coding,貌似对programming都失去了曾有的一点点sense了,今日有空再细瞄一下.net的委托和事件。
Delegate
- 首先,委托用于引用一类具有相同返回值和参数列表的方法(可以引用静态方法或者是实例方法),类似于函数指针,用于实现函数回调。
例如,我们如下声明了委托ProgressChangedDelegate,用于引用参数是int,返回void的方法。/// <summary> /// 进度改变通知委托 /// </summary> /// <param name="value">当前进度(0~100)</param> public delegate void ProgressChangedDelegate(int value);
- 委托是类型安全的,且委托就是一个类型,它是对特定签名(返回值和参数列表)的方法的包装。
看一下上面代码生成的IL代码,编译器其实在后面帮我们生成了一个ProgressChangedDelegate的类型,
且它是继承自类型System.MulticastDelegate(多播委托) - 委托类型的实例化。下面分别用静态方法和实例方法来实例化委托对象。
分析IL,C#简洁语法的背后,委托的实例化是通过传入方法指针和方法执行的对象的引用(静态方法是null)来执行构造函数来完成的。//declare and initial a delegate with static method ProgressConsoleDisplayer progressConsoleDisplayer = new ProgressConsoleDisplayer(); ProgressChangedDelegate progressDelegate = new ProgressChangedDelegate(ProgressConsoleDisplayer.DisplayProgress); //delegate refer to instance method ProgressFileDisplayer fileDisplayer = new ProgressFileDisplayer(@"c:\progress.log"); ProgressChangedDelegate anotherProgressDelegate = new ProgressChangedDelegate(fileDisplayer.DisplayProgress);
- 委托的调用。委托实例调用的方式跟调用它引用的方法一样。
progressDelegate(progressValue); anotherProgressDelegate(progressValue);
分析IL,C#自然的调用语法后面其实是调用委托对象的invoke方法(最后是调用所引用的MethodInfo的Invoke)。
- 既然delegate关键字声明的委托是多播委托,我们可以使用“+=”,“-=”操作符对两个委托对象进行捆绑和解除捆绑。
通过IL可以看到,这两个操作符的背后其实是调用Delegate类型的Combine和Remove两个静态方法来实现的。
Delegate Chain.
上面说的delegate声明的委托多播委托,也就是说它可以绑定多个方法进行回调。如下代码,进度的变更会依次显示到控制台和记录到文本。
///
/// 进度改变通知委托
///
/// 当前进度(0~100)
public delegate void ProgressChangedDelegate(int value);
class Program
{
static void Main(string[] args)
{
//declare and initial a delegate with static method
ProgressConsoleDisplayer progressConsoleDisplayer = new ProgressConsoleDisplayer();
ProgressChangedDelegate progressDelegate
= new ProgressChangedDelegate(ProgressConsoleDisplayer.DisplayProgress);
//delegate refer to instance method
ProgressFileDisplayer fileDisplayer = new ProgressFileDisplayer(@"c:\progress.log");
progressDelegate += fileDisplayer.DisplayProgress;
for (int progressValue = 0; progressValue <= 100; progressValue += 10)
{
progressDelegate(progressValue);
}
Console.Read();
}
}
///
/// 进度控制台输出器
///
public class ProgressConsoleDisplayer
{
public static void DisplayProgress(int value)
{
Console.WriteLine(string.Format("Console Display:Current Progress is {0}", value));
}
}
///
/// 进度文件输出器
///
public class ProgressFileDisplayer:IDisposable
{
private readonly FileStream fileStream=null;
private readonly StreamWriter writer = null;
public ProgressFileDisplayer(string filePath)
{
FileStream fileStream=File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
writer = new StreamWriter(fileStream);
}
public void DisplayProgress(int value)
{
writer.WriteLine(string.Format("File Display:Current Progress is {0}", value));
writer.Flush();
}
public void Dispose()
{
writer.Dispose();
fileStream.Dispose();
}
}
1.委托链是通过Delegate.Combine和Remove两个静态方法来进行绑定和删除操作的,每次Combine和Remove以后委托都会指向一个新的对象。
![image_thumb31 image_thumb31](https://images2017.cnblogs.com/blog/172622/201801/172622-20180112001111535-201083746.png)
2.委托的调用。多个委托对象combine后会放到内部的集合(数组)中保存,当调用时会按顺序调用数组中的每个元素,调用返回的结果是最后一个委托调用返回的结果。
delegate int ReturnIntDelegate();
public static void Run()
{
ReturnIntDelegate delegateInstance = () => { return 1; };
delegateInstance += () => { return 2; };
delegateInstance += () => { return 3; };
int result = delegateInstance();
Console.WriteLine(string.Format("delegate call result:{0}", result));
}
![image_thumb5 image_thumb5](https://images2017.cnblogs.com/blog/172622/201801/172622-20180112001112019-497930875.png)
委托链中委托的调用时简单的顺序调用,如果委托数组中的一个委托调用发生未处理的异常则后续的委托都不能调用,为避免这个问题,当然希望在每个委托调用时加入异常处理。又或者,我们希望每个委托在单独的线程上同时执行以避免阻塞,提高性能。总之,基于稳定和性能考虑,我们当然希望可以手工控制每个委托的调用。
![image_thumb7 image_thumb7](https://images2017.cnblogs.com/blog/172622/201801/172622-20180112001112879-855802836.png)
使用MulticastDelegate类的GetInvocationList实例方法可以获取Delegate[]对象,通过遍历数组,控制每个Delegate委托的调用。下面修改上面的代码,加入异常处理。
delegate int ReturnIntDelegate();
public static void Run()
{
ReturnIntDelegate delegateInstance = () => { return 1; };
delegateInstance += () =>
{
throw new Exception();
return 2;
};
delegateInstance += () => { return 3; };
//int result = delegateInstance();
int result = 0;
Delegate[] delegates= delegateInstance.GetInvocationList();
if (delegates != null)
{
foreach (ReturnIntDelegate del in delegates)
{
try
{
result=del();
}
catch (Exception ex)
{
//just ignore
}
}
}
Console.WriteLine(string.Format("delegate call result:{0}", result));
}
![image_thumb13 image_thumb13](https://images2017.cnblogs.com/blog/172622/201801/172622-20180112001113363-2114161845.png)
Event
事件机制是观察者模式的实现,当对象(Project)的状态改变,一个或者多个观察者(observer)会得到通知并做相应的更新。
-
在观察者模式中,观察者继承的基类或者接口都包含了一个方法供被观察对象来调用发送消息,这个方法其实
就是消息的契约,在事件机制中对应的就是委托,所以我们声明事件成员时需要指定事件的委托类型。
例如,我们在一个作业类Job中声明一个任务失败MissionFailed事件。public class Task { /// <summary> /// occur when task runs failed /// </summary> public event EventHandler MissionFailed; }
这里我使用的是.net库中的委托EventHandler,为遵守建议的命名规范,即使是自定义事件的委托,名称应该以事件名称开始,以EventHandler为后缀。
被观察者应该仅发送消息,而不会关心观察者的处理消息细节,所以这个标识消息的委托应该是无返回值的。
可以看一下EventHandler及其泛型版本的定义: -
事件消息的内容体现在委托的方法参数中,一般包含消息的发送者对象和额外参数对象,包含额外参数的类
按照规范应该以继承自EventArgs类,且名称以EventArgs结尾。
如我们定义任务失败事件的额外信息类,包含了错误信息字段。
public class MissionFailedEventArgs:EventArgs { public MissionFailedEventArgs(string error) { Message = error; } /// <summary> /// Error Info /// </summary> public string Message { get; private set; } }
相应修改事件声明:public event EventHandler<MissionFailedEventArgs> MissionFailed;
-
观察者模式中被观察者内部维护了一个观察者的列表,当观察者订阅通知时会被添加到此列表。当被观察者发送消息时,它会遍历列表,执行方法发送消息。
在事件中,这个是通过委托的多播来实现的。
首先,事件成员的本质一组对应于订阅事件和取消订阅事件的方法组。(我们可以对这两个方法进行自定义来实现事件的定制)
查看IL代码,事件的默认实现中编译器会自动为我们生成一个可访问性为private的跟事件同名的委托成员,还有add_[EventName]和remove_[EventName]
两个方法,而事件的订阅+=和取消订阅-=就是调用这两个方法来实现,而这两个方法的具体实现最终还是落到Delegate的Combine和Remove操作。
所以说.net的事件是基于委托的,是委托的封装。 -
触发事件。一般会在类中定义一个名为On[EventName]的Protected虚函数来引发事件。
/// <summary> /// Fired the MissionFailed Event /// </summary> protected virtual void OnMissionFailed(MissionFailedEventArgs e) { if (MissionFailed != null) //this should not be thread-safe { MissionFailed(this, e); } }
然后在需要的地方调用上面的方法触发事件,发送通知,如下在任务执行发生异常时触发事件。/// <summary> /// Run Task /// </summary> public void Run() { try { } catch (Exception ex) { OnMissionFailed(new MissionFailedEventArgs(e.Message)); throw; } }
可以猜测事件触发,根本上应该是委托的调用,查看IL代码可以证实。 - 事件的自定义实现。事件成员本质是一组方法,编译器会自动生成默认的实现代码,但我们也可以自定义Add,Remove方法显示定义事件。
可以参考window Form中的事件定义,由于事件默认的隐式定义会自动为每个事件成员定义一个委托成员,而如果事件没有被使用,会造成
内存空间的浪费。所以Form中的事件实现是通过字典的形式来操作委托:public class BusinessComponent:Component { private static object EVENT_BUSINESSSTART = new object(); private static object EVENT_BUSINESSEND = new object(); /// <summary> /// 业务开始事件 /// </summary> public event EventHandler BusinessStart { add { Events.AddHandler(EVENT_BUSINESSSTART, value); } remove { Events.RemoveHandler(EVENT_BUSINESSSTART, value); } } /// <summary> /// 业务终止事件 /// </summary> public event EventHandler BusinessEnd { add { Events.AddHandler(EVENT_BUSINESSEND, value); } remove { Events.RemoveHandler(EVENT_BUSINESSEND, value); } } }
下面尝试过滤某些事件订阅,同一个事件处理方法仅允许订阅一次。通过遍历委托调用列表,检查是否已经存在相同的委托。private EventHandler<MissionFailedEventArgs> missionFailed; /// <summary> /// occur when task runs failed /// </summary> public event EventHandler<MissionFailedEventArgs> MissionFailed { add { Delegate[] delegates = null; if (missionFailed != null && (delegates = missionFailed.GetInvocationList()) != null) { foreach (Delegate d in delegates) { if (d.Equals(value)) return; } } missionFailed+= value; } remove { } }
调用代码对事件使用同一方法订阅了两次。private static void TaskMissionFailedHandler(object sender, MissionFailedEventArgs e) { Console.WriteLine(string.Format("Time:{0},task run.", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))); }
static void Main(string[] args) { Task task = new Task(); task.MissionFailed += TaskMissionFailedHandler; task.MissionFailed += TaskMissionFailedHandler; task.Run(); Console.Read(); }
但只有一次订阅成功。
References:
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/
http://csharpindepth.com/Articles/chapter2/events.aspx
http://www.oodesign.com/observer-pattern.html