C#委托及事件
C#委托及事件
在C#中,委托(delegate)是一种引用类型,在其他语言中,与委托最接近的是函数指针,但委托不仅存储对方法入口点的引用,还存储对用于调用方法的对象实例的引用。
简单的讲委托(delegate)是一种类型安全的函数指针,首先,看下面的示例程序,在C++中使用函数指针。
首先,存在两个方法:分别用于求两个数的最大值和最小值。
int Max(int x,int y)
{
return x>y?x:y;
}
int Min(int x,int y)
{
return x
}
上面两个函数的特点是:函数的返回值类型及参数列表都一样。那么,我们可以使用函数指针来指代这两个函数,并且可以将具体的指代过程交给用户,这样,可以减少用户判断的次数。
下面我们可以建立一个函数指针,将指向任意一个方法,代码如下所示:
//定义一个函数指针,并声明该指针可以指向的函数的返回值为int类型,参数列表中包//括两个int类型的参数
int (*p)(int,int);
//让指针p指向Max函数
p=max;
//利用指针调用Max
c=(*p)(5,6);
我们的问题在于,上面的代码中,为什么不直接使用Max函数,而是利用一个指针指向Max之后,再利用指针调用Max函数呢?
实际上,使用指针的方便之处就在于,当前时刻可以让指针p指向Max,在后面的代码中,我们还可以利用指针p再指向Min函数,但是不论p指向的是谁,调用p时的形式都一样,这样可以很大程度上减少判断语句的使用,使代码的可读性增强!
在C#中,我们可以使用委托(delegate)来实现函数指针的功能,也就是说,我们可以像使用函数指针一样,在运行时利用delegate动态指向具备相同签名的方法(所谓的方法签名,是指一个方法的返回值类型及其参数列表的类型)。
6.1 使用委托(delegate)
6.1.1委托的建立
建立委托(delegate),过程有点类似于建立一个函数指针。过程如下:
1. 建立一个委托类型,并声明该委托可以指向的方法的签名(函数原型)delegate void MyDelegate(int a,int b);
2.建立一个委托类的实例,并指向要调用的方法
//利用委托类的构造方法指定,这是最为常见的一种方式
MyDelegate md=new MyDelegate(Max);
//利用自动推断方式来指明要调用的方法,该形式更类型于函数指针
MyDelegate md=Max;
3.利用委托类实例调用所指向的方法
int c=md(4,5);
下面通过实例来演示C#中委托的使用。
●案例操作020601:利用委托实现方法的
动态调用
首先,添加如下控件:
?两个RadioButton,分别用来让用户选
择求最大值以及求最小值
?二个TextBox,用来输入两个操作数
?一个TextBox,用来显示运算结果
?一个Button,用来执行运算
界面如下图所示:
下一步,在窗口中添加两个方法:Max,Min,这两方法的代码如下:
int Max(int x,int y)
{
return x>y?x:y;
}
int Min(int x,int y)
{
return x
}
窗口中的代码,如下图所示:
下一步:为了使用委托来实现动态指向,我们需要建立一个委托类“MyDelegate”,并建立该委托类型的一个实例,如下图所示:
上面的代码中,我们可以发现,此时,还没有让MyDelegate类型的实例“md”指向任何一个方法(即:md的值为null),原因是:在编写代码的时候,我们还不知道用户想要调用哪一个方法。
下一步,分别为两个RadioButton编写它们的“CheckedChanged”事件,代码如下:private void rbtMax_CheckedChanged(object sender, EventArgs e)
{
if (this.rbtMax .Checked ==true)
{
this.md = new MyDelegate(this.Max );
}
}
private void rbtMin_CheckedChanged(object sender, EventArgs e)
{
if (this.rbtMin .Checked ==true)
{
this.md = new MyDelegate(this.Min );
}
}
这段代码是,如果用户选择了求最大值的RadioButton,则让MyDelegate类型的实例“md”指向Max方法,如果用户选择了求最小值的RadioButton,则让MyDelegate类型的实例“md”指向Min方法。这样作的目的,就是要把选择的过程交给用户。
下一步,我们为界面中的Button编写Click事件,并利用委托来调用求最值的方法。代码如下所示:
private void btGetResult_Click(object sender, EventArgs e) {
if (this.md ==null )
{
MessageBox.Show("委托md没有指向任何方法!");
return;
}
int a = int.Parse(this.tbxOP1 .Text );
int b = int.Parse(this.tbxOP2 .Text );
int c = this.md(a,b);
this.tbxResult.Text = c.ToString();
}
从上面的代码中,可以发现,在使用委托之前,先要判断其值是否为空,如果不为空,则可以进行调用,同时,使用者可以看到,在调用md时,我们并没有关心md到底指向了哪一个方法,总之,md不为空的时候,就一定会指向Max和Min当中的一个。
为了让求最大值的RadioButton在程序开始运行的时候就被选中,在Form的Load 事件中添加如下代码:
private void Form1_Load(object sender, EventArgs e)
{
this.md = new MyDelegate(this.Max );
}
运行的效果如下图所示:
求最大值
求最小值
●委托使用的注意事项
?在C#中,所有的委托都是从
System.MulticastDelegate类派生的。
?委托隐含具有sealed属性,即不能用
来派生新的类型。
?委托最大的作用就是为类的事件绑定
事件处理程序。
?在通过委托调用函数前,必须先检查
委托是否为空(null),若非空,才能调
用函数。
?在委托实例中可以封装静态的方法也
可以封装实例方法。
?在创建委托实例时,需要传递将要映
射的方法或其他委托实例以指明委托将
要封装的函数原型(.NET中称为方法签
名:signature)。注意,如果映射的是
静态方法,传递的参数应该是类名.方法
名,如果映射的是实例方法,传递的参数
应该是实例名.方法名。
?只有当两个委托实例所映射的方法以
及该方法所属的对象都相同时,才认为它
们是想等的(从函数地址考虑)。
6.1.2 讨论委托类型
从上面的案例中,我们可以发现,在使用委托之前,先要定义一个委托类型,如下所示:
delegate int MyDelegate(int a, int b);
MyDelegate md = null;
既然叫做委托类型,就说明MyDelegate实际上是一个类,上面的写法只是一种简单的缩略写法,实际上,我们自己定义的委托,都是继承自System.MulticastDelegate类的,但是我们确不能自己定义一个类去继承自System.MulticastDelegate类,为了证明这点,我们可以使用ildasm工具,来查看“MyDelegate”的IL代码。
首先在Visual Studio控制台,如下图所示:
在打开的“Visual Studio2008 Command Prompt”窗口中,输入ildam,如下图所示:
运行之后,会出现ildasm的窗口,如下图所示:
下一步,打开刚才编译好的exe程序,如下图所示:
展开结点,并找到“MyDelegate”类型,将其展开,如下图所示:
在上图中,我们可以看到,对“MyDelegate”,存在如下“说明”:
extends [mscorlib]System.MulticastDelegate
与此同时,还存在着四个方法,即:
?.ctor:构造方法
?BeginInvoke
?EndInvoke
?Invoke
●MulticastDelegate类
MultiDelegate类是一个特殊类(Special Class),和System.Delegate类一样,该类只能够被编译器以及内置的工具类所继承,我们自定义的类是不能够显式的继承自该类的。
MultiDelegate类当中可以包括一个委托的链表,这个表中,可以包括一个或多个元素(每个元素都是一个委托),我们可以将这个表称为调用链。当我们调用一个MultiDelegate的时候,位于该MultiDelegate调用链中的委托就会被串行调用。这样我们就可以只调用一个方法,而多个相同签名的方法就会同时被串行调用。关于多播委托的说明,我们会在后面的内容中进行讲解。
●Invoke方法
为了解释Invoke方法,我们先来回顾一下,当一个委托指向了一个方法时是如何调用的,代码如下所示:
int c = this.md(a,b);
我们在调用委托,并执行该委托所指向的方法时,本质上就是调用了其Invoke 方法。实际上,我们可以直接调用其Invoke方法,代码如下所示:
int c = this.md.Invoke(a,b);
另外,与Invoke方法对应的BeginInvoke,是对Invoke方法的一个异步调用,而EndInvoke是异步调用完成后的处理方法,关于异步调用的说明,我们将在多线程的章节中进行说明。
6.1.3 使用多播委托(MulticastDelegate)
前面刚刚提及到MulticastDelegate,下面我们来看一下它的应用。
有的时候,我们想要调用一个委托,但同时可以执行多个方法(自定义事件中最为常见),比如,一个工作文档生成之后,系统要将生成文档日志,而且还要被保存到数据库中,对于以上二个操作,如果只想调用一个委托,就可以顺序完成,那么使用多播委托,就可以实现。
多播委托(MulticastDelegate)提供了一种类似于流水线式的钩子机制,只要加载到这条流水线上的委托,都会被顺序执行。因为所有的委托都继承自MulticastDelegate,因此所的委托都具备多播特性。
下面能过一个控制台程序来说明多播委托(MulticastDelegate)的使用方式。
●案例操作050602:使用多播委托
首先,建立一个控制台程序。在其中添加两个具备相同签名的方法:
?void CreateLogFile(string
originalPath):用于创建日志文件
?void WriteToDb(string
originalPath):用于将文件写入数据库
代码如下:
方法:void CreateLogFile(string originalPath)
///
/// 用于生成日志文档
///
/// 文件的原始路径
static void CreateLogFile(string originalPath)
{
if (!Directory .Exists ("log"))
{
Directory.CreateDirectory("log");
}
StreamWriter sw = new StreamWriter("log/log.txt" ,true);
sw.WriteLine("新文件已经创建,创
建时间:{0},文件路径:
{1}",DateTime .Now .ToLongTim
eString (),originalPath ); sw.Close();
Console.WriteLine("已经写入日志!");
}
方法:void WriteToDb(string originalPath)
///
/// 用于将文件写入数据库
///
/// 文件的原始路径
static void WriteToDb(string originalPath)
{
FileStream fs = new FileStream(originalPath ,FileMode.Open );
var buffer=new byte[fs.Length ];
fs.Read(buffer ,0,buffer.Length );
fs.Close();
SqlConnection con = new
SqlConnection("server=.
;database=test;uid=sa;p
wd=sa");
SqlCommand cmd = con.CreateCommand();
https://www.360docs.net/doc/65396273.html,mandText = "insert
into tb_files
values(@ID,@FileName,@C
reationTime,@FileBytes)
";
cmd.Parameters.Add("@I
D",SqlDbType.UniqueIde
ntifier).Value=Guid.Ne
wGuid ();
c
m
d
.
P
a
r
a
m
e
t
e
r
s
.
A
d
d
" @ C r e a t i o n T i m e " , S q l D b T y p e . D a t e T i m e ) . V a l u e
=
a t e T i m e . N o w
; c m d . P a r a m e t e r s . A d d ( " @ F i l e N a m e "
S q l D b T y p e . N T e x t ) . V a l u e = P a t h . G e t F i l e N a m e
( o r
g
i
n
a
l
P
a
t
h
)
;
cmd.Parameters.Add("@FileBytes",SqlDbType.Image ).Value=buffer ;
con.Open();
cmd.ExecuteNonQuery();
con.Close();
Console.WriteLine("已经写入数据库");
}
上面两个方法,具备相同签名,如果想同时串行调用这两个方法,还要定义一个
委托类型,代码如下:
///
/// 生成一个委托,用于实现多播操作
///
/// 文件的原始路径
delegate void MyMulticastDelegate(string path);
主函数代码如下所示:
static void Main(string[] args)
{
//创建原始文件
StreamWriter sw = new StreamWriter("new file.txt",false );
sw.WriteLine("this is a new file");
sw.Close();
//创建委托,并指向CreateLogFile方法
MyMulticastDelegate logDelegate=new MyMulticastDelegate
(CreateLogFile);
//创建委托,并指向WriteToDb方法
MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb ); MyMulticastDelegate multicastDelegate = logDelegate;
//在多播委托的调用链中添加新的委托元素
multicastDelegate = multicastDelegate + dbDelagate;
//调用多播委托,并且序列执行两个委托所指向的方法
multicastDelegate("new file.txt");
}
在主函数中,首先创建一个原始文件,然后建立两个委托分别指向CreateLogFile 方法以及WriteToDb方法,如下代码段所示:
MyMulticastDelegate logDelegate=new
MyMulticastDelegate(CreateLogFile);
MyMulticastDelegate dbDelagate = new MyMulticastDelegate(WriteToDb );下一步,将这两个方法合并到一个多播委托中,代码如下所示:MyMulticastDelegate multicastDelegate = logDelegate;
multicastDelegate = multicastDelegate + dbDelagate;
最后,利用多播委托,同时串行执行两个操作,代码段如下所示:multicastDelegate("new file.txt");
从上面的代码中,我们可以发现,对于两个委托来讲,“+”加操作是有意义的。如下面代码所示:
MyMulticastDelegate multicastDelegate = logDelegate;
multicastDelegate = multicastDelegate + dbDelagate;
这一点可以说明,如果想要将两个委托,放入到一个多播委托的调用链中,可以使用“+”操作符,换句话说,对于委托的“+”操作,就是在调用链中增加一个新的结点,并将一个新委托放置到该结点中。另外,和int类型的自加操作类似,委托的自加操作也进行简写,这种写法在注册事件的时候较为常用,代码如下:MyMulticastDelegate multicastDelegate = logDelegate;
multicastDelegate += dbDelagate;
该案例的完整代码如下:
///
/// 用于生成日志文档
///
/// 文件的原始路径
static void CreateLogFile(string originalPath)
{
if (!Directory .Exists ("log"))
{
Directory.CreateDirectory("log");
}
StreamWriter sw = new StreamWriter("log/log.txt" ,true);
sw.WriteLine("新文件已经创建,创
建时间:{0},文件路径:
{1}",DateTime .Now .ToLongTim
eString (),originalPath ); sw.Close();
Console.WriteLine("已经写入日志!");
}
///
/// 用于将文件写入数据库
///
/// 文件的原始路径
static void WriteToDb(string originalPath)
{
FileStream fs = new FileStream(originalPath ,FileMode.Open );
var buffer=new byte[fs.Length ];
fs.Read(buffer ,0,buffer.Length );
fs.Close();
SqlConnection con = new
SqlConnection("server=.
;database=test;uid=sa;p
wd=sa");
SqlCommand cmd = con.CreateCommand();
https://www.360docs.net/doc/65396273.html,mandText = "insert
into tb_files
values(@ID,@FileName,@C
reationTime,@FileBytes)
";
cmd.Parameters.Add("@I
D",SqlDbType.UniqueIde
ntifier).Value=Guid.Ne
wGuid ();
c
m
d
.
P
a
r
a
m
e
t
e
r
s
.
A
d
d
(
"
@
C
r
a t i o n T i m e " , S q l D
b T y p e . D a t e T i m e ) . V a l u e
= D a t e T
m e . N o w
; c m d . P a r a m e t e r s . A d d ( " @ F i l e N a m e " , S q l D
T y p e . N T e x t ) . V a l u e = P a t h . G e t F i l e N a m e
( o r i g i n a