其实弄.net已经快两年了,发现自己随着时间慢慢的对它的精髓有了更深的了解:
1:
委托实质上是一个类,是对方法的封装,委托内部有三个重要成员:目标,方法,前一个委托。普通代码里这样调用方法:
aBird.Fly();
封装到委托里,aBird 就是目标,Fly 就是方法。委托可以组成链,“前一个委托”用于支持这种链式结构。
MethodInvoker mi = new MethodInvoker(aBird.Fly);
mi();
这段和上面的 aBird.Fly 是一个效果,但不是直接调用。可以将 mi 传递到其他地方,再进行调用。委托在方法和调用方之间建立了间接性。
事件是基于委托的通信机制。如果一个对象的状态改变了,我们可能希望将这种改变通知给外界。从实现角度看,“通知”其实就是调用接收方的方法。困境在于,我们不知道谁对这些改变有兴趣,有多少人对这些改变有兴趣,所以,我们无法直接调用方法。.net中有两个办法解决这个问题,接口和委托。这两个的含义都是“约定”,目的则是“分离”,换句话说,就是:商量好了,分头行动。接口是对类成员的约定,委托是对参数和返回值的约定。对于简单的通知,接口有些麻烦,委托更好一些,所以,我们将一个委托加到类中,让它保存应该调用的方法。但光秃秃的一个字段有点难看,好像没穿衣服一样。为了让代码更好看,.net 引入了事件的概念。事件本质上是一个方法,事件提供 add, remove,将方法挂到这个委托中,或从此委托中移除。像这样:
button1.Click += new EventHandler(button1_Click), 订阅事件,
button1.Click -= new EventHandler(button1_Click), 退阅事件,
如果A订阅了B的事件,那么它们之间的引用关系是这样:B引用委托,委托引用A。这就是说,在退阅事件之前,A不会被当作垃圾回收,因为B包含对它的引用。
因此,委托和事件的区别是:委托与类,结构,接口,枚举是一类,
而事件属类成员,和属性,方法,字段是一类。
如果你的委托定义在类里面,那么这个委托的作用域为整个类,不是整个命名空间,在另外的类里是不能应用这个委托的,如若你的委托定义在类体之外,那么这个委托的作用域为整个命名空间,在各个类之间是可以共享的
Code
public delegate int sum(int a, int b);
public class number
{……
}
和
public class number
{
public delegate int sum(int a, int b);
……
}
是两回事情!!
而事件只能当作一个字段在类中声明
Code
public class Class1
{
public delegate void ShowEventHandler(object sender, ActionEventArgs ev);
public static event ShowEventHandler ShowEvent;
public static void OnShowEvent(object sender, ActionEventArgs ev)
{
if (ShowEvent != null)
{
ShowEvent(sender, ev);
}
}
}
2:多路广播(Multicasting)
能够引用成员方法已经很不错了,但是运用委托我们还可以做到更多。 在C#中,委托是多路广播的(multicast),也就是说它们可以一次同时指向多个方法。多路广播委托维护着一个方法列表,这些方法在该委托被调用时都将被调用。
Code
using System;
using System.IO;
namespace Akadia.SimpleDelegate
{
// 委托的定义
public class MyClass
{
// 申明一个带有一个字符串参数不返回值的委托
public delegate void LogHandler(string message);
// 使用委托就像使用方法一样。不过我们在调用前需要检查委托是否为空(委托没有引用任何方法)。
public void Process(LogHandler logHandler)
{
if (logHandler != null)
{
logHandler("Process() begin");
}
if (logHandler != null)
{
logHandler ("Process() end");
}
}
}
// 封装文件I/O操作的类
public class FileLogger
{
FileStream fileStream;
StreamWriter streamWriter;
// Constructor
public FileLogger(string filename)
{
fileStream = new FileStream(filename, FileMode.Create);
streamWriter = new StreamWriter(fileStream);
}
// 委托中将要使用的成员方法
public void Logger(string s)
{
streamWriter.WriteLine(s);
}
public void Close()
{
streamWriter.Close();
fileStream.Close();
}
}
// 调用多个委托的测试应用程序
public class TestApplication
{
// 委托中将要使用的静态方法
static void Logger(string s)
{
Console.WriteLine(s);
}
static void Main(string[] args)
{
FileLogger fl = new FileLogger("process.log");
MyClass myClass = new MyClass();
// 创造一个委托实例,引用TestApplication
// 中定义的静态方法Logger()和FileLogger类的实例f1的成员方法。
MyClass.LogHandler myLogger = null;
myLogger += new MyClass.LogHandler(Logger);
myLogger += new MyClass.LogHandler(fl.Logger);
myClass.Process(myLogger);
fl.Close();
}
}
}
测试编译:
# csc SimpleDelegate4.cs
# SimpleDelegate4.exe
Process() begin
Process() end
# cat process.log
Process() begin
Process() end
其实前面的那个订阅和退阅事件就是多播委托的一种应用
3:Events(事件)
C#中的事件模型是以事件编程模型为基础的,事件编程模型在异步编程时非常普遍。这种编程模型源于 “出版者和订阅者(publisher and subscribers)”思想。在这个模型中,出版者(subscribers)处理一些逻辑发布一个“事件”,它们仅发布这些的事件给订阅了该事件的订阅者(publishers)。
在C#中,任何对象都能发布一组其他应用程序能够订阅的事件。 当这些发布类(发布这些事件的类)触发了该事件时,所有订阅了该事件的应用程序都将被通知到。
下面是关于使用事件的一些重要惯例:
.NET Framework中的Event Handlers不返回任何值,带有2个参数
第一个参数是事件的源,也就是发布该事件的对象(译者注:具体来讲,应该是发布事件的类的实例)
第二个参数是一个继承了EventArgs的对象
事件作为发布它的类的属性存在。
关键字event(事件)控制着事件订阅类如何访问该事件属性
译者注:EventHandler是.NET Framework自带的事件委托类型。如上所叙,它带有2个参数,如果发布事件的类没有数据需要传送给订阅类,那么使用系统自带的EventHandler委托就足够了。如果类似事件第二个例子一样,需要把Clock数据传给订阅类,则通常使用自定义的委托,包含一个继承EventArgs的参数类,让这个参数类封装要传送的数据(如示例2)。
一个简单事件的示例
Code
using System;
using System.IO;
namespace Akadia.SimpleEvent
{
/* ========= Publisher of the Event ============== */
public class MyClass
{
// 定义一个名为LogHandler的委托,它封装带有一个string参数不返回任何值的方法
public delegate void LogHandler(string message);
// 定义基于上面定义的委托的事件
public event LogHandler Log;
// 代替使用委托作为参数的Process()方法。
// 调用事件,使用OnXXXX方法,XXXX是事件的名称
public void Process()
{
OnLog("Process() begin");
OnLog("Process() end");
}
// 防止事件为空,创建OnXXXX方法调用事件
protected void OnLog(string message)
{
if (Log != null)
{
Log(message);
}
}
}
// 封装文件I/O操作的类
public class FileLogger
{
FileStream fileStream;
StreamWriter streamWriter;
// 构造函数
public FileLogger(string filename)
{
fileStream = new FileStream(filename, FileMode.Create);
streamWriter = new StreamWriter(fileStream);
}
// 委托中将要使用的成员方法
public void Logger(string s)
{
streamWriter.WriteLine(s);
}
public void Close()
{
streamWriter.Close();
fileStream.Close();
}
}
/* ========= Subscriber of the Event ============== */
// 现在添加委托实例给事件变得更加简单清晰
public class TestApplication
{
static void Logger(string s)
{
Console.WriteLine(s);
}
static void Main(string[] args)
{
FileLogger fl = new FileLogger("process.log");
MyClass myClass = new MyClass();
// 订阅Logger方法和f1.Logger
myClass.Log += new MyClass.LogHandler(Logger);
myClass.Log += new MyClass.LogHandler(fl.Logger);
// 触发Process()方法
myClass.Process();
fl.Close();
}
}
}
编译测试:
# csc SimpleEvent.cs
# SimpleEvent.exe
Process() begin
Process() end
# cat process.log
Process() begin
Process() end
第二个事件例子
假设我们要创建一个Clock类,当本地时间每变化一秒钟,该类就使用事件来通知潜在的订阅者。 请看示例:
Code
using System;
using System.Threading;
namespace SecondChangeEvent
{
/* ======================= Event Publisher =============================== */
// 被其他类观察的钟(Clock)类,改类发布一个事件:SecondChange。观察该类的类订阅了该事件。
public class Clock
{
// 代表小时,分钟,秒的私有变量
private int _hour;
private int _minute;
private int _second;
// 定义名为SecondChangeHandler的委托,封装不返回值的方法,
// 该方法带参数,一个clock类型对象参数,一个TimeInfoEventArgs类型对象
public delegate void SecondChangeHandler (
object clock,
TimeInfoEventArgs timeInformation
);
// 要发布的事件
public event SecondChangeHandler SecondChange;
// 触发事件的方法
protected void OnSecondChange(
object clock,
TimeInfoEventArgs timeInformation
)
{
// Check if there are any Subscribers
if (SecondChange != null)
{
// Call the Event
SecondChange(clock,timeInformation);
}
}
// 让钟(Clock)跑起来,每隔一秒钟触发一次事件
public void Run()
{
for(;;)
{
// 让线程Sleep一秒钟
Thread.Sleep(1000);
// 获取当前时间
System.DateTime dt = System.DateTime.Now;
// 如果秒钟变化了通知订阅者
if (dt.Second != _second)
{
// 创造TimeInfoEventArgs类型对象,传给订阅者
TimeInfoEventArgs timeInformation =
new TimeInfoEventArgs(
dt.Hour,dt.Minute,dt.Second);
// 通知订阅者
OnSecondChange (this,timeInformation);
}
// 更新状态信息
_second = dt.Second;
_minute = dt.Minute;
_hour = dt.Hour;
}
}
}
// 该类用来存储关于事件的有效信息外,
// 还用来存储额外的需要传给订阅者的Clock状态信息
public class TimeInfoEventArgs : EventArgs
{
public TimeInfoEventArgs(int hour, int minute, int second)
{
this.hour = hour;
this.minute = minute;
this.second = second;
}
public readonly int hour;
public readonly int minute;
public readonly int second;
}
/* ======================= Event Subscribers =============================== */
// 一个订阅者。DisplayClock订阅了clock类的事件。它的工作是显示当前事件。
public class DisplayClock
{
// 传入一个clock对象,订阅其SecondChangeHandler事件
public void Subscribe(Clock theClock)
{
theClock.SecondChange +=
new Clock.SecondChangeHandler(TimeHasChanged);
}
// 实现了委托匹配类型的方法
public void TimeHasChanged(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Current Time: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
// 第二个订阅者,他的工作是把当前时间写入一个文件
public class LogClock
{
public void Subscribe(Clock theClock)
{
theClock.SecondChange +=
new Clock.SecondChangeHandler(WriteLogEntry);
}
// 这个方法本来应该是把信息写入一个文件中
// 这里我们用把信息输出控制台代替
public void WriteLogEntry(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Logging to file: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
/* ======================= Test Application =============================== */
// 测试拥有程序
public class Test
{
public static void Main()
{
// 创建clock实例
Clock theClock = new Clock();
// 创建一个DisplayClock实例,让其订阅上面创建的clock的事件
DisplayClock dc = new DisplayClock();
dc.Subscribe(theClock);
// 创建一个LogClock实例,让其订阅上面创建的clock的事件
LogClock lc = new LogClock();
lc.Subscribe(theClock);
// 让钟跑起来
theClock.Run();
}
}
}
结论
最后一个例子中的Clock类能够简单的实现打印时间而不是触发事件,所以为什么对关于使用委托的介绍感到烦躁呢?使用发布/订阅模式的最大好处就是当一个事件触发时能够通知任意数目的订阅类。这些订阅类不需要知道Clock类如何工作,而这个Clock类也不需要知道订阅者们如何来响应这个事件。类似的,按钮能够发布一个OnClick事件,任意数目不想关的对象能够订阅这个事件,并且在这个按钮被点击时被通知到。
发布者和订阅者通过委托很好的实现了解耦。这样大大增强了可扩展性和健壮性。 Clock类能够改变其洞察时间的方式而不会干扰到那些订阅类,订阅类也能改变其对时间改变事件作出的反应而不用打扰Clock类。两个类相互独立,互不干扰,使得维护代码变得更加容易。
4:关于系统自带的时间委托和自定义的运行时候的区别
Code
public partial class Form3 : Form
{
public delegate void ActionEventHandler(object sender, EventArgs ev);
public static event ActionEventHandler Action;
protected void OnAction(object sender, EventArgs ev)
{
if (Action != null)
{
Action(sender, ev);
}
}
public Form3()
{
InitializeComponent();
Form3.Action += new Form3.ActionEventHandler(Form3_Action);
}
private void Form3_Action(object sender, EventArgs ev)
{
this.label1.Text = "Hello world";
}
private void Form3_Load(object sender, EventArgs e)
{
OnAction(this,e);
}
}
上面是我自定义的一个事件,按照一般的理论,任何类都可以声明事件,但是要手动的触发事件时候此事件才对订阅者有效,所以我们不得不定义一个OnAction()来手动触发,我们在Form3中定义发布事件,同时自己也订阅了此事件,所以当我们触发这个事件时候,自己也能订阅到!
而系统自己的发布的事件却不需要声明一个OnEvent()来手动触发,一旦发布后,就可以触发!
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(292, 266);
this.Controls.Add(this.label1);
this.Name = "Form2";
this.Text = "Form2";
this.Load += new System.EventHandler(this.Form2_Load);
this.ResumeLayout(false);
this.PerformLayout();
这点是很重要的!
5:关于EventArgs
EventArgs是包含事件数据的类的基类,是没有事件数据的,所有基于EventArgs的类都负责在发送器和接收器之间来传送事件信息。
Code
public partial class Form1 : Form
{
public delegate void ActionEventHandler(object sender, ActionEventArgs ev);
public static event ActionEventHandler Action;
protected void OnAction(object sender, ActionEventArgs ev)
{
if (Action != null)
{
Action(sender,ev);
}
}
public Form1()
{
InitializeComponent();
Form1.Action += new Form1.ActionEventHandler(Form1_Action);
}
private void Form1_Action(object sender, ActionEventArgs ev)
{
this.label1.Text = ev.Str.ToString();
}
private void Form1_Load(object sender, EventArgs e)
{
ActionEventArgs ev = new ActionEventArgs("luhan");
OnAction(this,ev);
}
private void button1_Click(object sender, EventArgs e)
{
this.label1.Text += " panjun";
}
}
ActionEventArgs如下:
Code
public class ActionEventArgs:EventArgs
{
private string _str = "";
public ActionEventArgs(string str)
{
this._str = str;
}
public string Str
{
get
{
return _str;
}
set
{
_str = value;
}
}
}
以后如果再有什么心得就继续和大家一起分享!