PS:在网上看到的对委托讲解比较详细的一篇文章,主要供个人收藏,如果只想学习怎么使用委托,本博客的下一篇文章比较简洁。
(一)C#中为什么要引入委托
引言:
对于一些刚接触C# 不久的朋友可能会对C#中一些基本特性理解的不是很深,然而这些知识也是面试时面试官经常会问到的问题,所以我觉得有必要和一些接触C#不久的朋友分享下关于C#基础知识的文章,所以有了这个系列,希望通过这个系列让朋友对C#的基础知识理解能够更进一步。然而委托又是C#基础知识中比较重要的一点,基本上后面的特性都和委托有点关系,所以这里就和大家先说说委托,为什么我们需要委托。
一、C#委托是什么的?
在正式介绍委托之前,我想下看看生活中委托的例子——生活中,如果如果我们需要打官司,在法庭上是由律师为我们辩护的,然而律师真真执行的是当事人的陈词,这时候律师就是一个委托对象,当事人委托律师这个对象去帮自己辩护。这就是我们生活中委托的例子的。然而C#中委托的概念也就好比律师对象(从中可以得出委托是一个类,,因为只有类才有对象的概念,从而也体现了C#是面向对象的语言)。
介绍完生活中委托是个什么后,现在就看看C#中的委托怎样和生活中的对象联系起来的,C#中的委托相当于C++中的函数指针(如果之前学过C++就知道函数指针是个什么概念的了),函数指针是用指针获取一个函数的入口地址,然后通过这个指针来实现对函数的操作。C#中的委托相当于C++中的函数指针,也就说两者是有区别的:委托是面向对象的,类型安全的,是引用类型(开始就说了委托是个类),所以在使用委托时首先要 定义—— >声明—— >实例化—— >作为参数传递给方法—— >使用委托。下面就具体看下如何使用委托的:
一、定义:delegate void Mydelegate(type1 para1,type2 para2);
二、声明: Mydelegate d;
三、实例化:d =new Mydelegate(obj.InstanceMethod);(把一个方法传递给委托的构造器),前面三步就好比构造一个律师对象,方法InstanceMethod好比是当事人。
四、作为参数传递给方法:MyMethod(d);(委托实现把方法作为参数传入到另一个方法,委托就是一个包装方法的对象)。
五、在方法中使用委托。MyMethod方法好比是法官,MyMethod方法先调用委托,委托在调用方法InstanceMethod,这个过程就如法官向律师问话,然后律师之前肯定向当事人了解了案件的情况。C#委托中好比是律师,真真诉说案情的是当事人(真真被调用的是实例方法InstanceMethod)
MyMethod方法的定义如下:
{
// 使用委托
mydelegat(arg1,arg2);
}
二、C#中为什么要使用委托的?
相信经过上面的介绍,大家应该对委托不再陌生了吧,然而我们为什么需要委托的,好好地为什么要实例化中间这个对象的,为什么不直接在MyMethod方法里面调用InstanceMethod方法的,这样不是自找麻烦的吗?为了大家可以更好的明白为什么要使用委托,下面通过一个Window Form的 ”文字抄写员“ 程序要解释下为什么。
程序实现的功能是:在下方文本框输入文字,勾选“书写到”组合框中的“文本区1”或“文本区2”复选框后点击“开始”按钮,程序会自动将文本框中的文字”抄写“到对应的文本区中去。程序界面如下:
传统的实现代码为:
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
if (checkBox1.Checked == true)
{
textBox1.Clear();
textBox1.Refresh();
// 调用方法WriteRichTextBox1想文本区1写入文字
this.WriteTextBox1();
textBox3.Focus();
textBox3.SelectAll();
}
if (checkBox2.Checked == true)
{
textBox2.Clear();
textBox2.Refresh();
// 调用方法WriteRichTextBox2想文本区2写入文字
this.WriteTextBox2();
textBox3.Focus();
textBox3.SelectAll();
}
}
private void WriteTextBox1()
{
string data = textBox3.Text;
for (int i = 0; i < data.Length; i++)
{
textBox1.AppendText(data[i].ToString());
//间歇延时
DateTime now = DateTime.Now;
while(now.AddSeconds(1) >DateTime.Now)
{ }
}
}
private void WriteTextBox2()
{
string data = textBox3.Text;
for (int i = 0; i < data.Length; i++)
{
textBox2.AppendText(data[i].ToString());
//间歇延时
DateTime now = DateTime.Now;
while (now.AddSeconds(1) > DateTime.Now)
{ }
}
}
}
}
然而我们从代码中会发现WriteTextBox1()方法和WriteTextBox2()只有一行代码不一样的( textBox1.AppendText(data[i].ToString()); 和 textBox2.AppendText(data[i].ToString());),其他都完全一样,而这条语句的差别就在于向其中写入文本的控件对象不一样,一个是TextBox1和TextBox2,现在这样代码是实现了功能,带式我们试想下,如果要实现一个写入的文本框不止2个,而是好几十个甚至更多,那么不久要写出同样多数量的用于写入文本区的方法了吗?这样就不得不写重复的代码,导致代码的可读性就差,这样写代码也就是面向过程的一个编程方式,因为函数是对操作过程的一个封装,要解决这个问题,自然我们就想到面向对象编程,此时我们就会想到把变化的部分封装起来,然后再把封装的对象作为一个对象传递给方法的参数,下面就利用委托来重新实现下这个程序:
{
public partial class Form1 : Form
{
// 定义委托
private delegate void WriteTextBox(char ch);
// 声明委托
private WriteTextBox writeTextBox;
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
if (checkBox1.Checked == true)
{
textBox1.Clear();
textBox1.Refresh();
// 实例化委托
writeTextBox = new WriteTextBox(WriteTextBox1);
// 作为参数
WriteText(writeTextBox);
textBox3.Focus();
textBox3.SelectAll();
}
if (checkBox2.Checked == true)
{
textBox2.Clear();
textBox2.Refresh();
// 实例化委托
writeTextBox = new WriteTextBox(WriteTextBox2);
// 作为参数
WriteText(writeTextBox);
textBox3.Focus();
textBox3.SelectAll();
}
}
private void WriteText(WriteTextBox writetextbox)
{
string data = textBox3.Text;
for (int i = 0; i < data.Length; i++)
{
// 使用委托
writetextbox(data[i]);
DateTime now = DateTime.Now;
while (now.AddSeconds(1) > DateTime.Now)
{ }
}
}
private void WriteTextBox1(char ch)
{
textBox1.AppendText(ch.ToString());
}
private void WriteTextBox2(char ch)
{
textBox2.AppendText(ch.ToString());
}
}
}
引入委托后实现的代码中,我们通过WriteText方法来向文本区写入内容,它所执行的只是抽象的”写文本“操作,至于究竟像那个文本框写入文字,对于编写WriteText方法的程序来说是不知道,委托writeTextBox就像一个 接口 一样(面向对象设计原则中有一个很重要的原则就是——针对接口编程,不针对实现编程),屏蔽了操作对象的差别(方法到底是想向文本区1写入文本还是像文本区2写入文本,现在我方法里面不需要去关心,我只需要集中在实现”书写文本”这个操作,而不必纠结操作对象的选择)。
三、委托的作用到底是什么?——委托总结陈词
相信通过上面两部分大家也明白了委托是个什么东西以及C#中为什么要引入委托这个概念。现在就总结下引入委托后到底作用在那里? 从上面的委托代码中可以发现,引入委托后,编程人员可以把方法的引用封装在委托对象中(把过程的调用转化为对象的调用,充分体现了委托加强了面向对象编程的思想。),然后把委托对象传递给需要引用方法的代码,这样在编译的过程中我们并不知道调用了哪个方法,这样一来,C#引入委托机制后,使得方法声明和方法实现的分离,充分体现了面向对象的编程思想。
委托对自己的总结:
我是一个特殊的类,我定义了方法的类型,(就像int定义了数字类型一样,当用一个方法实例化委托对象时,这个委托就代表一个方法,这个方法的类型就是委托类型),我可以将方法当做另一个方法的参数来进行传递,使得程序更容易扩展。
(二)委托的本质
上一节我们已经和大家分享了我理解的——C#中为什么需要委托,上一节中简单介绍了下委托是什么以及委托简单的应用的,在这节中将对委托做进一步的介绍的,本节主要对委本质和委托链进行讨论。
一、委托的本质
平时我们很容易使用委托——用C# delegate关键字定义委托,再用new操作符构造委托实例,然后通过调用委托实例来调用回调方法(就是用一个了委托对象的变量来代替方法名,这句话如果刚接触的人不好理解的话,这里给个例子:MyDelegate mydelegate =new Mydelegate(obj.mymethod),MyDelegate 是定义的一个委托,假设定义的是没有参数的,然后调用委托实例是这样的——mydelegate(), 大家可以发现此时调用委托和调用方法的方式是一模一样的,如果没有看前面mydelegate是个委托类型,大家都会认为这是直接调用一个方法,而不是调用委托实例,通过这个例子大家应该很容易明白了这句话了吧——用一个委托对象的变量来代替方法名),相信通过括号内的讲解后,相信大家又会对委托有进一步的理解的——委托就是方法的代替品,委托变量此时着方法名,大家可以简单理解委托是方法的一个 “外号”。
前面的都介绍了委托的一些使用和理解的,现在就让我我们来进一步看看编译器和CLR在背后对我们用delegate 关键字定义的委托类型做了些什么事情的,前一个专题中我和大家说过委托是一个类,这么是有根据的,因为我们在IDE中定义一个委托类型时,最终是通过编译器将定义的代码转化为中间语言IL,然后再执行中间语言中的代码来转化为本机代码的,所以在Visual Studio中编写的代码只是一个包装而已,真真程序执行的是中间语言中的代码的。现在就看看编译器把我们定义的委托类型转化为什么样的中间语言代码的。
当我们在类中像下面这样定义一个委托时:
编译器把我们定义的委托类型编译成一个下面这样的类:
{
public DelegateTest(Object object, IntPtr method);
public virtual Void Invoke(int32 parm);
public virtual IAsyncResult BeginInvoke(Int32 parm, AsyncCallback callback, Object object );
public virtual void EndInvoke(IAsyncResult result);
}
从中间语言的代码就可以很明显的看出我们在代码中写的委托,对于中间语言来说就是一个类,该类继承于FCL中定义的Systme.MulticastDelegate类型,所有委托类型都派生于MulticastDelegate,该类中还定义了四个方法,一个构造函数,Invoke方法,还有就是两个异步方法BeginInvoke和EndInvoke方法,关于这两个异步方法,大家可以查看我博客中的线程系列。大家可以用ILDasm.exe工具去查看委托生成的中间代码,下面我截的一个图(从我们定义的DelegateTest的前面的图标和我们主程序传递Program的图标是一样的,然而Program是一个类,很明显定义的委托DelegateTes也是一个类的):
由于所有委托类型都是继承于MulticastDelegate,MulticastDelegate又继承于Delegate,所以委托类型继承了MulticastDelegate的字段、属性和方法,在这些成员中,有三个非公共字段与后面专题要介绍的委托链有关,所以在这里先列出来的:
字段 | 类型 | 解释 |
---|---|---|
_target | System.Object | 当委托对象包装的是一个静态方法时,这个字段为null,当委托对象包装一个实例方法时,这个字段引用的是方法所在的类的对象 |
_methodPtr | System.IntPtr | 一个内部的整数,可以认为是方法句柄,标识着要调用的方法 |
_invocationList | System.Object | 该字段通常为null,当构造一个委托链(多播委托)时,才引用一个委托数组。具体下一部分讲解。 |
大部分人可能会有这么个疑问,既然是非公共字段,所以在MSDN上是看不到的,那我是怎么知道有这三个字段的呢?大家可以通过Reflector工具是反编译查看源码,Multicastdelegate 类通过MSDN查找可以知道该类的命名空间和程序集,这样就可以更具程序集和命名空间用Reflector工具查看Multicastdelegate类的源码,下面是我用Reflector这个工具查看到的源码截图:
从截图中可以看出MulticastDelegate 类中只有两个字段,却没有前面表格中列出的_methodPtr和_target字段的,这两个字段是定义在Delegate类中,大家使用Reflector工具来查看的,这里就不具体贴图了,文章最后会给出Reflector工具下载链接的。
委托对象就是一个包装器,包装了一个方法和调用该方法时要操作的对象,例如,执行下面的代码时:
public class Program
{
// 声明一个委托类型,它的实例引用一个方法
// 该方法回去一个int 参数,返回void类型
public delegate void DelegateTest(int parm);
public static void Main(string[] args)
{
// 用静态方法来实例化委托
DelegateTest dtstatic = new DelegateTest(Program.method1);
// 用实例方法来实例化委托
DelegateTest dtinstance = new DelegateTest(new Program().method2);
}
private static void method1(int parm)
{
Console.WriteLine(“调用的是静态方法,参数值为:” + parm);
}
private void method2(int parm)
{
Console.WriteLine(“调用的是实例方法,参数值为:” + parm);
}
}
代码中dtstatic 和dtinstance变量引用了初始化好的DelegateTest委托对象,此时这两个委托对象的上面列出来的三个字段初始化情况如下图:
二、总结
本专题从中间语言的角度去详细解析定义的委托类型经编译器转化后的的中间语言是怎样来解释一个委托类型的,得到的结论是—— 委托实际上是一个类 ,该类派生于MulticastDelegate类,且继承了该类的_target,_methodPtr和_invocationList这三个字段,当我们初始化一个委托对象时,此时就会先初始化这三个字段,对于包装实例方法和静态方法的委托,初始化这三个字段也有所不一样,在上面的截图中也所体现,这里引用了一个很重要的字段——_invocationList(即委托实例的调用列表),对于委托对象包装一个方法时,该字段为null,如果委托对象要包装多个方法时,此时_invocationList字段就会被初始化为引用一个委托对象的数组(就是指向委托对象的一个集合),具体这方面的内容将在下一专题介绍委托链中为大家详细介绍。 到这里,本专题的内容也结束了,希望通过本专题,大家可以更进一步的理解C#中的委托。
Reflector工具的下载地址:http://files.cnblogs.com/zhili/Reflector.zip。
(三)委托链-同时委托多个方法
上一节中介绍了下编译器是如何来翻译委托的,从中间语言的角度去看委托,希望可以帮助大家进一步的理解委托,然而之前的介绍都是委托只是封装一个方法,那委托能不能封装多个方法呢?因为生活中经常会听到,我代表大家的意见等这样的说话,既然委托也是一个代表,那他如果只能代表一个人,那他的魅力就不是很大了吧,所以我们就会委托能不能代表多个方法的? 答案是可以的,这就是本节要讲的内容——委托链,委托链也是一个委托,只是因为它是把多个委托链在一起,所以我们就以委托链来这么称呼它的。
一、到底什么是委托链
我们平常实例化委托对象时都是绑定一个方法的, 前一个专题介绍的委托也是包装了一个方法的, 用前面的例子就是委派律师的只有一个人,也就是当事人只有一个的,但是现实生活中显然不是这样的,在官司的时候律师可以同时接多个案子,也是接收多个当时人的委派,这样,该律师就与多个当事人绑定在一起了, 需要了解多个当事人的案件情况的。其实这就是生活中的委托链,此时这位律师不仅仅是一个人的代表律师了,而是多个当事人的律师。生活中的委托链和C#中的委托链很类似的,现在就说说C#中的委托链到底是个什么的?
首先委托链就是一个委托,所以大家不要看到委托链感觉又是什么C#中的新特性的,然而要把多个委托链在一起,就必须存储多个委托的引用,那委托链对象是在哪里存储多个委托的引用的呢?还记得我们上一专题中,我们介绍的委托类型有三个非公共字段的吗?这三个字段是——_target,methodPtr 和_invocationList,至于这三个字段具体代表什么大家可以查看我的上一专题的文章,然而_invocationList 字段正是存储多个委托引用的地方的。
为了更好的解释_invocationList是如何来存储委托引用的,下面先看一个委托链的例子和运行结果,然后再分析原因:
namespace DelegateTest
{
public class Program
{
// 声明一个委托类型,它的实例引用一个方法
// 该方法回去一个int 参数,返回void类型
public delegate void DelegateTest(int parm);
public static void Main(string[] args)
{
// 用静态方法来实例化委托
DelegateTest dtstatic = new DelegateTest(Program.method1);
// 用实例方法来实例化委托
DelegateTest dtinstance = new DelegateTest(new Program().method2);
// 隐式调用委托
dtstatic(1);
// 显式调用Invoke方法来调用委托
dtinstance.Invoke(1);
// 隐式调用委托
dtstatic(2);
// 显式调用Invoke方法来调用委托
dtinstance.Invoke(2);
Console.Read();
}
private static void method1(int parm)
{
Console.WriteLine(“调用的是静态方法,参数值为:” + parm);
}
private void method2(int parm)
{
Console.WriteLine(“调用的是实例方法,参数值为:” + parm);
}
}
}
运行结果:
下面就来分析下为什么会出现这样的结果的:
一开始我们实例化了两个委托变量,如下代码:
DelegateTest dtstatic = new DelegateTest(Program.method1);
// 用实例方法来实例化委托
DelegateTest dtinstance = new DelegateTest(new Program().method2);
委托变量dtstatic和dtinstance引用的委托对象的初始状态如下图:
然后我们定义了一个委托类型的引用变量delegatechain,刚开始它没有任何委托对象,是一个空引用,当我们执行下面的一行代码时:
Combine 方法发现试图合并的是null和dtstatic,在内部,Combine直接返回dtstatic中的对象,此时delegatechain和dtstatic变量引用的都是同一个委托对象,如下图所示:
为了演示委托链,我们通过代码在再添加一个委托,此时就再调用了Combine方法,代码如下:
这时候,Combine方法发现delegatechain已经引用了一个委托对象了(此时已经引用了destatic引用的委托对象了),所以Combine会构造一个新的委托对象(这一点很想String.Concat,我们简单的使用是通过+操作符把两个字符串连接起来,这个新的委托对象会对它的私有字段_target和_methodPtr字段进行初始化,然后此时_invocationList字段初始化为引用了一个委托对象的数组,这个数组的第一个元素(下标为0)就是被初始化为引用包装了method1方法的委托,数组的二个元素被初始化为引用包装了method2方法的委托(也就是dtinstance引用的委托对象),最后delegaechain被设为引用新建的这个委托对象,下面是一个图,可以帮助大家理解委托链(也叫多播委托):
同样的道理,如果是添加第三个委托给委托链,过程也是和上面一样的, 此时又会新建一个委托对象,此时_invocationList字段会初始化为引用一个保存这三个委托对象数组,然而有人会问了——对于已经引用了委托对象的委托类型变量调用Combine方法后会创建一个新的委托对象,然后对新的这个委托对象的三个字段进行重新初始化话,最后把之前的委托类型变量引用新创建的委托对象(这里就帮大家总结下委托链的创建过程),那之前的委托对象怎么办呢? 相信大部分人会有这个疑问的,这点和字符串的Concat方法很像,之前的委托对象和——invocationList字段引用的数组会被垃圾回收掉(正是因为这样,委托和字符串String一样是不可变的)。
注意:我们还可以调用Delegate的Remove方法从链中删除委托,如调用下面代码时:
方法被调用时,它会扫描delegateChain(第一个参数)所引用的委托对象内部维护的委托数组( 如果对于委托数组为空的情况下调用Remove方法将不会有任何作用,就是不会删除任何委托引用,这里主要是说明扫描是从委托数组里进行扫描 ),如果找到delegateChain引用的委托对象的_target和_methodPtr字段和第二个参数(新创建的委托)中的字段匹配的委托,如果删除之后数组中只剩下一个数据项时,就返回那个数据项(而不会去新建一个委托对象再初始化的,此时的_invocationList为null,而不是保存一个委托对象引用的数组了,具体可以Remove一个后调试看看的),如果此时数组中还剩余多个数据项,就新建一个委托对象——其中创建并初始化_invocationList数组(此时的数组引用的委托对象已经少了一个了,因为用Remove方法删除了),并且,每次Remove方法调用只能从链中删除一个委托,而不会删除有匹配的_target和_methodPtr字段的所有委托(这个大家可以调试看看的)。
二、如何对委托链中的委托调用进行控制
通过上面相信大家可以理解如何创建一个委托链对象的,但是从运行结果中还可以看出,每次调用委托链时,委托链包装的每个方法都会顺序被执行,如果委托链中被调用的委托抛出一个异常,这样链中的后续所有对象都不能被调用,并且如果委托的前面具有一个非void的返回类型,则只有最后一个返回值会被保留,其他所有回调方法的返回值都会被舍弃,这就意味着其他所有操作的返回值都永远看不到的吗? 事实却不是这样的,我们可以通过调用Delegate.GetInvocationList方法来显式调用链中的每一个委托,同时可以添加一些自己的定义输出。
GetInvocationList 方法返回一个由Delegate引用构成的数组,其中每一个数组都指向链中的一个委托对象。在内部,GetInvocationList创建并初始化一个数组,让数据的每一个元素都引用链中的一个委托,然后返回对该数组的一个引用。如果_invocatinList字段为null,返回的数组只有一个元素,该元素就是委托实例本身。下面就通过一个程序来演示下的:
{
class Program
{
// 声明一个委托类型,它的实例引用一个方法
// 该方法回去一个int 参数,返回void类型
public delegate string DelegateTest();
static void Main(string[] args)
{
// 用静态方法来实例化委托
DelegateTest dtstatic = new DelegateTest(Program.method1);
// 用实例方法来实例化委托
DelegateTest dtinstance = new DelegateTest(new Program().method2);
DelegateTest dtinstance2 = new DelegateTest(new Program().method3);
// 定义一个委托链对象,一开始初始化为null,就是不代表任何方法(我就是我,我不代表任何人)
DelegateTest delegatechain = null;
delegatechain += dtstatic;
delegatechain += dtinstance;
delegatechain += dtinstance2;
delegatechain =(DelegateTest)Delegate.Remove(delegatechain,new DelegateTest(method1));
delegatechain = (DelegateTest)Delegate.Remove(delegatechain, new DelegateTest(new Program().method2));
Console.WriteLine(Test(delegatechain));
Console.Read();
}
private static string method1()
{
return ”这是静态方法1”;
}
private string method2()
{
throw new Exception(“抛出了一个异常”);
}
private string method3()
{
return ”这是实例方法3”;
}
// 测试调用委托的方法
private static string Test(DelegateTest chain)
{
if (chain == null)
{
return null;
}
// 用这个变量来保存输出的字符串
StringBuilder returnstring = new StringBuilder();
// 获取一个委托数组,其中每个元素都引用链中的委托
Delegate[] delegatearray = chain.GetInvocationList();
// 遍历数组中的每个委托
foreach (DelegateTest t in delegatearray)
{
try
{
//调用委托获得返回值
returnstring.Append(t() + Environment.NewLine);
}
catch (Exception e)
{
returnstring.AppendFormat(“异常从 {0} 方法中抛出, 异常信息为:{1}{2}”, t.Method.Name, e.Message, Environment.NewLine);
}
}
// 把结果返回给调用者
return returnstring.ToString();
}
}
}
运行结果截图:
从运行结果可以看出,此时我们可以获得每一个回调方法的返回值,并且可以加入一些自定义的返回值的(程序中加入了换行字符串),这样就可以对委托链中的每个委托对象进行控制了,即使其中一个抛出异常,此时我们也可以进行捕获,而不会导致后续的委托对象不能被调用的问题。
三、总结
本专题主要介绍如何创建一个委托链以及对于创建一个委托链的过程进行了详细的分享,第二部分主要先指出了委托了一些局限性,然后通过调用GetInvocationList方法来返回一个委托数组,这样就可以通过遍历委托数组中的每个委托来通知委托的调用过程,这样就可以对委托链的调用进行更多的控制的。到此本专题也就介绍完了,通过这三个专题对委托的介绍,相信大家会对委托有一个更深的理解。