【菜鸟学c#】委托和事件(一、委托)

来源:互联网 发布:加工中心编程开头 编辑:程序博客网 时间:2024/06/04 23:28

       八旬女码农为何裸死街头,数百只程序猿为何半夜惨叫?线程安全为何屡遭黑手?跨线程操作控件为何频频报错?晦涩难懂的篇篇装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#门下,潜心学习基础知识几个月,也写过几个小程序,实践之后回头再深入打基础。

感谢您能看到这里,如果这里有我的理解偏差或是信口雌黄,请方家一定给予指正。

献丑了。

(第一节里那个定时器的类,我并没有完成,请你自己动手把它完成吧!)

1 0