至今最大之敌:委托和事件(重中之重)
1.委托的定义
delegate(委托)是表示将方法作为参数传递给其他方法。 委托类似于函数指针,但与 函数指针不同的是,委托是面向对象的,类型安全的和保险的。 委托既能引用静态方法, 也能引用实例方法
2.委托的引入
在引入委托之前,我们先来看一段代码:
class HelloWorld{ public void GreetPeople(string name){ EnglishGreeting(name); } public void EnglishGreeting(string name){ Console.WriteLine(“Morning, ” + name); } }
假设以后这段代码需要全球化,加入中国人问候的方法。 -- 首先需要添加中国人问候的方法修改代码如下:
public void ChineseGreeting(string name){ Console.WriteLine(“早上好”, + name); }
然后添加枚举来区分语言,修改代码如下:
enum Language{ English, Chinese }
为调用 ChineseGreeting()这个方法我们同样需要修改 GreetingPeople() 这个方法:
public void GreetPeople(string name, Language language){ if(language == Language.Chinese){ ChineseGreeting(name); }else if(language == Language.English){ EnglishGreeting(name); } }
最终代码如下:
class HelloWorld{ public void GreetPeople(string name, Language language){ //这里的 Language 为上文提到的枚举 if(language == Language.Chinese){ ChineseGreeting(name); }else if(language == Language.English){ EnglishGreeting(name); } } public void EnglishGreeting(string name){ Console.WriteLine(“Morning, ” + name); } public void ChineseGreeting(string name){ Console.WriteLine(“早上好”, + name); } }
这个小程序真的做好了吗? -- 上面的方案大家很容易想到。 利用枚举去扩展语言。 但是这个解决方案 扩展性很 差。 假如日后我们需要加入 日语,韩语,拉丁语等。 那我们不得不反复修改 枚举添加新 的语言 和 GreetingPeople() 内部利用 if ... else 或者 switch 分支去根据传入的参数,判断 调用某个语言进行问好。
思考分析
我们看(string name),string 是参数类型,name 是参数变量。 我们根据传入的形参 去进行相应的赋值。 那么假如 GreetPeople() 方法可以接受另一个参数变量,这个参数变 量可以代表某个方法, 当我们调用 GreetPeople() 的时候就不要在内部进行判断,我们直接 告诉它,喂你帮我调用 xxx 方法,问题不就解决了吗? ++++实际上我们只需要修改成这样: GreetPeople(string name, **** method), 那么现在的 问题就是 method 参数的这个形参的具体类型是什么。 ++++没错,method 的类型就是委托。 这里的 method 参数能够确定方法的种类,进一步讲, method 代表方法的参数类型和返回类型。
声明并定义委托
class HelloWorld{ //声明委托 public delegate void GreetingDelegate(string name); ...... }
void 表示委托代表的方法的返回值类型。
string name 表示委托代表的方法的参数类型。
有了委托之后我们可以修改 GreetPeople(string name, **** method),代码如下:
public void GreetingPeople(string name, GreetingDelegate method){ //委托的调用方法同方法调用一样 method(name); }
-这里的 GreetingDelegate 就是 method 的类型,或者叫类。 需要注意的是委托的声明 方式和类却完全不同。 实际上,委托在编译的时候确实会编译成类。 因为 Delegate 是一 个类,所以在任何可以声明类的地方都可以声明委托。
利用委托实现最终代码效果
class HelloWorld{ public delegate void GreetingDelegate(string name); public static void EnglishGreeting(string name){ Console.WriteLine(“hello, ” + name); } public static void ChineseGreeting(string name){ Console.WriteLine(“你好,” + name); } public void GreetingPeole(string name, GreetingDelegate method){ method(name); } } //测试运行代码 class Program{ static void Main(string[] args){ HelloWorld hw = new HelloWorld(); hw.GreetingPeople(“中国人”, HelloWorld.ChineseGreeting); Console.ReadKey(); } }
总结
委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传 递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用 If-Else(Switch)语句, 同时使得程序具有更好的可扩展性。
将方法绑定到委托
既然委托同 string 都是类型,那我们也可以利用委托声明类似 name 这样的委托变量。 修改代码如下:
class Program{ static void Main(string[] args){ HelloWorld hw = new HelloWorld(); HelloWorld.GreetingDelegate delegate1, delegate2; delegate1 = HelloWorld.EnglishGreeting; delegate2 = HelloWorld.ChineseGreeting; hw.GreetingPeople(“中国人”, delegate1); hw.GreetingPeople(“yanlz”, delegate2); Console.ReadKey(); } }
如你所料,这样是没有问题的。 委托不同于 string 的一个特征: 可以将多个方法赋给 同一个委托,或者叫将多个方法绑定到同一个委托,当调用这个委托的时候,将依次调用其 所绑定的方法。 如下:
HelloWorld.GreetingDelegate delegate1; delegate1 = HelloWorld.EnglishGreeting; delegate1 += HelloWorld.ChineseGreeting; hw.GreetingPeople(“yanlz”, delegate1);
实际上我们可以绕过调用 GreetingPeople(),通过委托来直接调用 EnglishGreeting 和 ChineseGreeting:
HelloWorld.GreetingDelegate delegate1; delegate1 = HelloWorld.EnglishGreeting; delegate1(“yanlz”);
委托绑定方法时需要注意(多播委托), 注意这里,第一次用的“=”,是赋值的语法; 第二次,用的是“+=”,是绑定的语法。 如果第一次就使用“+=”,将出现“使用了未赋 值的局部变量”的编译错误。 既然委托属于类,我们也可以利用这一特性直接 new 出委托实例。
HelloWorld.GreetingDelegate delegate1 = new HelloWorl.GreetingDelegate(HelloWorld.ChineseGreeting); delegate1 += HelloWorld.EnglishGreeting; delegate1(“yanlz”);
既然给委托可以绑定一个方法,那么也应该有办法取消对方法的绑定。 很容易想到, 利用 -=,代码如下:
HelloWorld.GreetingDelegate delegate1 = new HelloWorld.GreetingDelegate(HelloWorld.ChineseGreeting); delegate1 += HelloWorld.EnglishGreeting; delegate1(“yanlz”); delegate1 -= HelloWorld.ChineseGreeting; delegate1(“yanlz”);
匿名函数
我们利用委托实现一个按钮的点击事件 首先我们先声明一个类,代码如下:
Click click = delegate(){ Console.WriteLine(“按钮被点击了 2222”); } click();
运行程序,输出 按钮被点击了 2222
总结
以前我们都是先声明委托,在赋值对应的方法或者直接 new 关联一个方法。实际上 匿 名方法的出现就是在初始化时内敛声明的方法,使得委托的语法更简洁。
Func 委托
Func 委托的 5 种类型
1、delegate TResult Func
2、delegate TResult Func<t1, tresult="">(T1 arg1)
3、delegate TResult Func<t1, t2,="" tresult="">(T1 arg1, T2 arg2)
4、delegate TResult Func<t1, t2,="" t3,="" tresult="">(T1 arg1, T2 arg2, T3 arg3)
5、delegate TResult Func<t1, t2,="" t3,="" t4,="" tresult="">(T1 arg1, T2 arg2, T3 arg3, T4 arg4) ++++Func 委托类型说明:
--(1)只能委托无参但是有返回值的函数,TResult 就是其返回类型。
--(2)只能委托具有一个传入参数,有返回值的函数,T1 为一个传入参数,TResult 为返 回类型。
--(3)只能委托具有二个传入参数,有返回值的函数,T1 和 T2 为两个传入参数,TResult 为返回类型。
--(4)和(5),以此类推。 ++++Func 委托总结 使用 Func 委托函数必须带有返回值,即 TResult。
Func<string> func = delegate(){ return “我是 func<TResult>委托出来的结果”; }; string result = func(); Console.WriteLine(result);
-- 注: 这里为了演示 func 委托,直接用的匿名方法,我们同样可以为 func 委托绑定 方法,这里不做演示。
++++Func 委托总结(第二种 func 类型演示)
Func<string, string> func2 = delegate(string s){ return s.ToLower(); } string result2 = func2(“HELLO”); Console.WriteLine(result2)
注: 这里为了演示 func 委托,直接用的匿名方法,我们同样可以为 func 委托绑定 方法,这里不做演示。 Func 委托总结(第三种 func 类型演示)
Func<stirng, string, string> func3 = delegate(string value1, string value2){ return value1 + “ ” + value2; } Console.WriteLine(func3(“我是”, “Func<T1, T2, TResult>委托出来的结果”));
注: 这里为了演示 func 委托,直接用的匿名方法,我们同样可以为 func 委托绑定 方法,这里不做演示。
Action 委托
+Action 委托的 5 种类型
-- 1、delegate void Action(); 无参,无返回值
-- 2、delegate void Action(T1 arg1);
-- 3、delegate void Action<t1, t2="">(T1 arg1, T2 arg2);
-- 4、delegate void Action<t1, t2,="" t3="">(T1 arg1, T2 arg2, T3 arg3);
-- 5、delegate void Action<t1, t2,="" t3,="" t4="">(T1 arg1, T2 arg2, T3 arg3, T4 arg4); ++++Action 委托类型说明:
-- (1)没有传入参数,也没有返回值,那么它适合代理那些无参,无返回值的函数。
-- (2)有一个传入参数,无返回值,适合代理有参,无返回值的函数。
-- (3)有两个传入参数,无返回值,适合代理有参*2,无返回值的函数
--(4)和(5),以此类推。
委托总结
+至于 Predicate 委托用的太少,这里不做叙述。
++++注意: Func 委托和 Action 委托唯一的区别就是在于代理的方法(函数)有没有返回值。
-- 有返回值选择 Func 委托。
-- 没有返回值选择 Action 委托。
++++系统内置的两种委托绝大多数情况下可以适配任何情况,无需我们在写大量的代码去定 义委托。但是不代表内置委托匹配任何情况。 根据特殊情况还需我们手动定义委托,切记。
Lambda 表达式
+“Lambda”表达式是一个匿名函数,是一种高效的类似于函数式编程的表达式,Lambda 简化了开发中需要编写的代码量。它可以包含表达式和语句,并且可以用创建委托或表达式 目录树类型,支持带有可绑定到委托或表达式树的输入参数的内敛表达式。所有 Lambda 表 达式都使用 Lambda 运算符=>,该运算符读作“goes to”。
++++Lambda 运算符的左边是输入参数(如果有),右边是表达式或语句块。
++++Lambda 表达式是由.NET2.0 演化而来的,也是 LINQ 的基础,熟练地掌握 Lambda 表达 式能够快速地上手 LINQ 应用开发。
++++初识 Lambda 表达式 首先我们先声明一个类包含三个委托,代码如下:
class Lambda{ public delegate void delegate1(string name); //带参数的委托 public delegate void delegate2(); //不带参数的委托 public delegate int delegate3(int x, int y); //有返回值有参数的委托 }
Lambda 表达式的使用
delegate1 d1 = (x) => { Console.WriteLine(“这是一个用 Lambda 表达式委托,参数为” + x); } d1(“严立钻”); //调用 delegate3 d3 = (x, y) => { Console.WriteLine(“这是一个用 Lambda 表达式委托带两个参数,和返回值”); return x + y; }; Console.WriteLine(d3(1, 2));
事件的由来
事件
我们继续思考委托中ᨀ到的第一个程序: 上面的三个方法都定义在 HelloWorld 类中, 这样做是为了理解的方便,实际应用中,通常都是 GreetPeople 在一个类中,ChineseGreeting 和 EngLishGreeting 在另外的类中。这通常叫做业务与逻辑相分离。 GreetPeople 是问好的业 务。 具体使用中文还是英文问好要在各自的类中实现。 达到解耦的目的。
分析猫和老鼠示例
假如有一只猫叫 Tom,有两只老鼠 Jerry 和 Jack,Tom 只要叫一声喵,我是 Tom。 两只 老鼠就会说,老猫来了,快跑。
++++分析上面的案例,有几个类,如何处理类与类之间的关系?
++++猫和老鼠
-- 1、至少存在两个类,Cat 和 Mouse
-- 2、在 Cat 类中存在一个方法,叫 Shout。
-- 3、当 Shout 方法触发时,Mouse 类应该执行某个方法,比如 Run。
-- 显然猫和老鼠并不认识,不会主动通知老鼠我来了。 那么老鼠应该是监听猫的行为, 当猫的行为触发了,老鼠自然就知道了。
事件的引入
在 Cat 类中,不应该关联 Mouse 类。此时委托事件的方式就是最好的处理办法,下面 我们创建一个 Cat 类,部分代码如下:
private string name; public Cat(string name){ this.name = name; } public delegate void GetShoutEventHandler(); public event CatShoutEventHandler CatShout;
事件的定义
事件是说在发生其他类或对象关注的事情时,类或对象可通过事件通知它们。 事件的 关键字是 event。
public event CatShoutEventHandler CatShout;
这行代码的意思是声明事件 CatShout,它的事件类型是委托 CatShoutEventHandler。
正如上节课我们提到的,委托是一种引用方法的类型,一旦为委托分配了方法,委托将 与该方法具有完全相同的行为。
Shout 部分代码
public void Shout(){ Console.WriteLine(“喵,我是{}!”, name); if(CatShout != null){ //表示当执行 Shout 方法时,如果 CatShout 中有对象登记事件, 则执行 CatShout(); } }
调用事件与调用委托是一样的,这里需要注意 CatShout()之所以不带任何参数,是因为 CatShout 的事件类型是委托 CatShoutEventHandler(),而它本身是无参数,无返回值的。
Mouse 部分代码:
private string name; public Mouse(string name){ this.name = name; } public void Run(){ Console.WriteLine(“老猫来了,{0}快跑!”, name); }
Main 函数部分代码
Cat cat = new Cat(“Tom”); Mouse m1 = new Mouse(“Jerry”); Mouse m2 = new Mouse(“Jack”); cat.CatShout += new Cat.CatShoutEventHandler(m1.Run); cat.CatShout += new Cat.CatShoutEventHandler(m2.Run); cat.Shout(); Console.Read();
代码分析: 我们先来看看在我们声明委托的时候,这句代码的意义:
public delegate void CatShoutEventHandler();
这行代码里面我们用的修饰符是 public,众所周知意味着公开的意思,任何外部存 在的类都可以进行赋值(绑定委托时使用=号)。
在某种程度上,委托破坏了对象的封装性。
如何封装委托
你是猴子请来的逗逼吗? 为了保证不破坏对象的封装性,我们将 public 修改为 private。(这特么简直就是在搞笑!) 因为声明委托的目的就是为了把它暴露在类的客户端进行方法的注册,让外部帮我去做 事情的。 你把它声明为 private 了,客户端对它根本就不可见,那它还叫委托吗?
++++那如何封装委托呢? 我们刚接触面向对象的时候,介绍过字段一般推荐用 private 修饰,为了防止外界破坏 类的内部结构,同时为了外界能访问到私有属性,我们引入属性这一概念。 即面向对象三 大特性之一的封装性。 于是,Event 出场了,它封装了委托类型的变量。 使得: 在类的内部,不管你声明它 是 public 还是 protected,它总是 private 的。 在类的外部,注册“+=”和注销“-=”的访问 限定符与你在声明事件时使用的访问符相同。
++++为了验证这个问题,我们修改了 Main 函数部分代码:
Cat cat = new Cat(“Tom”); Mouse m1 = new Mouse(“Jerry”); Mouse m2 = new Mouse(“Jack”); //这里我们修改了 += 为 = cat.CatShout = new Cat.CatShoutEventHandler(m1.Run); cat.CatShout += new Cat.CatShoutEventHandler(m2.Run); cat.Shout(); Console.Read();
运行程序,会出现编译错误
!!!CS 0070 事件 Cat.CatShout 只能出现在+=或-=的左边(从类型”Cat”中使用时除外)
编译错误很清晰的告诉我们除了在类的内部,我们不能用除了+=符号来绑定事件,当然 也可以通过反编译去查看源码(后面高级课我们会介绍如何利用 Refector 反编译源代码) ++++事件其实没有什么不好理解的,声明一个事件不过类似于声明一个进行了封装的委托 类型的变量而已。
事件和委托有什么区别
总结
++++1、事件是一种特殊的委托的实例,或者说是受限制的委托,是委托一种特殊应用,在 类的外部只能施加 +=,-= 操作符,二者本质上是一个东西。
++++2、事件只允许用 add(+=),remove(-=) 方法来操作,这导致了它不允许在类的外部被 直接触发,只能在类的内部适合的时机触发。 委托可以在外部被触发,但是别这么用。 使 用中,委托常用来表达回调,事件表达外发的接口。
++++3、事件不可以当作形参传递,但是委托可以。
思考一个问题,如何在猫来的时候让老鼠知道是哪只猫来了呢? ++++这时候我们需要定义一个类,这个类继承自 EventArgs。 ++++EventArgs 是包含事件数据的类的基类。换句话说见明知意,这个类的作用就是用来在 事件触发时传递数据的。(当然也可以用别的方法实现后面会介绍) ++++新建类 CatShoutEventArgs,代码如下:
class CatShoutEventArgs : EventArgs{ private string name; public string Name{ set{ name = value; } get{ return name; } } }
修改 Cat 类部分代码:
-- 1、修改委托
public delegate void CatShoutEventHandler(object sender, CatShoutEventArgs args);
-- 2、修改 Shout 中部分代码:
CatShoutEventArgs args = new CatShoutEventArgs(); args.Name = this.name; CatShout(this, args);
修改 Mouse 类部分代码:
public void Run(object sender, CatShoutEventArgs args){ Console.WriteLine(“老猫{0}来了,{1}快跑!”, args.Name, name); }
运行程序,你会发现结果和之前显示的不一样了! 我们通过一个类去传递事件发生时 想要传递的参数,对于客户端(Main)方法我们一句代码都没有动过! 我们修改了代码,客户端没有更改就改变了结果。 换句话说,对于我们内部的类来说 修改没有影响到别的类,这就是一定程度上的松耦合。
猫和老鼠案例总结
通过猫和老鼠的案例,我们学习了事件的用法。 其实这个案例是典型的观察者设计模 式,称为 Observer 设计模式: Observer 设计模式是为了定义对象间的一种一对多的依赖关 系(一只猫多个老鼠),以便于当一个对象的状态改变时,其他依赖于它的对象会被自动 告知并更行。 Observer 模式是一种松耦合的设计模式。