重温CLR(十二) 委托
回调函数是一种非常有用的编程机制,它的存在已经有很多年了。.NET通过委托来提供回调函数机制。不同于其他平台(比如非托管C++)的回调机制,委托的功能要多得多。例如,委托确保回调方法是类型安全的(这是clr最重要的目标之一)。委托还允许顺序调用多个方法,并支持调用静态方法和实例方法。
初识委托
c“运行时”的qsort函数获取指向一个回调函数的指针,一遍对数组中的元素进行排序。在windows中,窗口过程、钩子过程和异步过程调用等都需要回调函数。.net framework中,回调方法的应用更是广泛。例如,可以登记回调方法来获取各种各样的通知,例如未处理的异常、窗口状态变化、菜单项选择、文件系统变化、窗体控制事件和异步操作已完成等。
在非托管C/C++中,非成员函数的地址只是一个内存地址。这个地址不携带任何额外信息,。比如函数期望收到的参数个数、参数类型、函数返回值类型以及函数调用协定。简单地说,非托管C/C++回调函数不是类型安全的(不过他们确实是一种非常轻量级的机制)。
.NET的回调函数和非托管windows编程环境的回调函数一样有用,一样普遍。但是,.net提供了称为委托的类型安全机制。为了理解委托,先来看看如何使用它。
委托4个最基本的步骤:
1)定义委托类型
2)有一个方法包含要执行的代码(签名要与委托相同)
3)创建一个委托实例化(包含声明委托对象)
4)执行调用(invoke)委托实例
具体解释如下:
1.定义委托类型
委托类型就是参数类型的一个列表以及一个返回类型。
delegate void StringProcessor(string input);
其中的StringProcessor是一个类型。
2.定义签名相同的方法
定义的方法要与委托有类型相同的返回值和参数。
private void GetStringLength(object x){} //C#2.0以后认为一致
3.创建委托实例
创建委托实例就是指定在调用委托实例时执行的方法。
StringProcessor proc1,proc2
//GetStringLength 实例方法
proc1= new StringProcessor(GetStringLength);
//GetString 静态方法
proc2 += GetString;
4.调用委托
调用委托就是调用一个委托实例方法。
proc1("Hello World");
以下代码演示了如何声明、创建和使用委托。
// 1.声明委托类型
internal delegate void Feedback(Int32 value);
internal class Program
{
private static void Main(string[] args)
{
StaticDelegateDemo();
InstanceDelegateDemo();
ChainDelegateDemo1(new Program());
ChainDelegateDemo2(new Program()); } private static void StaticDelegateDemo()
{
Console.WriteLine("----- Static Delegate Demo -----");
Counter(, , null);
// 3.创建委托实例
Counter(, , new Feedback(Program.FeedbackToConsole));
Counter(, , new Feedback(FeedbackToMsgBox));
Console.WriteLine();
} private static void InstanceDelegateDemo()
{
Console.WriteLine("----- Instance Delegate Demo -----");
Program di = new Program();
// 3.创建委托实例
Counter(, , new Feedback(di.FeedbackToFile)); Console.WriteLine();
} private static void ChainDelegateDemo1(Program di)
{
Console.WriteLine("----- Chain Delegate Demo 1 -----");
// 3.创建委托实例
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(di.FeedbackToFile); Feedback fbChain = null;
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
fbChain = (Feedback)Delegate.Combine(fbChain, fb3);
Counter(, , fbChain); Console.WriteLine();
fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Counter(, , fbChain);
} private static void ChainDelegateDemo2(Program di)
{
Console.WriteLine("----- Chain Delegate Demo 2 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(di.FeedbackToFile); Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;
Counter(, , fbChain); Console.WriteLine();
fbChain -= new Feedback(FeedbackToMsgBox);
Counter(, , fbChain);
} private static void Counter(Int32 from, Int32 to, Feedback fb)
{
for (Int32 val = from; val <= to; val++)
{
// 如果指定了任何回调,就可以调用它
if (fb != null)
// 4.调用委托
fb(val);
}
} // 2.声明签名相同的方法
private static void FeedbackToConsole(Int32 value)
{
Console.WriteLine("Item=" + value);
} // 2.声明签名相同的方法
private static void FeedbackToMsgBox(Int32 value)
{
Console.WriteLine("Item=" + value);
} // 2.声明签名相同的方法
private void FeedbackToFile(Int32 value)
{
StreamWriter sw = new StreamWriter("Status", true);
sw.WriteLine("Item=" + value);
sw.Close();
}
}
委托的简单使用
用委托回调静态方法和实例方法
在上面的代码中,我们可以清楚的看到用委托如何回调静态方法。直接将静态方法绑定到委托的实例上,再通过实例进行调用。
理解counter方法的设计及其工作方式之后,再来看看如何利用委托回调静态方法。
在StaticDelegateDemo方法中第二次调用counter,为第三个参数传递新构造的feedback委托对象。委托对象是方法的包装器(wrapper),使方法能通过包装器来间接回调。本例中,静态方法的完整名称Program.FeedbackToConsole被传给feedback委托类型的构造器,这就是要包装的方法。
注意:FeedbackToConsole方法被定义成Program类型内部的私有方法,但counter方法能调用Program类型的私有方法。这明显没有问题,因为counter和FeedbackToConsole在同一个类型中定义,即使不在同一个类型,也不会出问题,只要feedback委托对象是有具有足够安全性/可访问性的代码创建的,便没有问题。
将方法绑定到委托时,C#和CLR都允许引用类型的协变性和逆变性。协变性是指方法能返回从委托的返回类型派生的一个类型。逆变性是指方法获取的参数可以是委托的参数类型的基类。例如下面的委托:
deleget Object MyCallback(FileStream s);
完全可以构造该委托类型的一个实例,并和具有一下原型的一个方法绑定:
String SomeMethod(Stream s);
在这里,SomeMethod的返回类型(String)派生自委托的返回类型(Object);这种协变性是允许的。SomeMethod的参数类型(Stream)是委托的参数类型(FileStream)的基类;这种逆变性是允许的。
注意,协变性和逆变性只能用于引用类型,不能作用于值类型和void。所以下面示例是错误的:
Int32 SomeMethod(Stream s);//这是错误的
值类型和void之所以不支持协变性和逆变性,是因为它们的存储结构是变化的,而引用类型的存储结构始终是一个指针。
用委托回调实例方法
使用委托回调实例方法,在上面代码中演示已经非常清楚了,就不细说了。
包装实例方法很有用,因为对象内部代码可以访问对象的实例成员。这意味着对象可以维护一些状态,并在回调方法执行期间利用这些信息。
委托揭秘
从表面看,委托似乎很容易使用:用C#的delegate关键字声明,用熟悉的new操作符构造委托实例,用熟悉的方法调用语法来调用回调函数(用引用了委托对象的变量替代方法名)。
然而,实际情况远比前面例子演示的复杂的多。编译器和CLR在幕后做了大量工作来隐藏复杂性。本节重点讲解了编译器和CLR如何协同工作来实现委托。掌握这些知识有助于加深对委托的理解,并学会如何更高效地使用。另外,还要介绍通过委托来实现的一些附加功能。
首先让我们重写审视下面的代码:
internal delegate void Feedback(Int32 value);
看到这行代码,编译器实际会像下面这样定义一个完整的类:
internal class Feedback: System.MulticastDelegate {
// 构造器
public Feedback(Object object, IntPtr method);
// 这个方法和源代码指定的原型一样
public virtual void Invoke(Int32 value);
// 以下方法实现了对回调方法的异步回调
public virtual IAsyncResult BeginInvoke(Int32 value, AsyncCallback callback, Object object);
public virtual void EndInvoke(IAsyncResult result);
}
编译器定义的类有4个方法:一个构造器、Invoke、BeginInvoke和EndInvoke。本节重点解释构造器和Invoke,BeginInvoke和EndInvoke看留到后面讲解。
事实上,可用ILDasm.exe查看生成的程序集,验证编译器真的会自动生成这个类,如图17-1所示:
在这个例子中,编译器定义了一个名为Feedback的类,该类派生自FCL定义的System.MulticastDelegate类型(所有委托类型都派生自System.MulticastDelegate类型)。
提示:System.MulticastDelegate类派生自System.Delegate,后则又派生自System.Object。之所以有两个委托类,是有历史原因的。
从图中可知Feedback的可访问性是private,因为委托在源代码中声明为internal类。如果源代码改成使用public可见性,编译器生成的类也会是public类。要注意,委托类即可嵌套在一个类型中定义,也可以在全局范围中定义。简单地说,由于委托是类,所以凡是能够定义类的地方,都能定义委托。
由于所有委托类型都派生自MulticastDelegate,所以它们继承了MulticastDelegate的字段、属性和方法。在这些成员中,有三个非公共字段是最重要的。
Delegate类定义了两个只读的公共实例属性:Target和Method。给定一个委托对象的引用,可查询这些属性。Target属性返回一个引用,它指向回调方法要操作的对象。简单的说,Target属性返回保存在私有字段_target中的值。如果委托对象包装的是一个静态方法,Target将返回null。Method属性返回一个System.Reflection.MethodInfo对象的引用,该对象标识了回调方法。简单地说,Method属性有一个内部转换机制,能将私有字段_methodPtr中的值转换为一个MethodInfo对象并返回它。
可通过多种方式利用这些属性。例如,可检查委托对象引用是不是一个特定类型中定义的实例方法:
Boolean DelegateRefersToInstanceMethodOfType(MulticastDelegate d ,Type type) {
return ((d.Target != null) && d.Target.GetType() == type);
}
还可以写代码检查回调方法是否有一个特定的名称(比如FeedbackToMsgBox):
Boolean DelegateRefersToInstanceMethodOfName(MulticastDelegate d ,String methodName) {
return (d.Method.Name == methodName);
}
注意,所有委托都有一个构造器,它要获取两个参数:一个是对象引用,另一个是引用回调方法的一个整数。然而,如果仔细看下签名的源代码,会发现传递的是Program.FeedbackToConsole和di.FeedbackToFile这样的值,这似乎不可能通过编译吧?
然而,C#编译器知道要构造的是委托,所以会分析源代码来确定引用的是哪个对象和方法。对象引用被传给构造器的object参数,标识了方法的一个特殊IntPtr值(从MethodDef或MemberRef元数据token获得)被传给构造器的method参数。对于静态方法,会为object参数传递null值。在构造器内部,这两个实参分别保存在_target和_methodPtr私有字段中。除此之外,构造器还将_invocationList字段设为null,对这个字段的讨论推迟到后面。
所以,每个委托对象实际都是一个包装器,其中包装了一个方法和调用该方法时要操作的一个对象。例如,在执行以下两行代码之后:
Feedback fbStatic = new Feedback(Program.FeedbackToConsole);
Feedback fbInstance = new Feedback(new Program.FeedbackToFile());
fbStatic和fbInstance变量将引用两个独立的,初始化好的Feedback委托对象,如图17-2所示。
知道了委托对象如何构造并了解其内部结构之后,在来看看回调方法是如何调用的。为方便讨论,下面重复了Counter方法的定义:
private static void Counter(Int32 from, Int32 to, Feedback fb) {
for (Int32 val = from; val <= to; val++) {
// 如果指定了任何回调,就调用它们
if(fb != null ){
fb(val);
}
}
}
这里的null检查必不可少,因为fb知识可能引用了feedback委托对象的变量,他也可能为null。这段代码看上去像是调用了一个名为fb的函数,并向它传递一个参数(val)。但事实上,这里没有名为fb的函数。再次提醒你注意,因为编译器知道fb是引用了委托对象的变量,所以会生成代码调用该委托对象的Invoke方法。也就是也就是还说,编译器在看到以下代码时:
fb(val);
将生成以下代码,好像源代码本来就是这么写的:
fb.Invoke(val);
为了验证编译器生成的代码来调用委托类型的Invoke方法,可利用ILDasm.exe来检查生成的IL代码:
.method private hidebysig static void Counter(int32 from,int32 'to',class ConsoleTest.Feedback fb) cil managed
{
// 代码大小 41 (0x29)
.maxstack
.locals init ([] int32 val,
[] bool CS$$)
IL_0000: nop
IL_0001: ldarg.
IL_0002: stloc.
IL_0003: br.s IL_001d
IL_0005: nop
IL_0006: ldarg.
IL_0007: ldnull
IL_0008: ceq
IL_000a: stloc.
IL_000b: ldloc.
IL_000c: brtrue.s IL_0018
IL_000e: nop
IL_000f: ldarg.
IL_0010: ldloc.
IL_0011: callvirt instance void ConsoleTest.Feedback::Invoke(int32)
IL_0016: nop
IL_0017: nop
IL_0018: nop
IL_0019: ldloc.
IL_001a: ldc.i4.
IL_001b: add
IL_001c: stloc.
IL_001d: ldloc.
IL_001e: ldarg.
IL_001f: cgt
IL_0021: ldc.i4.
IL_0022: ceq
IL_0024: stloc.
IL_0025: ldloc.
IL_0026: brtrue.s IL_0005
IL_0028: ret
} // end of method Program::Counter
IL代码
其实,完全可以修改Counter方法来显式调用Invoke方法,如下所示:
private static void Counter(Int32 from, Int32 to, Feedback fb) {
for (Int32 val = from; val <= to; val++) {
// 如果指定了任何回调,就调用它们
if(fb != null ){
fb.Invoke(val);
}
}
}
前面说过,编译器是在定义Feedback类时定义Invoke的。所以Invoke被调用时,它使用私有字段_target和_methodPtr在指定对象上调用包装好的回调方法。注意,Invoke方法的签名与委托的签名是匹配的。由于Feedback委托要获取一个Int32参数,并返回void,所以编译器生成的Invoke方法也要获取一个Int32参数,并返回void。
用委托回调多个方法(委托链)
委托本身就已经相当有用了,在加上对委托链的支持,它的用处就更大了!委托链是由委托对象构成的一个集合。利用委托链,可调用集合中的委托所代表的全部方法。为了理解这一点,请参考第一节中的示例代码中的ChainDelegateDemo1方法。在这个方法中,在Console.WriteLine语句之后,我构造了三个委托对象并让变量fb1、fb2和fb3引用每一个对象,如图17-3所示:
指向Feedback委托对象的引用变量fbChain旨在引用委托链,这些对象包装了可以回调的方法。fbChain被初始化为null,表明目前没有回调的方法。使用Delegate类的公共静态方法Combine,可以将一个委托添加到链中:
Feedback fbChain = null;
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
执行以上代码时,Combine方法会视图合并null和fb1。在内部,Combine直接返回fb1中的值,所以fbChain变量现在引用的就是fb1变量引用的那个委托对象。如图17-4所示:
再次调用了Combine方法,在链中添加第二个委托:
fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
在内部,Combine方法发现fbChain已经引用了一个委托对象,所以Combine会构造一个新的委托对象。这个新的委托对象对它的私有字段_target和_methodPtr进行初始化,具体值对目前讨论的来说并不重要。重要的是,_invocationList字段被初始化为引用一个委托对象数组。这个数组的第一个元素(索引为0)被初始化为引用包装了FeedbackToConsole方法的委托。数组的第二个元素(索引为1)被初始化为引用包装了FeedbackToMsgBox方法的委托。最后,fnChain被设为引用新建的委托对象,如图17-5所示:
为了在链中添加第三个委托,再次调用了Combine方法:
fbChain = (Feedback)Delegate.Combine(fbChain, fb3);
同样的,Combine方法会发现fbChain已经引用了一个委托对象,于是又Combine会构造一个新的委托对象。这个新的委托对象对它的私有字段_target和_methodPtr进行初始化,具体值对目前讨论的来说并不重要。重要的是,_invocationList字段被初始化为引用一个委托对象数组。这个数组的第一个元素(索引为0)被初始化为引用包装了FeedbackToConsole方法的委托,数组的第二个元素(索引为1)被初始化为引用包装了FeedbackToMsgBox方法的委托,数组的第三个元素(索引为2)被初始化为引用包装了FeedbackToFile方法的委托。最后,fnChain被设为引用新建的委托对象。注意之前新建的委托以及_invocationList字段引用的数组已经被垃圾回收器回收了。如图17-6所示:
在ChainDelegateDemo1方法中,用于设置委托链的所有代码已经执行完毕,我将fnChain变量交给Counte方法:
Counter(1, 2, fbChain);
Counter方法内部的代码会在Feedback委托对象上隐式调用Invoke方法,这在前面已经讲过了。在fnChain引用的委托上调用Invoke时,该委托发现私有字段_invocationList不为null,所以会执行一个循环来遍历数组中的所有元素,并依次调用每个委托包装的方法。在本例中,首先调用的是FeedbackToConsole,然后是FeedbackToMsgBox,最后是FeedbackToFile。
以伪代码的方式,Feedback的Invoke的基本上是向下面这样实现的:
public void Invoke(Int32 value) {
Delegate[] delegateSet = _invocationList as Delegate[];
if (delegateSet != null) {
foreach(var d in delegateSet)
d(value);// 调用委托
}else{//否则,不是委托链
_methodPtr.Invoke(value);
}
}
注意,还可以使用Delegate公共静态方法Remove从委托链中删除委托,如下所示。
fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Remove方法被调用时,它扫描的第一个实参(本例是fbChain)所引用的那个委托对象内部维护的委托数组(从末尾向索引0扫描)。Remove查找的是其_target和_methodPtr字段与第二个实参(本例是新建的Feedback委托)中的字段匹配的委托。如果找匹配的委托,并且(在删除之后)数组中只剩下一个数据项,就返回那个数据项。如果找到匹配的委托,并且数组中还剩余多个数据项,就新建一个委托对象——其中创建并初始化_invocationList数组将引用原始数组中的所有数据项(删除的数据项除外),并返回对这个新建委托对象的引用。如果从链中删除了仅有的一个元素,Remove会返回null。注意,每次Remove方法调用只能从链中删除一个委托,它不会删除有匹配的_target和_methodPtr字段的所有委托。
前面展示的例子中,委托返回值都是void。但是,完全可以向下面这样定义Feedback委托:
public delegate Int32 Feedback (Int32 value);
如果这样定义,那么该委托的Invoke方法就应该向下面这样(伪代码形式):
public Int32 Invoke(Int32 value) {
Int32 result;
Delegate[] delegateSet = _invocationList as Delegate[];
if (delegateSet != null) {
foreach(var d in delegateSet)
result = d(value);// 调用委托
}else{//否则,不是委托链
result = _methodPtr.Invoke(_target,value);
}
return result;
}
数组中的每个委托被调用时,其返回值被保存到result变量中。循环完成后,result变量只包含调用的最后一个委托的结果(前面的返回值会被丢弃),该值返回给调用Invoke的代码。
C#对委托链的支持
为方便C#开发人员,C#编译器自动为委托类型的实例重载了+=和-=操作符。这些操作符分别调用了Delegate.Combine和Delegate.Remove。使用这些操作符,可简化委托链的构造。
比如下面代码:
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;
取得对委托链调用的更多控制
此时,想必你已经理解了如何创建委托对象,以及如何调用链中的所有对象。链中的所有项都会被调用,因为委托类型的invoke方法包含了对数组中所有项进行遍历的代码。因为Invoke方法中的算法就是遍历,过于简单,显然,这有很大的局限性,除了最后一个返回值,其它所有回调方法的返回值都会被丢弃。但局限并不止于此。如果被调用的委托中有一个抛出一个异常或阻塞相当长的时间,会出现什么情况?由于这个简答的算法是顺序调用链中的每一个委托,所以一个委托对象出现问题,链中后续的所有对象都调用不了。显然,这个算法还不够健壮。
由于这个算法的局限,所以MulticastDelegate类提供了一个实例方法GetInvocationList,用于显式调用链中的每一个委托,同时又可以自定义符合自己需要的任何算法:
public abstract class MulticastDelegate :Delegate {
// 创建一个委托数组,其中每个元素都引用链中的一个委托
public sealed override Delegate[] GetInvocationList();
}
GetInvocationList方法操作从MulticastDelegate派生的对象,返回包含Delegate引用的一个数组,其中每一个引用都指向链中的一个委托对象。在内部,GetInvocationList构造并初始化一个数组,让它的每个元素都引用链中的一个委托,然后返回对该数组的引用。如果_invocationList字段为null,返回的数组就只有一个元素,该元素引用链中唯一的委托,即委托实例本身。
可以很容易地写一个算法来显示调用数组中每个对象,下面是代码演示:
// 定义一个 Light 组件
private sealed class Light
{
// 该方法返回 light 的状态
public String SwitchPosition()
{
return "The light is off";
}
} // 定义一个 Fan(风扇)组件
private sealed class Fan
{
// 该方法返回 fan 的状态
public String Speed()
{
throw new InvalidOperationException("The fan broke due to overheating");
}
} // 定义一个Speaker(扬声器)组件
private sealed class Speaker
{
// 该方法返回 speaker 的状态
public String Volume()
{
return "The volume is loud";
}
} // 定义委托
private delegate String GetStatus(); public static void Main()
{
// 声明一个为null的委托
GetStatus getStatus = null; // 构造三个组件,将它们的状态方法添加到委托链中
getStatus += new GetStatus(new Light().SwitchPosition);
getStatus += new GetStatus(new Fan().Speed);
getStatus += new GetStatus(new Speaker().Volume); // 输出该委托链中,每个组件的状态
Console.WriteLine(GetComponentStatusReport(getStatus));
} // 该方法用户查询几个组件的状态
private static String GetComponentStatusReport(GetStatus status)
{
// 如果委托链为null,则不进行任何操作
if (status == null) return null;
// 用StringBuilder来记录创建的状态报告
StringBuilder report = new StringBuilder();
// 获取委托链,其中的每个数据项都是一个委托
Delegate[] arrayOfDelegates = status.GetInvocationList();
// 遍历数组中的每一个委托
foreach (GetStatus getStatus in arrayOfDelegates)
{
try
{
// 获取一个组件的状态报告,将它添加到StringBuilder中
report.AppendFormat("{0}{1}{1}", getStatus(), Environment.NewLine);
}
catch (InvalidOperationException e)
{
// 在状态报告中生成一条错误记录
Object component = getStatus.Target;
report.AppendFormat(
"Failed to get status from {1}{2}{0} Error: {3}{0}{0}",
Environment.NewLine,
((component == null) ? "" : component.GetType() + "."),
getStatus.Method.Name, e.Message);
}
}
// 返回遍历后的报告
return report.ToString();
}
委托链的更多控制
执行结果为:
The light is off
Failed to get status from ConsoleTest.GetInvocationList+Fan.Speed
Error: The fan broke due to overheating
The volume is loud
委托定义不要太多(泛型委托)
许多年前,.NET Framework刚开始开发时,Microsoft引入委托的概念。开发人员在FCL中添加类时,他们在引入了回调方法的所有定法定义新的委托类型。随着时间的推移,他们定义了太多的委托。事实上,现在仅仅在MSCorLib.dll中,就有接近50个委托类型。比如:
public delegate void TryCode(Object userData);
public delegate void WaitCallback(Object state);
public delegate void TimerCallback(Object state);
...
你发现上面几个委托的共同点了吗?它们其实全是一样的:这些委托类型的变量引用的方法都是获取一个Object,并且返回void。没有理由定义这么多委托类型,定义一个就好了!
现在,.NET Framewoke现在支持泛型,所以实际上只需要几个泛型委托就可以表示获取多达16个参数的方法:
public delegate void Action(); //这不是泛型
public delegate void Action<T>(T obj);
public delegate void Action<T1,T2>(T1 obj1,T2 obj2);
public delegate void Action<T1,T2,T3>(T1 obj1,T2 obj2,T3 obj3);
...
public delegate void Action<T1,...,T16>(T1 obj1,...,T16 obj16);
所以,.NET Framework现在提供17个Action委托,它们从无参数一直到最多16个参数。如果方法需要获取16个意思、上的参数,就必须定义自己的委托类型,但这种情况应该是极其罕见的。除了Action委托,.NET Framewoke还提供了17个Func函数,它们允许回调方法方法返回一个值:
public delegate TResult Func<TResult>();
public delegate TResult Func<T,TResult>(T1 arg);
public delegate TResult Func<T1,T2,TResult>(T1 arg1,T2 arg2);
...
public delegate TResult Func<T1,...,T16,TResult>(T1 arg1,...,T16 arg16);
建议尽量使用这些委托类型,而不是在代码中定义更多的委托类型。这样可以减少系统中的类型数目,同时简化编码。然而,如果需要使用ref或out关键字,以引用的方式传递一个参数,就可能不得不定义自己的委托:
delegate void Bar(ref Int32 z);
使用获取泛型实参和返回值的委托时,可利用逆变和协变,而且建议你总是利用这些功能,因为它们没有副作用,而且是你的委托适用于更多情形。
c#为委托提供的简化语法
许多开发人员认为和委托打交道很麻烦。因为它的语法很奇怪。例如以下代码:
button1.Click += new EventHandle(button1_Click);
其中的button1_Click是一个方法,它看起来像下面这样:
void button1_Click(Object sender, EventArgs e) {
// 按钮单击后要做的事情....
}
第一行代码的思路是向按钮控件登记button1_Click方法的地址,以便在该按钮被单击时,可以调用方法。许多开发人员认为,仅仅为了指定button1_CLick方法的地址,就构造一个EventHandle委托对象,这显得有点不可思议。然而,构造EventHandle委托对象是CLR要求的,因为这个对象提供了一个包装器,可确保(被包装的)方法只能以类型安全的方式调用。这个包装器还支持调用实例方法和委托链。但是很多开发人员不想研究这些细节,更喜欢像下面这样的写代码:
button1_Click += button1_Click;
幸好,C#编译器为开发人员提供了一些用于处理委托的简化方法。后文描述的实际上可归为C#的语法糖(syntactical sugar)。这些简化语法为程序员提供了一种更简答的方式生成clr和其他编程语言处理委托时所必须的il代码。
不需要构造委托对象
如前所示,C#允许指定回调方法的名称,不必构造一个委托对象包装器。例如:
public sealed class AClass
{
private static void CallbackWithoutNewingADelegateObject(){
ThreadPool.QueueUserWorkItem(SomeAsyncTask,);
}
private static void SomeAsyncTask(Object o) {
Console.WriteLine(o);
}
}
这里,ThreadPool类的静态方法QueueUserWorkItem期望接受对一个WaitCallback委托对象的引用,委托对象中包装的是对SomeAsyncTask方法的一个引用。由于C#编译器能够自己进行推断,所以可以省略构造WaitCallback委托对象的代码,使整个代码的可读性更强,也更容易理解。当然,当代码编译时,C#编译器会生成IL代码来构建WaitCallback委托对象——我们只是在语法上得到了简化而已。
不需要定义回调方法(lambda)
在前面的代码中,是将回调方法SomeAsyncTask传给ThreadPool的QueueUserWorkItem方法。C#允许我们以内联的方式写回调方法的代码。不必再另外定义方法写。例如,前面的代码可以重写为下面这样:
public sealed class AClass
{
private static void CallbackWithoutNewingADelegateObject(){
ThreadPool.QueueUserWorkItem(SomeAsyncTask,);
}
private static void SomeAsyncTask(Object o) {
Console.WriteLine(o);
}
}
注意,传给QueueUserWorkItem方法的第一个实参其实是一个lambda表达式。通过C# limbda表达式操作符=>,可以很容易地识别这种表达式。lambda表达式可在编译器预计需要一个委托的地方使用。编译器看到这个lambda表达式之后,会在类中自动创建一个新的私有方法。这个新方法成为匿名函数(anonymous function),因为方法的名称是编译器自动创建的,开发人员一般不知道这个名称。通过ILDasm.exe查看C#编译器将该方法命名为了<CallbackWithoutNewingADelegateObject>b__0,它获取一个Object参数,返回void.
编译器选择的方法名以<符号开头,这是因为在C#中,标识符是不能包含<符号的;这就确保了你不会碰巧定义一个编译器自动选择的名称。顺便说一句,虽然C#禁止标识符包含<符号,但是CLR允许,这也就是为什么编译不会出错的原因了。另外注意,虽然可将方法名作为一个字符串来传递,通过反射来访问方法,但是C#语言规范指出,编译器生成名称的方式是没有任何保证的。例如,每次编译代码,编译器生成的方法都可能是一个不同的名称。
通过ILDasm.exe,我们还注意到C#编译器向这个方法应用了一个名为System.Runtime.CompilerServices.CompilerGeneratedAttribute的attribute,指出方法是编译器生成的,而非开发人员定义的。=>操作符右侧的代码被放入这个编译器生成的方法中。
注意:C#2.0面世时,它引入了一个称为匿名方法的功能。和C#3.0引入的lambda表达式相似,匿名方法描述的也是用于创建匿名函数的一个语法。C#语言规范建议开发人员使用新的lambda表达式,而不要使用旧的匿名方法语法,因为lambda表达式语法更简洁,代码更容易写、读和维护。
注意:写lambda表达式时没有办法向编译器生成的方法引用定制特性。此外,不能向方法应用任何方法修饰符(比如unsafe)。但这一版不会有什么问题,因为编译器生成的匿名函数总是私有方法,而且要么是静态的,要么是非静态的,具体取决于方法是否访问了任何实例成员,所以,没必要向方法应用public之类的修饰符。
最后,如果卸载前面的代码编译,c#编译器会将这些代码改写为下面:
lambda表达式必须匹配waitCallback委托:获取一个object并返回void。但在指定参数名称时,我简单地将obj放在=>操作符左侧。在=>操作符右侧,返回void。然而,如果在这里放一个返回值不为void的表达式,编译器生成的代码会直接忽略返回值,因为编译器生成的方法必须用void返回类型来满足waitCallback委托。
另外还要注意,匿名函数被标记为private,禁止非类型内定义的代码访问(尽管反射能揭示出方法确实存在)。另外,匿名函数被标记为static,因为代码没有访问任何实例成员(也不能访问,因为CallbackWithoutNewingADelegateObject本身是静态方法)。如果CallbackWithoutNewingADelegateObject方法不是静态的,匿名函数的代码就可以包含对实例成员的引用。不包含实例成员引用,编译器仍会生成静态匿名函数,因为它的效率比实例方法搞。之所以更高效,是因为不需要额外的this参数,但是,如果匿名函数的代码确实引用了实例成员,编译器就会生成非静态匿名函数。
=>操作符左侧指定传给lambda表达式的参数的名称。下例总结了一些规则:
//如果不需要返回值 使用action
Action a = () => Console.WriteLine("");
//如果委托不获取任何参数,就是用()
Func<string> f = () => "jeff";
//如果委托获取1个或更多参数,可显示指定类型
Func<int,string> f2=(int n)=> n.ToString();
//如果委托获取1个或更多参数,编译器可推断类型,而且可以省略括号
Func<int,string> f3=n=> n.ToString();
//如果委托有ref或out参数,必须显式指定ref、out和类型
Bar b = (out int n) => n = ;
如果主题由两个或多个语句构成,必须用大括号将语句封闭。在用了大括号的情况下,如果委托预期返回值,还必须在主体中添加return语句。
提示:lambda表达式的主要优势在于,它从你的源代码中移除了一个"间接层"。或者说避免了迂回,正常情况下,必须写一个单独的方法,命名该该按方法,再在需要委托的地方传递这个方法。方法名提供了引用代码主题的一种方式,如果要在多个地方引用同一个代码主题,单独写一个方法并命名确实是理想方案。但如果只需要在代码中引用一次,那么lambda表达式允许直接内联那些代码,不必为它分配名称,提高了编程效率。
局部变量不需要手动包装到类中即可传给回调方法
前面展示了回调代码如何引用类中定义的其他成员。但有时候,还希望回调代码引用存在于方法中的局部参数或变量。下面有个有趣的例子:
internal sealed class AClass2
{
internal static void UsingLocalVariablesInTheCallbackCode(Int32 numToDo)
{
// 一些局部变量
Int32[] squares = new Int32[numToDo];
AutoResetEvent done = new AutoResetEvent(false); // 在其它线程上执行一系列任务
for (Int32 n = ; n < squares.Length; n++)
{
ThreadPool.QueueUserWorkItem(
delegate(Object obj)
{
Int32 num = (Int32)obj;
// 耗时任务
squares[num] = num * num;
// 如果是最后一个任务,则让主线程继续执行
if (Interlocked.Decrement(ref numToDo) == )
done.Set();
}, n);
}
// 等待其他所有线程执行完毕
done.WaitOne();
// 显示任务
for (Int32 n = ; n < squares.Length; n++)
Console.WriteLine("Index {0}, Square={1}", n, squares[n]);
}
}
这个例子实际演示了C#如何简单的实现一个非常复杂的任务。以上方法定义了一个参数numToDo和两个局部变量aquares和done,而且lambda表达式的主体引用了这些变量。现在,想象以下lambda表达式主体中的代码在一个单独的方法中(事实上,也的确如此)。变量的值如何传递给这个单独的方法呢?唯一的方法就是定义一个新的辅助类,这个类要为我们打算传给回调代码的每一个值都定义一个字段。此外,回调代码还必须定义这个辅助类中的一个实例方法。然后,UsingLocalVariablesInTheCallbackCode方法必须构造辅助类的一个实例,用方法定义的局部变量的值来初始化这个实例中的字段。然后,构造绑定到辅助对象/实例方法的委托对象。
注意:当lambda表达式造成编译器生成一个类时,而且参数/局部变量被转变成该类的字段后,变量引用的对象的生存周期被延长了。正常情况下,在方法中最后一次使用参数/局部变量之后,这个参数/局部变量就会"离开操作作用域",结束其生命周期。但是,将变量转变成另一个类的字段后,只要包含字段的那个对象不"死",字段引用的对象也不会"死"。这在大多数应用程序中不是大的问题,但有时需要注意一下。
提示:毫无疑问,C#的lambda表达式功能很容易被开发人员滥用。我开始使用lambda表达式时,花了一些时间来熟悉它。毕竟,你在一个方法中写的代码实际不再这个方法中,除了有违直觉,还使调试和但不执行变得更有挑战性。
我给自己设定了一个规则:如果需要在回调放方法中包含3行以上代码,就不适用lambda表达式。相反的,我会手动写一个方法,并为其分配一个自己的名称。但如果使用得当,匿名方法趋势能显著提升开发人员效率和代码的可维护性。在以下代码中,使用Lambda表达式感觉非常自然,没有它们,这样的代码会很难写、读以及维护。
//创建并输出和一个string数组
string[] names = {"green", "grant", "tom"};
//只获取含有小写字母’a’的名字
char charToFind = 'a';
names = Array.FindAll(names, c => c.IndexOf(charToFind) >= );
//将每个字符串的字符转换为大写
names = Array.ConvertAll(names, c => c.ToUpper());
Array.ForEach(names,Console.WriteLine);
委托和反射
到本节为止,使用委托都要求开发人员事先知道回调方法的原型。例如,如果fb是引用了一个Feedback委托的变量(第一节第二个示例程序),那么为了调用这个委托,代码应该这样写:
fb(item); //item为Int32类型
可以看出,在编码的时候,开发人员必须知道回调方法需要多少个参数,以及这些参数的具体类型。还好,开发人员几乎总是知道这些信息,所以像前面那样写代码是没有问题的。
不过在个别情况下,开发人员在编译时并不知道这些信息。在"事件"讨论EventSet类型时,曾经展示过一个例子。这个例子用一个字典来维护一组不同的委托类型。在运行时,为了引发事件,要在字典中查找并调用委托。但在编译时,我们不能准确地知道要调用哪个委托,哪些参数必须传给委托的回调方法。
幸好System.Delegate提供了一个CreateDelegate方法。在编译时不知道委托的这些必要信息时,可利用这个方法来创建并调用一个委托。以下是MethodInfo未该方法定义的重载:
public abstract class MethodInfo:MethodBase
{
// 构造保证了一个静态方法的委托
public virtual Delegate CreateDelegate(Type delegateType)
{
return null;
}
//构造保证了一个实例方法的委托;target引用this实参
public virtual Delegate CreateDelegate(Type delegateType,Object target)
{
return null;
}
}
创建好委托后,利用delegate的dynamicInvoke方法调用它,如下所示
// 调用委托并传递参数
public Object DynamicInvoke(params Object[] args);
使用反射API,首先必须获取引用了回调方法的一个MethodInfo对象。然后,调用CreateDelegate方法来构造由第一个参数delegateType所标识的Delegate派生类型的对象。如果委托包装了实例方法,还要向CreateDelegate传递一个target参数,指定作为this参数传给实例方法的对象。
所有CreateDelegate方法构造的都是从Delegate派生的一个类型新对象,具体类型由第一个参数type来标识。MethodInfo参数指出应该回调的方法;要用反射来获取这个值。如果希望委托包装一个实例方法,还要向CreateDelegate传递一个firstArgument参数,指定应作为this参数(第一个参数)传给实例方法的对象。最后,如果委托不能绑定到method参数指定的方法,CreateDelegate通常应该抛出一个异常。
System.Delegate的DynamicInvoke方法允许调用委托对象的回调方法,传递一组在运行时确定的参数。调用DynamicInvoke时,它会在内部保证传递的参数与回调方法期望的参数兼容。如果兼容,就调用回调方法;否则抛出一个异常。DynamicInvoke返回回调方法所返回的对象。
下面代码展示了如何使用CreateDelegate和DynamicInvoke方法:
// 下面是一些不同的委托定义
private delegate Object TwoInt32s(Int32 n1, Int32 n2);
private delegate Object OneString(String s1);
internal static class DelegateReflection
{
public static void Go(String[] args)
{
if (args.Length < )
{
String fileName = Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().Location); String usage =
@"Usage:" +
"{0}{1} delType methodName [Arg1] [Arg2]" +
"{0} where delType must be TwoInt32s or OneString" +
"{0} if delType is TwoInt32s, methodName must be Add or Subtract" +
"{0} if delType is OneString, methodName must be NumChars or Reverse" +
"{0}" +
"{0}Examples:" +
"{0} {1} TwoInt32s Add 123 321" +
"{0} {1} TwoInt32s Subtract 123 321" +
"{0} {1} OneString NumChars \"Hello there\"" +
"{0} {1} OneString Reverse \"Hello there\"";
Console.WriteLine(usage, Environment.NewLine, fileName);
return;
} // 将delType参数转换为一个委托类型
Type delType = Type.GetType(args[]);
if (delType == null)
{
Console.WriteLine("Invalid delType argument: " + args[]);
return;
} Delegate d;
try
{
// 将Arg1参数转换为一个方法
MethodInfo mi = typeof(Program).GetMethod(args[], BindingFlags.NonPublic | BindingFlags.Static); // 创建包装了静态方法的一个委托对象
d = Delegate.CreateDelegate(delType, mi);
}
catch (ArgumentException)
{
Console.WriteLine("Invalid methodName argument: " + args[]);
return;
} // 创建一个数组,其中只包含要通过委托对象传给方法的参数
Object[] callbackArgs = new Object[args.Length - ];
if (d.GetType() == typeof(TwoInt32s))
{
try
{
// 将String类型的参数转换为Int32类型的参数
for (Int32 a = ; a < args.Length; a++)
callbackArgs[a - ] = Int32.Parse(args[a]);
}
catch (FormatException)
{
Console.WriteLine("Parameters must be integers.");
return;
}
}
if (d.GetType() == typeof(OneString))
{
// 只复制String参数
Array.Copy(args, , callbackArgs, , callbackArgs.Length);
}
try
{
// 调用委托并显示结果
Object result = d.DynamicInvoke(callbackArgs);
Console.WriteLine("Result = " + result);
}
catch (TargetParameterCountException)
{
Console.WriteLine("Incorrect number of parameters specified.");
}
}
// 这个回调方法获取2个Int32类型的参数
private static Object Add(Int32 n1, Int32 n2)
{
return n1 + n2;
}
// 这个回调方法获取2个Int32类型的参数
private static Object Subtract(Int32 n1, Int32 n2)
{
return n1 - n2;
}
// 这个回调方法获取1个String类型的参数
private static Object NumChars(String s1)
{
return s1.Length;
}
// 这个回调方法获取1个String类型的参数
private static Object Reverse(String s1)
{
Char[] chars = s1.ToCharArray();
Array.Reverse(chars);
return new String(chars);
}
}
反射使用委托
运行结果
Usage:
DelegateStudy2 delType methodName [Arg1] [Arg2]
where delType must be TwoInt32s or OneString
if delType is TwoInt32s, methodName must be Add or Subtract
if delType is OneString, methodName must be NumChars or Reverse Examples:
DelegateStudy2 TwoInt32s Add
DelegateStudy2 TwoInt32s Subtract
DelegateStudy2 OneString NumChars "Hello there"
DelegateStudy2 OneString Reverse "Hello there"
请按任意键继续. . .
重温CLR(十二) 委托的更多相关文章
- 《Inside C#》笔记(十二) 委托与事件
C#的委托与C++的函数指针类似,但委托是类型安全的,意味着指针始终会指向有效的函数.委托的使用主要有两种:回调和事件. 一 将委托作为回调函数 在需要给一个函数传递一个函数指针,随后通过函数指针调用 ...
- 重温CLR(二)生成、部署以及程序集
将类型生成到模块中 class Program { static void Main(string[] args) { Console.WriteLine("Hi"); } } 该 ...
- java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)
java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessCo ...
- 桥接模式 桥梁模式 bridge 结构型 设计模式(十二)
桥接模式Bridge Bridge 意为桥梁,桥接模式的作用就像桥梁一样,用于把两件事物连接起来 意图 将抽象部分与他的实现部分进行分离,使得他们都可以独立的发展. 意图解析 依赖倒置原 ...
- JAVA基础知识总结:一到二十二全部总结
>一: 一.软件开发的常识 1.什么是软件? 一系列按照特定顺序组织起来的计算机数据或者指令 常见的软件: 系统软件:Windows\Mac OS \Linux 应用软件:QQ,一系列的播放器( ...
- 「十二省联考 2019」皮配——dp
题目 [题目描述] #### 题目背景一年一度的综艺节目<中国好码农>又开始了.本季度,好码农由 Yazid.Zayid.小 R.大 R 四位梦想导师坐镇,他们都将组建自己的梦想战队,并率 ...
- 利用WPF建立自己的3d gis软件(非axhost方式)(十二)SDK中的导航系统
原文:利用WPF建立自己的3d gis软件(非axhost方式)(十二)SDK中的导航系统 先下载SDK:https://pan.baidu.com/s/1M9kBS6ouUwLfrt0zV0bPew ...
- 201871010123-吴丽丽《面向对象程序设计(Java)》第十二周学习总结
201871010123-吴丽丽<面向对象程序设计(Java)>第十二周学习总结 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ ...
- 201871010106-丁宣元 《面向对象程序设计(java)》第十二周学习总结
201871010106-丁宣元 <面向对象程序设计(java)>第十二周学习总结 正文开头: 项目 内容 这个作业属于哪个课程 https://home.cnblogs.com/u/nw ...
- 201271050130-滕江南-《面向对象程序设计(java)》第十二周学习总结
201271050130-滕江南-<面向对象程序设计(java)>第十二周学习总结 项 目 内 容 这个作业属于哪个课程 https://www.cnblogs.co ...
随机推荐
- Mysql修改表结构详解
添加字段: alter table `user_movement_log`Add column GatewayId int not null default 0 AFTER `Regionid` (在 ...
- CTR的贝叶斯平滑
参考论文: Click-Through Rate Estimation for Rare Events in Online Advertising 参考的博客: 1.https://jiayi797. ...
- netty-socketio整合springboot消息推送
netty-socketio整合springboot消息推送 1.netty-socketio消息推送 1)在项目中常常涉及到消息推送的情况,消息推送要求的实时性,使用传统的方式已经不能满足需求了: ...
- dom&bom的起源,方法,内容,应用
Document Object Model的历史可以追溯至1990年代后期微软与Netscape的"浏览器大战"(browser wars),双方为了在JavaScript与JSc ...
- 基于vue-cli,测试非父子传值时,碰到 keep-alive的神奇
非父子传值测试 一直都很好奇非父子传值到底如何,结果入坑许久才爬出来,才知道在脚手架里测试就是坑. 问题: 测试非父子传值时,由于组件之间是通过路由进行跳转,值传过去又被刷掉 思路: 因为路由跳转,相 ...
- js排序算法04——归并排序
归并排序是一种分治算法.思想是把原数组切分成较小的数组,直到每个小数组只有一个位置,再将小数组归并成较大的数组,直到最后有一个完整有序的大数组. js实现如下: function mergeSort( ...
- SQL 添加字段
制定添加在那个字段后面 ALTER TABLE `szq`.`org_sales_daily` ADD COLUMN `trade_id_onl_count` int(11) NOT NULL DEF ...
- C# 常用字符串处理办法
再基础的东西不常用的话就得记下来...不然就忘记了. C#字符串中特殊字符的转义 一个是双引号",另一个就是转义符\ 对于同样一个字符串:地址:"C:\Users\E.txt&qu ...
- KindEditor富文本编辑框和BeautifulSoup的基本使用
KindEditor富文本编辑框 1.进入官网 2.下载 官网下载:http://kindeditor.net/down.php 本地下载:http://files.cnblogs.com/files ...
- 一条命令修改Linux密码
方法一.直接使用passwd命令 /bin/echo newpass|/usr/bin/passwd --stdin username *注:该方式只适用于红帽系操作系统,比如centos,redha ...