C#委托学习笔记
时间:2010-12-30 来源:阿寻
回调(Callback)函数是windows编程的一个重要部分。回调函数实际上是方法调用的指针,也成为函数指针,是一个非常强大的编程特性。.NET以委托的形式实现了函数的指针的概念。与C/C++的函数指针不同的是.NET委托是类型安全的。也就是说C/C++的函数指针只不过是一个指向内存单元的指针,我们无法知道这个指针实际指向什么,像参数和返回类型等就无从知晓了。
当把方法传送给其他方法时,需要用到委托。如考虑以下的函数:
C++:
#include <iostream>
#include <string>
using namespace std;
int fun(int);
int fun_call(int (*f)(int),int);
void main(int argc,char* argv[])
{
typedef int (*fp)(int);
fp fpt;
fpt=fun;
count<<fun_call(fpt,1);
}
int fun(int a)
{
return a-1;
}
int fun_call(int (*fp)(int),int b)
{
return (fp(10)+b);
}
上述程序的“ftp=fun”实现函数指针的初始化,直接将fun的地址赋给函数指针ftp,然后传送给fun_call,fun_call可以根据这两个参数计算出结果:fp(10)=9,9+1=10。实现了把方法传送给其他方法。
函数指针最常用的是使用函数指针数组来批量调用函数:
int f1(){return 1;}
int f2(){return 2;}
int f3(){return 3;}
void main(int argc,char* argv[])
{
tpyedef int (* fp)();
fp fps[3]={f1,f2,f3};
for(int 0;i<2;i++)
{
cout<<fps[i]<<endl; //实现按数组序列号调用函数
}
}
在编译时我们不知道第二个方法会是什么,这个信息只能在运行时得到,所以需要把第二个方法作为参数传递给第一个方法。在C/C++,只能提取函数的地址,并传送为一个参数。c是没有类型安全性的,可以把任何函数传送给需要函数指针的方法。这种直接的方法会导致一些问题,例如类型安全性,在面向对象编程中,方法很少是孤立存在的,在调用前通常需要与类实例相关联。而这种指针的方法没考虑这种情况。所以.NET在语法上不允许使用这种直接的方法。如果要传递方法,就必须把方法的细节封装在一种新的类型的对象中,这种新的对象就是委托。
委托,实际上只是一种特殊的对象类型,其特别之处在于,我们之前定义的所有对象都包含数据,而委托包含的只是函数的地址。
1、在c#中声明委托
delegate void Method(int x);
定义了委托就意味着告诉编译器这种类型的委托代表了哪种类型的方法,然后创建该委托的一个或多个实例。编译器在后台将创建表示该委托的一个类。也就是说,定义一个委托基本上是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在类的内部定义,也可以在类的外部定义。注意,委托是类型安全性非常高的,因此定义委托时,必须给出它所代表的方法签名和返回类型等全部细节。
2、在C#中使用委托
using System;
namespace DelegateSpace
{
class DelegateTest
{
private delegate string GetString();
static void Main()
{
Test test=new Test();
GetString method=new GetString(test.Add);
Console.WriteLine(method());
}
}
class Test
{
public string Add(int x,int y)
{
return (x+y).ToString();
}
}
}
上述程序中声明了类型为GetString的委托,并对它初始化,使它指向对象test的方法Add(int x,int y)。在C#中,委托在语法上总是带有一个参数的构造函数,这个参数就是委托指向的方法,这个方法必须匹配最初定义委托时的签名。如上例中委托是这样定义的:“delegate string GetString();”要求被委托的函数的返回类型是string,如果test.Add(int x,int y)返回的是int,则编译器就会报错。还要注意赋值的语句:“GetString method=new GetString(test.Add);“不能写成"GetString method=new GetString(test.Add(3,2));"因为test.Add(2,3)返回的是string。而委托的构造函数需要把传进的是函数的地址,这很像C/C++的函数指针。
3、多播委托
调用委托的次数与调用方法的次数相同,如果要调用多个方法,就需要多次显式调用这个委托。委托也可以包含多个方法。这种委托称为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。所以,委托的签名必须返回void,否则,就只能得到委托调用的最后一个方法的结果。
如:
delegate void DoubleOp(double value);
class MainEntry
{
static void Main()
{
DoubleOp operations=MathOperation.MultiplyByTwo;
operations+=MathOperation.Square;
}
}
class MathOperation
{
public static double MultiplyByTwo(double value)
{
return value*2;
}
public static double Square(double value)
{
return value*value;
}
}
上面的“DoubleOp operation=MathOperation.MultiplyByTwo;operation+=MathOperation.Square;” 等价于“DoubleOp operation1=MathOperation.MultiplyByTwo;DoubleOp operation2=MathOperation.Square;DoubleOP operations=operation1+operation2;”,多播委托还可以识别运算符-和-=,用于从委托中删除方法调用。
通过一个多播委托调用多个方法还有一个大问题。多播委托包含一个逐个调用委托的集合。如果通过委托调用一个方法抛出异常,整个迭代就会终止。在这种情况下,为了避免这个问题,应手动迭代方法列表。可以使用Delegate类定义的方法GetInvocationList(),它返回一个Delegate对象数组。
如考虑以下代码:
public delegate void DemoDelegate();
class Program
{
static void One()
{
Console.WriteLine("One");
throw new Exception("Test!");
}
static void Two()
{
Console.WriteLine("Two");
}
static void Main()
{
DemoDelegate dl=One;
dl+=Two;
try
{
dl();
}
catch(Exception)
{
Console.WriteLine("Exception caught!");
}
}
}
运行结果:
One
Exception caught!
修改后的代码如下:
static void Main()
{
DemoDelegate dl=One;
dl+=Two;
Delegate[] delegates=dl.GetInvocationList();
foreach(DemoDelegate d in delegates)
{
try
{
d();
}
}
catch(Exception)
{
Console.WriteLine("Exception caught");
}
}
运行结果如下:
One
Exception caught
Two
同样地,如果委托签名不是返回void,但希望得到所有的经委托调用后的结果,也可以用GetInvocationList()得到Delegate对象数组,再用上面的迭代方式获得返回结果。