八旬女码农为何裸死街头,数百只程序猿为何半夜惨叫?线程安全为何屡遭黑手?跨线程操作控件为何频频报错?晦涩难懂的篇篇装B教程,究竟是何人所为?EventArgs类的背后到底是委托还是事件?GUI编程的事件背后又隐藏着什么?敬请关注OOTV年度巨献《走近科学》,让我们跟随镜头走进委托和事件的内心世界。
一直对委托和事件概念模模糊糊,也搞不懂明明简单的代码为什么要用委托,经过一段时间的深入,终于明白一点了,现在就以一个菜鸟学习的心路历程,一步一步地“走近科学”。
视频在这里:http://blog.csdn.net/bzzd2001/article/details/43796309
一、为什么要使用委托
(如果你不在乎为什么要用委托,可以跳过本节,直接从第二部分开始阅读)
写了几个小程序,一直没觉得哪里该用委托,或者说写的时候对委托还不太了解。所以觉得要委托做什么呢?学了委托以后,当然可以在很多地方,硬硬地以委托的形势进行改写,这又有什么意义呢?除了增加代码量,没有任何好处。估计也是很多新手的困惑,觉得我不学委托,照样写代码。
我举个不太恰当的例子,你是一个孩子,刚刚学会了走,学会了跑,但还没有学会跳(甚至不知道别人还有“跳”这种技能),你也同样会认为这个世界已经是你的了,从家到学校的路程,照样和那些掌握了“跳”技能的同学一样能完成。而有一些刚刚学会“跳”的同学,为了在上学路上用上这个技能,还会比你更费力,用时更长。你为此不理解学习“跳”的意义,但当有一次,途径的一个小桥断了,你不得不绕了很远的路才到学校,而别人可以轻松的“跳”过去。这一次还算幸运,还有路可绕,可一下次,很有可能你遇到的问题,已经无路可绕,非“跳”不可了。
那对于委托来说,什么时候才会非“委托”不可呢?我们还是通过代码来看吧
让一个新手来做一道题:实现一个能定时做某事的类,就像winform的timer控件一样的东西,快过年了,我们就写个能给大家拜年的代码吧
namespace delegate_1
{
//我们写的定时器类
class MyTimer
{
public static void Loop(int sleep,string title) //一个能循环调用方法的方法
{
Console.WriteLine("我要开始{0}了!",title); //先喊一嗓子告诉大家我要做啥子
while (true)
{
DoSomeThing();
System.Threading.Thread.Sleep(sleep); //间隔一下再循环
}
}
public static void DoSomeThing() //具体要做事的在这里
{
Console.WriteLine("新年快乐!万事如意!");
Console.WriteLine("********^**********");
Console.WriteLine("********^**********");
}
}
class Program
{
//客户端调用我们写好的类
static void Main(string[] args)
{
MyTimer.Loop(2000, "拜年"); //把要间隔的时间和要做的事情的名称传到调用的方法里
}
}
}
代码很容易懂吧,每2秒向大家拜一次年。但这个定时器类不能只用来拜年了,春节很快就过去了,这个类就废了。
现在我想用这个类,来做一个电子表,每1秒显示一下当前时间。
嗯,好办,这样改
namespace delegate_1
{
//我们写的定时器类
class MyTimer
{
public static void Loop(int sleep,string title) //一个能循环调用方法的方法
{
Console.WriteLine("我要开始{0}了!",title); //先喊一嗓子告诉大家我要做啥子
while (true)
{
// DoSomeThing(); //原来写的方法调用要注释掉
TimeDisplay(); //调用新写的时间显示方法
System.Threading.Thread.Sleep(sleep); //间隔一下再循环
}
}
public static void DoSomeThing() //具体要做事的在这里,其实具体的做事的方法写在定时器类里是不好的
{
Console.WriteLine("新年快乐!万事如意!");
Console.WriteLine("********^**********");
Console.WriteLine("********^**********");
}
public static void TimeDisplay() //新加入的时间显示方法,其实具体的做事的方法写在定时器类里是不好的
{
DateTime dt = DateTime.Now;
Console.WriteLine(dt.ToString());
}
}
class Program
{
//客户端调用我们写好的类
static void Main(string[] args)
{
MyTimer.Loop(1000, "报时"); //把要间隔的时间和要做的事情的名称传到调用的方法里
}
}
}
看起来很容易,So Easy!无论要用我的定时器做什么事情,只要在定时器类(MyTimer)里修改代码就可以了。
可是这真的好吗?其实客户要调用我们的定时器类,需要传进来三个参数就好了,一个是int类型的间隔时间,一个是string类型的事情的名称,还有一个是个具体的做事方法。
这三个参数,都应该在Main方法里,由最终调用定时器类的用户传给Loop方法。但我们现在给Loop方法只传进去了2个参数,第三个“参数”,也就是具体的做事方法,我们是写死在定时器类的,这明显是不对的,定时器只能定时循环,他哪里会拜年?
你封装好的定时器类,交给你的团队或客户去调用,就这样让别人改来改去?代码改错了,屎盆子不还是要扣到你头上。WinFrom里的Timer控件,什么时候允许你改它的源代码了?它只需要你传入两个参数,一个循环的时间,一个要做的事就行了,我们的定时器类该如果封装呢?
好办,我把Loop方法的签名的2个参数改成三个参数就行了。
//我们写的定时器类
class MyTimer
{
public void Loop(int sleep, string title, 方法??? DoSomeThing)
{
Console.WriteLine("我要开始{0}了!",Title); //先喊一嗓子告诉大家我要做啥子
while (true)
{
DoSomeThing();
System.Threading.Thread.Sleep(sleep); //间隔一下再循环
}
}
}
坏了,第3个参数写不好了,间隔时间sleep是int类型,事情名称title是string类型,那这个DoSomeThing方法是什么类型呢?
我们都知道,方法是类的成员,方法不是类型,不是类型哪有类型?只有类型才能写的方法的签名里去啊。
桥断了,只能“跳”过去了。
不会“跳”的人,要么学“跳”,要么转身回家。
二、委托是什么
上一节讲到,方法没有办法当作参数传给另一个参数,因为方法不是类型。
如果才能把方法当作参数传给另一个方法呢?很多人都说,明白了,用委托,委托就是一个方法当参数传给另一个方法。
是的,很多教程文章里都说:把一个方法当参数传给另一个方法。
可是我想说,错了!方法既然不是类型,他就永远不能把方法本身作为参数传给另一个方法,臣妾做不到啊!
自己做不到的事情怎么办呢?
委托别人办啊!
委托谁来办呢?
委托“委托”来办啊!
这“委托”是谁啊,他能办吗?他凭什么能办啊?
“委托”当然能办啊,因为“委托”是类型啊。
委托是类型?
是啊,和int类型,string类型,类类型(class)一样,委托是类型,一个能表示方法的数据类型。
很容易理解,委托是引用类型,他引用的是方法。
委托和类最为相似,只不过类表示的是数据和方法的集合,而委托则持有一个或多个方法。(《C#图解教程》P239)
上面那句是书的话,用我的话,简单的理解,类里面有成员(属性、字段、方法),而委托只有方法的“快捷方式”,委托不是方法本身,只是方法的一个引用。
委托里面可以有一个方法的“快捷方式”,也可以有一堆"快捷方式"。就像windowns系统里,一个程序可以有多个快捷方式,可以放到桌面上或别的文件夹里,一个方法也可以有多个重复的“快捷方式”,塞到一个或多个委托里。
当然,如果你心里明白了这一点,如果一个委托只持有一个方法,你完全可以这个委托就当作方法本身。调用委托就是调用方法,把委托当作参数传给别的方法,就相当于把这个方法当作参数传给别的方法。这就是别的教程里所说的:把一个方法当参数传给另一个方法。其实就是把一个方法,创建一个“快捷方式”,然后把这个“快捷方式”传给别的方法。
三、委托的调用
前面说过,委托可以持有一个方法或多个方法,下面就以只持有一个方法的简单委托为例,说明一下,为了让委托做事,拢共分几步:
□ 声明委托类型(所有的定义类型在使用前都要先声明,比如类、枚举)
□ 有一个方法(这个方法要和这个委托相“兼容”,如何“兼容”下面会讲)
□ 创建一个委托实例
□ 调用委托
1、声明委托类型
正如上面所说,委托是类型,就好像类是类型一样。与类一样,委托类型必须在创建实例之前声明。示例代码如下:
delegate void MyDel(int sum);
这个声明就是说:我是个委托,我现在要找一个方法,这个方法和符合我的要求。
什么要求呢:第一,这个方法的返回类型是void,也就是没有返回值。第二,这个方法有一个int类型的参数。
只要符合这两个要求,我就可以接受你。
委托声明和方法的声明很像,有返回类型和签名。
区别是:以delegate关键字开头,并且没有方法体。
2、找一个“兼容”的方法
什么是“兼容”的方法?就是返回类型和方法的签名(参数)都要和第一步所声明的委托类型一致。
void PrintIntA(int x)
{
Console.WriteLine(x*2);
}
static void PrintIntB(int sum)
{
Console.WriteLine(x * 5);
}
int PrintIntC(int x)
{
return x * 2;
}
void PrintIntD(int x,int y)
{
Console.WriteLine(x * y);
}
在上面这四个方法中,只有PrintIntA和PrintIntB是和我们声明的委托类型是相“兼容”的,因为他们没有返回值并且有单个int参数。
3、创建委托实例
既然已经有了一个委托类型和一个相“兼容”的方法,我们就可以创建该委托类型的一个实例,再给他指定一个“兼容”的方法。
//最原始、原“笨”的写法
MyDel del;
dele1 = new MyDel(MyClass.PrintIntB);
//稍稍简化的写法
MyDel del = new MyDel(MyClass.PrintIntB);
//快捷写法,也是最好理解的写法
MyDel del = MyClass.PrintIntB;
这三种写法是完全一样的。尤其是快捷写法,代码的可读性很强,可以简单理解把方法赋值给了委托。委托也就成了这个方法的“代言人”、“代名词”、“快捷方式”。
有朋友会问,为什么还要搞出这么多写法,直接规范写成快捷的不就好了吗?
其实上面两种写是C# 1.0里的标准写法,第三种快捷写法直接传递方法名称,而不是显示实例化,这是自C#2.0开始支持的一个新语法。新版本肯定要向下兼容,所以这三个写法都可以。我反正是写到快捷写法的。
我先写,你们随意。:-)
4、调用委托
这是很容易的事(当然这是指同步调用,可以用BeginInvoke和EndInvoke来异步调用委托,那暂时不是菜鸟考虑的事)。
调用委托就像调用方法一样,就像我前面说的,你就把委托当作方法或方法的“快捷方式”就行了。
像上一步里创建的委托实例del,可以这样调用。
dele3(5);
这样调用其实和直接调用方法一样。
MyClass.PrintIntB(5);
上面的代码都是零零碎碎的,下面贴段完整的代码。
using System;
namespace delegate2
{
delegate void MyDel(int sum); //声明委托类型,这个类型只能“兼容”无返回值并且有单个int参数的方法
class MyClass
{
public void PrintIntA(int x) //一个实例方法,这个方法的返回类型及签名完全符合委托的要求
{
Console.WriteLine(x * 2);
}
public static void PrintIntB(int sum) //一个静态的方法,完全符合委托的要求
{
Console.WriteLine(sum * 5);
}
}
class Program
{
static void Main(string[] args)
{
MyClass Mc = new MyClass(); //实例化一个MyClass类
//最原始、原“笨”的写法(调用静态方法)
MyDel dele1;
dele1 = new MyDel(MyClass.PrintIntB);
//稍稍简化的写法(调用静态方法)
MyDel dele2 = new MyDel(MyClass.PrintIntB);
//快捷写法,也是最好理解的写法(调用静态方法)
MyDel dele3 = MyClass.PrintIntB;
//快捷写法(调用实例方法)
MyDel dele4 = Mc.PrintIntA;
//调用委托
dele1(2);
dele2(2);
dele3(2);
dele4(2);
}
}
}
运行结果:
当然整个第三节内容,就是为了使用委托而使用委托,因为只有这样简单的委托,才容易让新手更容易理解。
看到这里你可能困惑了,明明直接就能完成的成,非要加个中间人才能完成,那你想想,明星明明自己能走能说能干活,为啥还非要请个经纪人?
委托的实质是间接完成某种操作,事实上,许多面向对象编程技术都在做同样的事情。我们看到,这增大了复杂性(看看为了输出这点儿内容,用了多少行代码),但同时也增加了灵活性。(《深入理解C#》第3版 P30)
四、合并和删除委托
1、合并委托
到目前为止,我们讲到的委托还只有一个方法。我们可以像做1+1=2一样,合并两个委托。
MyDel delA = Mc.PrintA; //创建并初始化一个委托实例
MyDel delB = MyClass.PrintB; //创建并初始化一个委托实例
MyDel delC = delA + delB; //合并调用列表
我们使用“+”运算符,创建了第三个委托,这个委托持有2个方法。这两个方法叫做委托的调用列表。有的书上叫做委托链。比如有篇文章这样写道:“将多个方法捆绑到同一个委托对象上,形成委托链,当调用这个委托对象时,将依次调用委托链中的方法。”
其实他的意思就是:一个委托可以持有多个方法,调用委托会调用调用列表中的每一个方法。
补充一句:如果一个方法在调用列表中出现多次,当委托补调用时,每次在列表中遇到这个方法时它都会被调用一次。
2、为委托添加方法
如下面代码为委托的调用列表“添加”了两个方法。
MyDel delA = Mc.PrintA; //创建并初始化一个委托实例
delA += MyClass.PrintB; //增加一个方法
delA += Mc.PrintA; //增加一个方法
咦,“添加”上为什么加了引号呢?因为实际上,委托其实是恒定的,不变的。委托实例被创建后不能再被改变。在使用+=运算符时,实际发生的是创建了一个新的委托,它的调用列表是“+=”左边的委托加上右边的方法的组合。然后将这个新的委托赋值给delA。我们可以用“+=”为委托“添加”多个方法。
3、从委托中移除方法
移除用“-=”运算符。
delA -= MyClass.PrintC; //从委托移除一个方法
和添加方法一样,移除方法其实是创建了一个新的委托。新委托是旧委托的副本,只是比旧委托少了一个方法。
如果去移除一个委托中并不存在的方法,等于这句没写,无效语句。
如果委托中的方法被移除完了,委托就成了空委托,空委托不能调用,调用会报错。所以我们在使用委托前有必要检查一个委托是否为null。
五、委托的其它知识点
1、调用带返回值的委托
到目前为止,我们创建的委托全部都没的返回值。事实上,在实际编程很少用到带返回值的委托。
所以这里只简要说一下。
如果是只持有一个方法的简单委托,你完全可以把委托当作方法本身来调用,所以方法怎么返回,委托也一样返回。
class Program
{
public static int Add20(int x) //一个静态的具名方法,返回类型是int
{
return x + 20;
}
delegate int MyDel(int sum);
static void Main(string[] args)
{
MyDel dele = Add20; //将具名方法赋值给委托
Console.WriteLine("计算结果是:{0}",dele(5));
}
}
如果是持有多个方法的委托,调用列表中最后一个方法返回的值主是委托所返回的值。调用列表中所有的其他方法的返回值都会被忽略。
2、匿名方法、Lambda表达式
匿名方法是C# 2.0才引入的,它简化了委托的调用,只是语法的精简。以后我们有机会再谈。在实际编程中,匿名方法的委托使用的要比具名方法的委托要多得多,特别是c# 3.0引入了Lambda表达式,更加简化匿名方法的的语法,使代码列简练、优雅、可读性更强。
上面那段代码使用就是具名方法,我们可以看出,这个名为Add20的具名的方法,他的方法体内的代码只有一行,完全没有必要使用繁琐的具名方法写法,改写如下:
第二版本代码:使用匿名方法
class Program
{
//public static int Add20(int x) //原来的具名方法不要了,为了便于读者找不同,不删除,只注释掉
//{
// return x + 20;
//}
delegate int MyDel(int sum);
static void Main(string[] args)
{
MyDel dele = delegate(int x) //将匿名方法赋值给委托
{
return x + 20;
}; //注意这里有个分号,表示这句语句的结束。
Console.WriteLine("计算结果是:{0}",dele(5));
}
}
如果使用Lambda表达式会让代码更简洁,更容易理解。
MyDel dele = delegate(int x) { return x + 20; }; //匿名方法。(拉成一行是不是变得好看了?)
MyDel lam1 = (int x) => { return x + 20; }; //Lambda表达式
MyDel lam2 = (x) => { return x + 20; }; //Lambda表达式
MyDel lam3 = x => { return x + 20; }; //Lambda表达式
MyDel lam4 = x => x + 20 ; //Lambda表达式
上面5行代码是等价的。从第1行到2行,是用Lambad表达式代替了匿名方法。所以delegate关键字不要了,加了一个Lambad运算符=>。读作“goes to”。
第2行到第3行转换,少写了参数类型,这是因为编译器已经从委托的声明中知道了参数的类型,就没有必要再写了。
第3行到第4行转换,少写了(),这是因为这个委托只有一个参数,()就显得没有意义了,可以不写了。如果没有参数,则必须要写上()。
第4行到第5行转换,少写了{ return ; },这是因为,如果Lambad表达式允许表达式的主体是语句块或表达式,我们可以将语句块替换为return后面的表达式。
最后一种形式的写法,只有原始匿名方法的四分之一,非常简洁易懂。其实,如果先引入了Lamdba表达式,就不会存在匿名方法。(《C#图解教程》P252)
看一下使用这种写法的完整代码。
第三版代码:使用Lambad表达式
using System;
namespace delegate3
{
class Program
{
delegate int MyDel(int sum);
static void Main(string[] args)
{
MyDel lam4 = x => x + 20; //Lambda表达式
Console.WriteLine("计算结果是:{0}",lam4(5));
}
}
}
3、异步调用委托
可以用BeginInvoke和EndInvoke来异步调用委托,那可能是另一遍长篇大论的文章才能解决了。
六、为什么要学习委托
我的观点:
1、一个小孩必须要学会“跳”,哪怕他学会了,也经常用不到。但偶尔也会遇到只有“跳”才能解决的问题。最少,当他看到别人在“跳”时,他要知道这是“跳”。
2、委托增大了复杂性,但同时也增加了灵活性。实际上,使用了Lambad表达式后,委托也用不了几行代码。
3、WinForm里好多跨线程操作控件要使用委托。
4、别人写的好多类里用到的委托,你要能看懂,或是会修改以满足自己的要求。
5、只有理解了委托,才能更好的学习事件,下一篇文章,我们要学习事件。在下一篇文章里,我会举个银行账户转账到账时给登记的手机发送短信的例子,相信你一定会感兴趣。
最后声明:本人是编程菜鸟(不单是C#菜鸟),学习DELPHI三个月后转投C#门下,潜心学习基础知识几个月,也写过几个小程序,实践之后回头再深入打基础。
感谢您能看到这里,如果这里有我的理解偏差或是信口雌黄,请方家一定给予指正。
献丑了。
(第一节里那个定时器的类,我并没有完成,请你自己动手把它完成吧!)