本文的内容需要一定的OOP知识,不过我会在另一个内容介绍,由于实现细节,我会先写这部分。
大家关于委托、事件和消息大多是从WinForm编程接触的,首先是你在可视化的设计器里双击控件写所谓事件的处理代码,让编译器帮你做其他事情。然后可能你会听说,事件是和委托有关系的,你可能又会听说事件处理机制和消息也是有关系的。
那么什么是委托,什么是事件,什么是消息呢?
不急,我们从委托开始。
委托是什么?我先撂一句话:委托是方法的类型。
为什么这么说,大家知道,从类和对象的关系来看。对象是类的实例,类是对象的类型,类是对象的抽象,对象拥有类的所有属性和方法,委托和方法的关系也是如此。(如果觉得理解方便的话,你也可以先把委托和方法的关系理解为继承中父类抽象方法的和子类实现其抽象方法关系。)
接下来,我们将通过几个小例子来初步认识委托。
①打招呼的例子(改编自来自网络的例子)
我们需要实现一个方法:打招呼(SayHi)。
我们可能会在利用如下代码向控制台输出:
1 using System; 2 3 namespace DelagateEG 4 { 5 public class Program 6 { 7 public static void Main(string[] args) 8 { 9 Console.WriteLine("早上好!"); 10 //Console.ReadKey(); 11 } 12 } 13 }
这是十分基础的,那么我们加大难度:我们希望通过传入参数来实现对某个具体的人物对象打招呼。
可能您会利用如下代码实现:
1 using System; 2 3 namespace DelagateEG 4 { 5 public class Program 6 { 7 public static void Main(string[] args) 8 { 9 SayHi("Johness"); 10 //Console.ReadKey(); 11 // 输出 早上好!Johness 12 } 13 14 public static void SayHi(string name) 15 { 16 Console.WriteLine("早上好! " + name); 17 } 18 } 19 }
不知道大家觉得怎么样?反正我觉得这样不好,只说一点吧:我传过去的是什么?是一个英文名,那么为什么还是用中文的打招呼?让人家外国友人听得懂可以不?
可能有的朋友会想在SayHi方法主体里判断。如下:
1 public static void SayHi(string name) 2 { 3 if (/*判断是输入的中文姓名*/) 4 { 5 Console.WriteLine("早上好! " + name); 6 } 7 else 8 { 9 Console.WriteLine("Morning! " + name); 10 } 11 }
但是您不会这样做吧?这样做简直就失败了,首先:如果我是中国人,我英语才入门,或者说根本不懂英语,随便给自己来了个英文名,你跟我说“Morning”我根本听不懂;其次,如果现在除了中文和英文,我还需要程序用其他语言打招呼,不可能加继续加if-else吧。
那么,我们可以再传递一个代表语言的参数,比如枚举。当然,现在我们用字符串做一个例子出来。
1 public static void SayHi(string name,string lanType) 2 { 3 switch(lanType) 4 { 5 case "Chinese": 6 Console.WriteLine("早上好! " + name); 7 break; 8 case "English": 9 Console.WriteLine("Morning! " + name); 10 break; 11 // case 12 // …… 13 14 default: 15 break; 16 } 17 }
这样是否可以了呢?不得不说,这样确实解决了问题,但是如果我需要添加新语言,那么是不是我需要修改SayHi方法呢?而且如果是使用枚举作为另一个参数,就还得添加新的枚举。程序的可扩展性就会很差。
在实现真正地将判断的方法提取出来之前我们将代码略做修改,使它更接近面向对象的一些规则:
1 // 判断语言并打招呼 2 public static void SayHi(string name,string lanType) 3 { 4 switch(lanType) 5 { 6 case "Chinese": 7 ChineseSayHi(name); 8 break; 9 case "English": 10 EnglishSayHi(name); 11 break; 12 // case 13 // …… 14 15 default: 16 break; 17 } 18 } 19 20 // 中文的打招呼 21 public static void ChineseSayHi(string name) 22 { 23 Console.WriteLine("早上好! " + name); 24 } 25 26 // 英文的打招呼 27 public static void EnglishSayHi(string name) 28 { 29 Console.WriteLine("Morning! " + name); 30 } 31 32 // ……其他语言
大胆想象一下:如果我们要真正实现将判断语言的方法和打招呼的方法分离该怎么处理?
也许我们可以利用下面的代码(可能有一些牵强):
1 ///2 /// 判断语言类型并打招呼的方法 3 /// 4 /// 任务姓名 5 /// 语言类型 6 public static void JudgeLanguageAndSayHi(string name,LanguageType lanType) 7 { 8 // 判断语言 9 switch (lanType) 10 { 11 case LanguageType.Chinese: 12 SayHi(name, ???); 13 break; 14 case LanguageType.English: 15 SayHi(name, ???); 16 break; 17 // case 18 // …… 19 20 default: 21 break; 22 } 23 }
以上是一个判断语言的方法,在它的方法内部通过分支结构判断语言类型,然后调用了打招呼的方法。
这样可能有一些牵强,但是我们先看一下,上面的代码中???代表什么?我大胆地猜想是不是可以将具体的打招呼的方法传递给SayHi方法,???代表的就是诸如ChineseSayHi或者EnglishSayHi的方法:
1 ///2 /// 打招呼的方法 3 /// 4 /// 人物姓名 5 /// 具体的打招呼的方法 6 public static void SayHi(string name, *** concreteMethod) 7 { 8 concreteMethod(name); 9 } 10 11 12 ///13 /// 语言类型的枚举 14 /// 15 public enum LanguageType 16 { 17 ///18 /// 中文 19 /// 20 Chinese, 21 ///22 /// 英文 23 /// 24 English 25 26 // ……其它语言 27 }
当判断了语言类型后,我们调用SayHi方法,并传递对应的方法作为参数,而SayHi方法在内部调用作为参数传递的具体方法来打招呼 ( 大家可能觉得这样绕了一些弯,不过有时候确实需要这样做,比如异常处理) 。
把方法作为参数传递,可能吗?通过类与对象的关系,我们可以得知,我们想将对象作为参数传递给方法需要找到能代表该对象的类型,比如传递字符串作为参数传递给打招呼的方法,我们就必须在方法声明时指定要接收一个string类型的参数:
1 // 中文的打招呼 2 public static void ChineseSayHi(string name) 3 { 4 Console.WriteLine("早上好! " + name); 5 } 6 7 // 英文的打招呼 8 public static void EnglishSayHi(string name) 9 { 10 Console.WriteLine("Morning! " + name); 11 } 12 13 // ……其他语言
这个很好理解是吧?但是如果把方法作为参数传递的话我们该用什么类型来代表它呢?
C#里有这样的类型:委托。
委托和类是同级的概念,如同类规定了它的对象(或实例)应该有的成员和方法一样,委托也规定了它的实例(即方法)的属性:参数和返回值。
我们可以使用delegate声明委托:
1 public delegate void concreteMethod(string name);
委托规定了它的实例的属性,比如以上的委托,它规定了实例(即具体的方法):必须只有一个字符串类型的参数,必须返回void。它并没有规定子类的具体实现方式。
也就是说上面的委托代表了一类“以一个字符串类型作为参数,并且返回void”的方法。
1 [public|private|protected|……] [……] [static] [……] void MethodName(string param) 2 { 3 statement1; 4 statement2; 5 statement3; 6 statement4; 7 }
我先将使用了委托的解决方案贴出来:
1 using System; 2 3 namespace DelagateEG 4 { 5 ///6 /// 定义委托 7 /// 8 /// 9 public delegate void ConcreteMethod(string name); 10 11 public class Program 12 { 13 public static void Main(string[] args) 14 { 15 JudgeLanguageAndSayHi("Johness", LanguageType.Chinese); 16 JudgeLanguageAndSayHi("阿何", LanguageType.English); 17 // 输出 早上好! Johnesss 18 // Morning! 阿何 19 } 20 21 ///22 /// 判断语言类型并打招呼的方法 23 /// 24 /// 任务姓名 25 /// 语言类型 26 public static void JudgeLanguageAndSayHi(string name, LanguageType lanType) 27 { 28 // 判断语言 29 switch (lanType) 30 { 31 case LanguageType.Chinese: 32 SayHi(name, ChineseSayHi); 33 break; 34 case LanguageType.English: 35 SayHi(name, EnglishSayHi); 36 break; 37 // case 38 // …… 39 40 default: 41 break; 42 } 43 } 44 45 ///46 /// 打招呼的方法 47 /// 48 /// 人物姓名 49 /// 具体的打招呼的方法 50 public static void SayHi(string name, ConcreteMethod concreteMethod) 51 { 52 concreteMethod(name); 53 } 54 55 // 中文的打招呼 56 public static void ChineseSayHi(string name) 57 { 58 Console.WriteLine("早上好! " + name); 59 } 60 61 // 英文的打招呼 62 public static void EnglishSayHi(string name) 63 { 64 Console.WriteLine("Morning! " + name); 65 } 66 67 // ……其他语言 68 69 } 70 71 ///72 /// 语言类型的枚举 73 /// 74 public enum LanguageType 75 { 76 ///77 /// 中文 78 /// 79 Chinese, 80 ///81 /// 英文 82 /// 83 English 84 85 // ……其它语言 86 } 87 }
个人建议:和类同级的如委托和枚举最好放在命名空间下而不放在类内部(并非绝对)。
我们再来梳理一下整个过程:
一,定义一个具有一个字符串参数的没有返回值的委托,我们会使用它来代表我们具体的打招呼的方法。
二,定义一个判断语言的方法,在方法内部我们调用打招呼的方法,并将对应语言的具体打招呼的方法传递给它,让它在内部调用。
三,定义相关枚举和具体方法并测试。
如果不能理解的话也不用着急,只需要记住一点:把它当作类来看,只要记住一点:它的实例是方法就可以了。
刚刚的例子大家肯定会觉得多此一举,如果不会的话,可能您对OOP的理解有一些深入了,也可能您并没有进入我所营造的环境。
如果我估计得没错的话,肯定会有朋友会纠结我为什么在JudgePanguageAndSayHi判断后不直接调用具体的方法。虽然我解释了很多遍,但是如果实在是纠结的话我可以告诉大家:可以跳过中间环节,也是用委托来做。当然,这可能会困难一些。
我先将修改过的代码贴出来再为大家解释:
1 using System; 2 3 namespace DelagateEG 4 { 5 ///6 /// 定义委托 7 /// 8 /// 9 public delegate void ConcreteMethod(string name); 10 11 public class Program 12 { 13 ///14 /// 定义委托类型的属性 15 /// 16 private static ConcreteMethod SayHi; 17 18 public static void Main(string[] args) 19 { 20 JudgeLanguage(LanguageType.Chinese); 21 SayHi("Johness"); 22 JudgeLanguage(LanguageType.English); 23 SayHi("阿何"); 24 // 输出 早上好! Johnesss 25 // Morning! 阿何 26 } 27 28 ///29 /// 纯粹的判断语言的方法 30 /// 可以改一下名字 31 /// 最好改为“修改或设置语言” 32 /// 33 /// 34 private static void JudgeLanguage(LanguageType languageType) 35 { 36 // 判断语言 37 switch (languageType) 38 { 39 case LanguageType.Chinese: 40 // 直接指定委托代表的具体方法 41 SayHi = ChineseSayHi; 42 break; 43 case LanguageType.English: 44 // 委托的构造方法指定其代表的方法 45 SayHi = new ConcreteMethod(EnglishSayHi); 46 break; 47 // case 48 // …… 49 50 default: 51 break; 52 } 53 } 54 55 // 中文的打招呼 56 public static void ChineseSayHi(string name) 57 { 58 Console.WriteLine("早上好! " + name); 59 } 60 61 // 英文的打招呼 62 public static void EnglishSayHi(string name) 63 { 64 Console.WriteLine("Morning! " + name); 65 } 66 67 // ……其他语言 68 69 } 70 71 ///72 /// 语言类型的枚举 73 /// 74 public enum LanguageType 75 { 76 ///77 /// 中文 78 /// 79 Chinese, 80 ///81 /// 英文 82 /// 83 English 84 85 // ……其它语言 86 } 87 }
这一次我修改了两处地方:一是在类内部增加了一个委托类型的属性,这个应该不难理解吧?相当于你自己定义的学生类中有一个字符串类型的属性你将它用来代表学生的姓名一样;二是我直接不要了原来的SayHi方法(请大家看看我的注释,由于时间关系我先写到这儿了)。
我完整地写了一个例子,让学生给大家打招呼:
1 using System; 2 3 namespace DelagateEG 4 { 5 ///6 /// 语言类型的枚举 7 /// 8 public enum LanguageType 9 { 10 ///11 /// 中文 12 /// 13 Chinese, 14 ///15 /// 英文 16 /// 17 English 18 19 // ……其它语言 20 } 21 }
1 using System; 2 3 namespace DelagateEG 4 { 5 6 ///7 /// 委托 8 /// 它本身和打招呼没有任何关系 9 /// 只是它能代表的一类方法中包括了我们需要用到的打招呼的方法 10 /// 11 /// 12 public delegate void SayHiHandler(); 13 14 ///15 /// 学生类 16 /// 17 public class Student 18 { 19 // 学生姓名和年龄 20 public string Name { get; set; } 21 public int Age { get; set; } 22 23 // 学生使用的语言 24 public LanguageType Language { get; set; } 25 26 // 委托成员,我们用它来保存具体的打招呼的方法 27 private SayHiHandler _sayHi; 28 // 封装委托,让外界通过SayHi访问_syHi 29 // 由于委托是方法的类型,所以我们可以将它当方法用 30 // 在外界访问它之前将它绑定到具体方法 31 public SayHiHandler SayHi 32 { 33 get 34 { 35 switch (Language) 36 { 37 case LanguageType.Chinese: 38 _sayHi = ChineseSayHi; 39 break; 40 case LanguageType.English: 41 _sayHi = new SayHiHandler(EnglishSayHi); 42 break; 43 // …… 44 45 default: 46 break; 47 } 48 return _sayHi; 49 } 50 } 51 52 // 在构造函数中初始化一些值 53 public Student() 54 { 55 Name = "Johness"; 56 Language = LanguageType.Chinese; 57 } 58 59 // 中文的打招呼 60 public void ChineseSayHi() 61 { 62 Console.WriteLine("大家早上好! 我是" + Name); 63 } 64 65 // 英文的打招呼 66 public void EnglishSayHi() 67 { 68 Console.WriteLine("Morning! I'm" + Name); 69 } 70 71 // ……其他语言 72 73 } 74 }
1 using System; 2 3 namespace DelagateEG 4 { 5 public class Program 6 { 7 public static void Main(string[] args) 8 { 9 // 使用默认值访问打招呼的方法 10 new Student().SayHi(); 11 12 // 使用自定义的内容打招呼 13 new Student { Name = "阿何", Language = LanguageType.English }.SayHi(); 14 Console.ReadKey(); 15 16 /* 17 * 18 * 输出 大家早上好! 我是Johness 19 * Morning! I'm 阿何 20 * 21 */ 22 } 23 } 24 }
先介绍到这儿。
2012-04-01 18:26:05
现在介绍第二个例子,是我们老师在讲解委托时使用的。由于我是自学的,很多知识并不系统和全面,借用老师的例子可能大家更容易理解。
②显示拷贝进度。
我们都使用过文件或者文件夹的拷贝或者移动。我们在拷贝或移动文件夹时,系统会显示一个进度条表示当前进行了多少个文件的拷贝。那么现在,要求我们通过程序代码实现这个功能,我们会怎么做呢?
假如我们拷贝文件夹(即目录) 的方法是这样的:
1 ///2 /// 拷贝目录的方法 3 /// 4 public static void CopyDir() 5 { 6 // 假如我们拷贝的目录有100个文件 7 // 进行一次循环即完成一个文件的拷贝 8 for (int i = 0; i < 100; i++) 9 { 10 11 } 12 }
那么,我们可能比较简单的写法是在for循环的内部写实现。比如:
1 ///2 /// 拷贝目录的方法 3 /// 4 public static void CopyDir() 5 { 6 // …… 7 // …… 8 for (int i = 0; i < 100; i++) 9 { 10 Console.WriteLine("当前拷贝{0}/{1}", i + 1, 100); 11 } 12 }
当然,这样写是可以完成在控制台输出。但是如果我们在WinForm程序的窗体中显示进度条呢?当然,有的朋友可能会说:我们可以传递一个进度条控件到方法内部。那么我可能频繁地切换进度条显示(比如说在控制台下和界面切换)或者使用其它控件或平台呢?这样的话程序代码的复用性就会变得十分低。
那么我们怎么解决这个问题呢?使用委托可以吗?
答案是肯定的。
我们利用如下代码解决这个问题:
1 using System; 2 using System.Threading; 3 4 namespace DelagateEG 5 { 6 // 委托,用于代表显示拷贝进度的方法 7 public delegate void ShowMsgHanler(int current, int max); 8 9 public class Program 10 { 11 public static void Main(string[] args) 12 { 13 // 我们测试一下代码 14 15 // 测试用第一种方法显示进度 16 ShowMsgHanler showMsag1 = new ShowMsgHanler(ShowMsg1); 17 CopyDir(showMsag1); 18 Console.ReadKey(); 19 /* 输出 20 * 21 * 当前拷贝1/100 22 当前拷贝2/100 23 * …… 24 当前拷贝99/100 25 当前拷贝100/100 26 */ 27 28 // 测试第二种方法 29 ShowMsgHanler showMsag2 = new ShowMsgHanler(ShowMsg2); 30 CopyDir(showMsag2); 31 Console.ReadKey(); 32 /* 输出 33 * 34 * 当前拷贝第1个文件,总共100 35 当前拷贝第2个文件,总共100 36 …… 37 当前拷贝第99个文件,总共100 38 当前拷贝第100个文件,总共100 39 */ 40 } 41 42 ///43 /// 拷贝目录的方法 44 /// 45 /// 46 /// 显示进度的方法 47 /// 48 public static void CopyDir(ShowMsgHanler showMsg) 49 { 50 // 加入我们拷贝的目录有100个文件 51 // 进行一次循环即完成一个文件的拷贝 52 for (int i = 0; i < 100; i++) 53 { 54 showMsg(i, 100); 55 // 为了显示更清晰 56 // 可以使用如下代码让拷贝进度慢一些 57 // Thread.Sleep(300); 58 } 59 } 60 61 ///62 /// 实现显示进度的方法1 63 /// 64 /// 当前拷贝文件 65 /// 总文件数 66 public static void ShowMsg1(int current, int max) 67 { 68 Console.WriteLine("当前拷贝{0}/{1}", current + 1, max); 69 } 70 71 ///72 /// 实现显示进度的方法2 73 /// 用于对比方法1 74 /// 75 public static void ShowMsg2(int current, int max) 76 { 77 Console.WriteLine("当前拷贝第{0}个文件,总共{1}", current + 1, max); 78 } 79 } 80 }
那么如果这样写了,我可以用方法三做界面的进度条显示,方法四用在网页上,等等……这种做法在实际编程上用得十分多。
而且也符合“开闭原则”。
2012-04-02 19:58:11