最近在学委托,经过摘录、整理,总结如下:
回调(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对象数组,再用上面的迭代方式获得返回结果。
4、匿名方法
使用委托还有另外一种方式:通过匿名方法。匿名方法是用作委托参数的一个代码块。
如下代码:
using System;
namespace DelegateTest
{
class Program
{
delegate string delegateString(string val);
static void Main()
{
string mid=",middle part";
delegateString anonDel=delegate(string param)
{
param+=mid;
param+=" and end";
return param;
};
Console.WriteLine(anonDel("Strat of string"));
}
}
}
该代码块使用方法级的字符串变量mid,该变量是在匿名方法的外部定义的,并添加到要传送的参数中,接着代码返回该字符串值。匿名方法的优点是减少要编写的代码。不必定义仅由委托使用的方法。在为事件定义委托时,这是很显然的。这有助于减低代码的复杂性,尤其是定义了好几个事件时,代码会显得比较简单。使用匿名方法时,代码执行得不太快。
在使用匿名方法时,必须遵循一些规则:
1)在匿名方法中不能使用跳转语句跳到该匿名方法的外部;
2)匿名方法外部的跳转语句不能跳到该匿名方法的内部;
3)在匿名方法内部不能访问不安全代码,也不能访问在匿名方法外部使用的ref和out参数,但可以使用在匿名方法外部定义的其他变量。
5、λ表达式
这是C# 3.0为匿名方法提供的一个新方法。如前面的语句:
...
static void Main()
{
string mid=...;
delegateString anonDel=param=>
{
param+=mid;
param+=" and end";
return param;
};
...
}
...
λ表达式=>的左边列出了匿名方法需要的参数,右边列出了实现代码,实现代码放在花括号中,类似于前面的匿名方法,如果实现代码只有一行,可以删除花括号和return语句,编译器会自动添加该语句。
如:public delegate bool Predicate(int val);
Predicate pl=x=>x>5;
在上面的λ表达式中,左边定义了变量x,这个变量的类型自动设置为int,因为这是通过委托定义的,实现代码返回比较x>5布尔结果。如果x大于5,则返回true,否则返回false。
6、协变和抗变
委托调用的方法不需要与委托声明的定义类型相同。由此出现协变和抗变。
1)返回类型协变
方法的返回类型可以派生于委托定义的类型。如下代码:
public class DelegateReturn
{
}
public class DelegateReturn2:DelegateReturn
{
}
public delegate DelegaReturn MyDelegate1();
class Program
{
static void Main()
{
MyDelegate1 d1=Method1;
d1();
}
static DelegateReturn2 Method1()
{
DelegateReturn2 d2=new DelegateReturn2();
return d2;
}
}
上述代码中,委托MyDelegate定义为返回DelegateReturn类型。赋予委托实例d1的方法返回DelegateReturn2类型,DelegateReturn2派生自Delegate,根据子类“是”父类的这种关系,满足了委托的需求。这称为返回类型的协变。
2)参数类型的抗变
委托定义的参数可能不同于委托调用的方法,这里是返回类型不同,因为方法使用的参数类型可能派生自委托定义的类型。如下代码:
public class DelegateParam
{
}
public class DelegateParam2:DelegateParam
{
}
public delegate void MyDelegate2(DelegateParam2 p);
class Program
{
static void Main()
{
MyDelegate2 d2=Method2;
DelegateParam2 p=new DelegateParam2();
d2(p);
}
static void Method2(DelegateParam p)
{
}
}
上述代码中,委托使用的参数类型是DelegateParam2,而赋予委托实例d2的方法使用的参数类型是DelegateParam,DelegateParam是DelegateParam2的基类。