作者:浪子花梦,一个有趣的程序员 ~
此文详细的讲解委托的各方面技术,文章可能有点长,希望你能看完 ^ _ ^
文章目录
初识委托(示例代码讲解)
开始讲解之前,让我们看看什么是回调函数,下面是我在度娘上面Copy的一段话:
当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个
被传入
的、后又被调用
的函数就称为回调函数
(callback function)。
回调函数是一种非常有用的编程机制,.net 平台通过 委托 来提供回调函数机制。
委托有许多有用的功能,比如下面几种做些:
- 委托确保回调方法是类型安全的(CLR 最重要的目标之一)
- 顺序调用多个方法
- 支持调用静态方法、实例方法
为了使我们快速的理解委托,让我们先来看看如何的使用它。下面的代码演示了我们如何的声明委托、创建委托和使用委托:
// 定义一个委托类型,接收 Int32,返回 void
internal delegate void Feedback(Int32 value);
class Program
{
static void Main(string[] args)
{
StaticDelegateDemo(); // 静态方法委托调用示例
InstanceDelegateDemo(); // 实例方法委托调用示例
}
private static void StaticDelegateDemo()
{
Console.WriteLine("----- Static Delegate Demo -----");
Counter(1, 3, null);
// 回调一个静态方法
Counter(1, 3, new Feedback(Program.FeedbackToConsole));
Console.WriteLine();
}
private static void InstanceDelegateDemo()
{
Console.WriteLine("----- Static Instance Demo -----");
Program p = new Program();
// 回调一个实例方法
Counter(1, 3, new Feedback(p.FeedbackToFile));
Console.WriteLine();
}
private static void Counter(Int32 from, Int32 to, Feedback fb)
{
for (Int32 val = from; val <= to; val++)
{
// 判断是否有回调函数存在
if (fb != null)
{
fb(val); // == fb.Invoke(val)
}
}
}
private static void FeedbackToConsole(Int32 value)
{
Console.WriteLine("Item = " + value);
}
private void FeedbackToFile(Int32 value)
{
// 向一个文件中写入数据
using (StreamWriter sw = new StreamWriter("Status.txt", true))
{
sw.WriteLine("Item = " + value);
}
}
}
上面的代码中,我们演示了委托的最常用的方法,其中的委托定义如下所示:
// 定义一个委托类型,接收 Int32,返回 void
internal delegate void Feedback(Int32 value);
它的回调方法接收一个 Int32类型的参数,并返回 void型 .
其中有两处地方进行了委托的创建,如下所示:
// 回调一个静态方法
Counter(1, 3,new Feedback(Program.FeedbackToConsole)
);
// 回调一个实例方法
Counter(1, 3,new Feedback(p.FeedbackToFile)
);
红色部分就是创建了委托的对象,并指定了一个回调方法,他们分别是静态方法,实例方法 . . .
委托的调用方法如下所示:
首先,我们判断委托对象是否有回调函数的存在,如果有的话,我们直接利用上面这种方式进行调用回调函数,也就是用那本身的样子进行调用:
fb.Invoke(val);
其中 Main主函数中第一个方法的结果如下所示:
----- Static Delegate Demo -----
Item = 1
Item = 2
Item = 3
第二个方法是在一个文件中查看,结果如下所示:
回调方法的逆变与协变
将方法绑定到委托时,C# 和 CLR 允许引用类型的逆变与协变性,规则如下所示:
- 逆变性: 方法获取的参数可以是委托参数类型的基类
- 协变性: 方法返回的类型可以是委托返回类型的派生类
示例代码如下所示:
// 定义一个委托类型,接收 Int32,返回 void
internal delegate Object Feedback(StreamWriter stm);
class Program
{
static void Main(string[] args)
{
Feedback fb = new Feedback(SayHello);
String str = fb.Invoke(null).ToString();
Console.WriteLine(str);
}
// TextWriter是StreamWriter 的基类 String是Object的派生类
private static String SayHello(TextWriter sw)
{
using (sw = new StreamWriter("huameng.txt", false))
{
sw.WriteLine("langzihuameng");
}
return "数据存储成功!";
}
}
文件中的结果如下所示:
委托深层探索
我们学习一门语言,不只是简单的会用就行了,我们还需要了解语言背后发生的一些事,下面我们就来看看编译器和CLR在委托背后做的一些事吧 . . .
首先,让我们看看下面这一句代码:
internal delegate void Feedback(Int32 value);
表面上看它只是一行简单的代码,但编译器是定义了一个完整的类,我们打开 ILDASM工具查看这个类如下所示,里面有一个构造器、Invoke、BeginInvoke、EndInvoke:
此文章讲解 构造方法 和 Invoke,另外两个方法以后再说(主要书上没讲,^ _ ^,异步操作 . . .)
我们从上面的反编译工具可以看出来,这个类是从 MulticastDelegate
这个类派生而来,而这个类有三个非常重要的非公共字段:
- _target:回调方法的对象,如果是静态方法,则字段为 null
- _methodPtr:标识回调方法
- _invocationList:构造委托链时用它引用一个数组(下节所讲)
下面我们来看看委托对象在创建时,这些字段的值发生了什么变化,如下所示创建两个委托对象:
Feedback fbStatic = new Feedback(Program.FeedbackToConsole);
Feedback fbInstance = new Feedback(new Program().FeedbackToFile);
一个是以静态方法构造,另一个是以实例方法进行构造,我们来看看字段的值发生了什么:
当委托对象被构造时,他的内部结构就是这样变化 . . .
–
我们再来看看 Invoke方法,之前调用委托方法的方式是下面这样的:
private static void Counter(Int32 from, Int32 to, Feedback fb)
{
for (Int32 val = from; val <= to; val++)
{
// 判断是否有回调函数存在
if (fb != null)
{
fb(val);
}
}
}
其中 fb(val); 的本身是一个语法糖,他等价于 fb.Invoke(val); 这一行代码 . . .
下面我们再讲讲委托其它方面的知识 . . .
委托链详解
文章开头我们说过委托的几个好处之一:
顺序调用多个方法
委托链
就是委托对象的集合
,我们先来看看委托对象的集合方式是如何操作的,代码如下所示:
internal delegate void Feedback(Int32 value);
class Program
{
static void Main(string[] args)
{
ChainDelegateDemo1(new Program());
ChainDelegateDemo2(new Program());
}
private static void ChainDelegateDemo1(Program p)
{
Console.WriteLine("------- 委托链演示 1 -------");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
// 从委托链中 删除一个回调方法
//fbChain = (Feedback)Delegate.Remove(fbChain, fb1);
Counter(1, 2, fbChain);
}
private static void ChainDelegateDemo2(Program p)
{
Console.WriteLine("------- 委托链演示 2 -------");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(p.FeedbackToFile);
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
// 从委托链中 删除一个回调方法
//fbChain -= fb2;
Counter(1, 2, fbChain);
}
private static void Counter(Int32 from, Int32 to, Feedback fb)
{
for (Int32 val = from; val <= to; val++)
{
// 判断是否有回调函数存在
if (fb != null)
{
fb(val); // == fb.Invoke(val)
}
}
}
private static void FeedbackToConsole(Int32 value)
{
Console.WriteLine("Item = " + value);
}
private void FeedbackToFile(Int32 value)
{
// 向一个文件中写入数据
using (StreamWriter sw = new StreamWriter("Status.txt", true))
{
sw.WriteLine("Item = " + value);
}
}
}
其中的ChainDelegateDemo1 和 ChainDelegateDemo2 方法是我们新定义的,它们用来演示委托链是怎么构建的,这两个方法没有什么区别,只是第二种方法更加简单了而已,我们将在下下节的语法糖中讲解 . . .
这两个方法中的 fb1 与 fb2创建之后,它们在底层中代表的数据如下所示:
以 ChainDelegateDemo1 方法为例,当我们将 fbChain(此时为 null
) 与 fb1 中的委托对象链接在一起之后,关系是如下这样的(就像是 fbChain 指向了 fb1一样):
当我们再将 fbChain 与 fb2 中的委托对象链接在一起之后,关系是如下这样的:
此时委托链已经形成, fbChain 已经指向了两个委托对象,它们分别是 fb1 与 fb2 . . .
–
对委托链的控制
当我们使用 Invoke 访问这个委托链时,有着许多的局限性,那么这个局限性到底是什么呢?
比如说这些回调方法都是有一个返回值的,但是访问委托链时,最终结果只能返回最后一个委托对象的返回值,而其它的返回值我们就无法的掌控了,还有一个问题,当某一个委托对象出问题之后,比如说异常、阻塞等,后面的所以委托对象都将无法访问 . . .
.
还好, MulticastDelegate 类为我们提供了一个实例方法:GetInvocationList
,我们可以用它来显式的调用链中的每一个委托对象,可以对它们进行任何有意义的操作,下面我们就来看看在实例中,这个方法是如何的使用吧 . . .
首先,我们先定义如下的几个类,每个类中都有一个方法,这些方法用作回调方法测试:
// 电话
internal sealed class Phone
{
public String SayHello()
{
return "I'm is Phone";
}
}
// 苹果
internal sealed class Apple
{
// 这个苹果是坏的(引发异常,用于测试委托链)
public String SayHello()
{
throw new InvalidOperationException("This apple is bad");
}
}
internal sealed class Girl
{
public String SayHello()
{
return "My Name Is XiaoMei!";
}
}
当我们像下面这样调用委托链的时候,会引发异常:
ChainDemo chainDemo = null;
chainDemo += new ChainDemo(new Phone().SayHello);
chainDemo += new ChainDemo(new Apple().SayHello); // 会引发异常
chainDemo += new ChainDemo(new Girl().SayHello);
chainDemo.Invoke(); // 访问所有的委托对象
效果如下所示:
因为委托链中的某个委托对象出问题了!在这个示例中我们很明显的知道是谁,但我们不知道的情况下呢?这种结果岂不是很糟糕? 所以我们使用 GetInvocationList
方法就很好的解决了这个问题,使用情况如下所示:
ChainDemo chainDemo = null;
chainDemo += new ChainDemo(new Phone().SayHello);
chainDemo += new ChainDemo(new Apple().SayHello); // 会引发异常
chainDemo += new ChainDemo(new Girl().SayHello);
Delegate[] arrDelegates = chainDemo.GetInvocationList();
foreach (ChainDemo p in arrDelegates)
{
try
{
Console.WriteLine(p.Invoke()); // 如果没有问题,就输出结果
Console.WriteLine();
}
catch(InvalidOperationException e)
{
Console.WriteLine();
Console.WriteLine(p.Target.GetType().Name); // 获取委托中的 _object
Console.WriteLine(p.Method.Name); // 获取委托回调方法名称
Console.WriteLine(e.Message); // 异常消息信息
Console.WriteLine();
}
}
其中我们在访问委托链的时候,加了一个异常捕捉,这样我们就可以把委托链给访问完了,最终的结果如下所示:
我们不仅处理了有异常的情况,而且还输出每个委托对象的返回值 . . .
委托的讲解到此为止,下面我们在讲解其它方法的知识吧 . . .
泛型委托
泛型委托的定义和泛型方法、泛型类、泛型接口的定义方式是差不多,如下所示,我们演示了一个泛型委托:
internal delegate void Huameng<T>(T value);
class Program
{
static void Main(string[] args)
{
Huameng<Int32> huameng = null;
huameng += new Huameng<Int32>(SayHello);
huameng.Invoke(250);
Huameng<String> Langzi = null;
Langzi += new Huameng<String>(SayHello);
Langzi.Invoke("250");
}
private static void SayHello<T>(T value)
{
if (value is Int32)
{
Console.WriteLine("I'm is Int32!");
}
else if (value is String)
{
Console.WriteLine("I'm is String");
}
}
}
由于程序比较简单,我就不讲解了,结果输出如下所示:
注意的几点知识:
1)委托类型是可以定义可变参数的,像如下方式即可:
internal delegate void Huameng<T>(T value, params Int32[] arr);
2).net(.NET Framework)提供了两种可以直接使用的泛型委托类型:
Action
:不具有返回值Func
:具有返回值
这两种委托类型的使用方式如下所示:
static void Main(string[] args)
{
Action<Int32, Int32, Int32> action = new Action<Int32, Int32, Int32>(SayHello);
action.Invoke(2, 5, 0);
Func<String, Int32, String> func = new Func<string, int, string>(SayName);
String str = func("langzihuameng", 20);
Console.WriteLine(str);
}
private static String SayName(String str, Int32 num)
{
return str + " " + num.ToString();
}
private static void SayHello<T>(T value, T t1, T t2)
{
Console.WriteLine(value.ToString());
Console.WriteLine(t1.ToString());
Console.WriteLine(t2.ToString());
}
结果如下所示:
如果我们想给委托传递带引用的参数,上面这两个类型是不可以用的,我们只能使用自己定义的委托类型,比如下面这样的:
internal delegate void DemoArgs(ref Int32 value);
3)泛型实参
并有返回值
的委托支持逆变和协变
,例如下面这种情况:
这两行代码是不是有点眼熟呢? 它在讲解泛型一文有讲到,有兴趣的小伙伴可以看看我其它的 C#文章,而且这是个逆变与协变在上面也讲到过 . . .
使用委托的几种语法糖
下面我将演示几种C#提供的针对委托的简化语法,让你感受一下高级语言的强大 . . .
1)不需要构造委托对象,直接传入回调函数
我们看看下面的代码,为了让主线程中的 Hello,World!显示,我让程序睡眠 1 秒:
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(SayHello);
Thread.Sleep(1000);
Console.WriteLine("Hello, World!");
}
private static void SayHello(Object obj)
{
Console.WriteLine("Hello, World!");
}
QueueUserWorkItem 方法接收一个 WaitCallback 的委托对象,如下所示:
但我们的程序之中,可以允许直接传递回调方法,而不需要定义委托对象,CLR 会为我们自己声明对象 . . .
程序的结果如下所示:
.
2)lambda 表达式
lambda 表达式的出现,意味着我们将可以随定义匿名的回调方法,使用情况如下所示:
这样的写法是非常的方便的,但这种方式会使我们经常的滥用,我们建议 lambda表达式中的代码句不超过三行,如果超过三行,则自己定义一个方法,用于回调 . . .
C# 编译器会为 lambda表达式自动转换成为一个方法,方法名我们不得而知 . . .
结果如下所示:
下面我们来演示一下 lambda表达式的常见用法,代码如下所示:
第一个 Func类型,我们可以自动推断参数的类型,将lambda表达式的内容只有一条语句行,它会自动返回,另外要想将参数设置 ref 或者 out,我们必须使用自定义的委托类型 . . .
结果如下所示:
3)随意的调用局部变量
委托方法中还支持引用方法中的局部参数或者是变量,像下面这样,我们在 lambda表达式中使用方法中的局部变量:count,示例如下所示:
在引用局部变量时,C# 实际上是隐式的定义了一个类,我们打开 ildasm工具查看如下:
我们需要知识 C# 编译器 和 CLR 为我们做了什么事情,这样我们才能更好的成为 .net 大师 . . .
委托和反射(控制台命令演示)
如果我们在程序之中,不知道回调方法需要多少个参数,以及参数的具体类型,那么我们如何的使用哪个委托类型,这种选择就受到了限制,还好,C# 为我们提供了两个方法:
- CreateDelegate
- DynamicInvoke
其中,CreateDelegate 方法允许在编译时不知道委托的所有必要信息的前提下创建委托;
DynamicInvoke 方法用于调用委托对象的回调方法 . . .
下面我们在实例中来学习一下委托与反射的关系(一个完整的代码):
using System;
using System.Linq;
using System.Reflection;
// 两个不同类型的委托
internal delegate Object TwoInt32s(Int32 n1, Int32 n2);
internal delegate Object OneString(String s1);
class Program
{
static void Main(string[] args)
{
// 如果命令行参数 < 2,则作出提示
if (args.Length < 2)
{
String usage =
@"{0}Usage: " +
"{0} delType methodName [Arg1] [Arg2]" +
"{0} where delType is TwoInt32s, methodName must be Add of 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\"{1}";
Console.WriteLine(usage, Environment.NewLine, Environment.NewLine);
return;
}
// 将 args[0] 转化为委托类型
Type delType = Type.GetType(args[0]);
if (delType == null)
{
Console.WriteLine("Invalid delType argument: " + args[0]);
return;
}
Delegate d;
try
{
// 将 args[1] 转化为方法
MethodInfo mi = typeof(Program).GetTypeInfo().GetDeclaredMethod(args[1]);
// 从mi这个方法创建 delType类型的委托
d = mi.CreateDelegate(delType);
}
catch (ArgumentException)
{
Console.WriteLine("Invalid methodName argument: " + args[1]);
return;
}
// 存放传给方法的参数
Object[] callbackArgs = new Object[args.Length - 2];
// 如果是 TwoInt32s的委托 执行的操作
if(d.GetType() == typeof(TwoInt32s))
{
try
{
for (Int32 i = 2; i < args.Length; i++)
{
// String --> Int32
callbackArgs[i - 2] = Int32.Parse(args[i]);
}
}
catch (FormatException)
{
// 必须是 Int32型的数据
Console.WriteLine("Parameters must be integers.");
return;
}
}
// 如果是 OneString的委托 执行的操作
if (d.GetType() == typeof(OneString))
{
// 复制 String
Array.Copy(args, 2, callbackArgs, 0, callbackArgs.Length);
}
// 显示结果
try
{
// DynamicInvoke 执行委托对象对应的回调方法
Object result = d.DynamicInvoke(callbackArgs);
Console.WriteLine("Result = " + result);
}
catch (TargetParameterCountException)
{
// 对应的方法参数不正确
Console.WriteLine("Incorrect number of parameters specified.");
return;
}
}
// 两个参数
private static Object Add(Int32 n1, Int32 n2)
{
return n1 + n2;
}
private static Object Subtract(Int32 n1, Int32 n2)
{
return n1 - n2;
}
// 一个参数
private static Object NumChars(String s1)
{
return s1.Length;
}
private static Object Reverse(String s1)
{
return new String(s1.Reverse().ToArray());
}
}
我们使用控制台来使用这个程序,如下所示:
1)当我们随便输出点东西时,它会教给你正确的用法:
2)当我们输出正确的用法之后,如下所示:
控制台的命令挺好玩的,大家可以自己试一试 . . .
.
.
.