首页 > 代码库 > 委托和事件 (1) - 委托简析
委托和事件 (1) - 委托简析
个人认为,c#最重要的精髓在于其委托。
说实话现在已经是c#5.0的时代,c#6很快也要出来了,委托作为一个c#1就有的性质,已经早就被更高级的工具例如泛型委托,lambda表达式包装起来了,基本上已经很少有人会在程序中声明一个delegate。不过,了解一下基础也是很好的,
基本概念
委托是一个特殊的类(密封类),可以被视为函数指针,其代表一类和委托签名的输入输出变量类型和个数相同的方法。委托本身可以作为变量传入方法。
借用经典的greetPeople例子,在实际工作中,总会遇到类似的情况,即通过switch来对不同的输入执行不同的结果。但我们看到,其实每个switch执行的方法都很类似,方法的签名还完全相同。此时我们很容易想到的就是当再加入一个新的switch case的时候,我们除了要加一个新方法之外,还要对现成的GreetPeople方法进行修改,这违反了开闭原则(对修改关闭)。有没有一种方法,可以在不修改GreetPeople方法的前提下对程序进行扩展呢?
public class Program { public static void Main() { GreetPeople("Alex", "Chinese"); GreetPeople("Beta", "English"); GreetPeople("Clara", "France"); Console.ReadKey(); } public static void GreetPeople(string name, string lang) { switch (lang) { case "English": EnglishGreeting(name); break; case "Chinese": ChineseGreeting(name); break; case "France": FrenchGreeting(name); break; } } public static void EnglishGreeting(string name) { Console.WriteLine("Morning, " + name); } public static void ChineseGreeting(string name) { Console.WriteLine("早上好, " + name); } public static void FrenchGreeting(string name) { Console.WriteLine("Bonjour, " + name); } }
首先,我们要放弃使用switch,否则我们终究避免不了修改GreetPeople方法的命运。之后,我们自然而然的会想,假设我们在主函数里面传入的第二变量不是字符串,而是方法名,那么似乎我们就不需要那个switch了。因为我们会直接去到对应的方法,不用switch再分派过去。那么这件事该怎么实现呢?传入方法名到底意味着什么呢?这些方法的签名全都一样,我是否可以用某种手法将他们封装起来呢?
于是,委托就出现了,它可以解决上面我们所有的问题。委托代表了一类具有相同签名的方法,可以变身为其中任何一个。委托也可以作为变量传入方法,其行为和其他类型例如int,string完全一样。很多人觉得委托很不好理解,是因为委托代表的是方法,而普通类型代表的都是值或者对象。比如string,其可以代表任何的字符串,int也是可以代表在某个取值范围中任何的整数一样。委托则代表着某一类方法(视其定义而定),当某个函数的其中一个变量是委托时,意味着我们将要传入一个可以被该委托所代表的方法名。委托是方法的指针,可以指向不同的方法,类比一下,如同string可以指向堆上的字符串,int可以指向栈上的整数一样。
public class Program { //现在这个委托代表了一类输入一个字符串,没有输出的方法 public delegate void GreetPeopleDelegate(string name); public static void Main() { //利用委托,传入不同的方法会得到不同的结果 GreetPeople("Alex", ChineseGreeting); GreetPeople("Beta", EnglishGreeting); GreetPeople("Clara", FrenchGreeting); Console.ReadKey(); } //委托可以作为方法的变量,从而代替switch public static void GreetPeople(string name, GreetPeopleDelegate aGreetPeopleDelegate) { aGreetPeopleDelegate(name); } public static void EnglishGreeting(string name) { Console.WriteLine("Morning, " + name); } public static void ChineseGreeting(string name) { Console.WriteLine("早上好, " + name); } public static void FrenchGreeting(string name) { Console.WriteLine("Bonjour, " + name); } }
委托的方法和属性
1. MulticastDelegate(委托自己所在的密封类)
小写的delegate是你用来声明委托的关键字,当你声明完之后,编译器创建一个新的密封类,该类的类型是MulticastDelegate(继承自System.MultipleDelegate,其再继承自System.Delegate)这就是大写的和小写d的delegate关键字的区别。
这个新的密封类定义了三个方法,invoke, begininvoke和endinvoke。invoke是当你调用委托所代表的方法时隐式执行的,例如aGreetPeopleDelegate(name)实际上和aGreetPeopleDelegate.Invoke(name)没有区别。所以Invoke的方法签名永远和委托本身相同,即如果某委托签名为int a(int x, int y)则它的invoke签名一定是public int Invoke(int x, int y)。
后两者则赋予委托异步的能力。这两个方法放到多线程系列中进行分析。
2. System.MultipleDelegate和委托的调用列表(方法链)
System.MultipleDelegate中重要的方法GetInvocationList()获得当前委托所代表的方法的各种信息。注意这个方法返回的是一个数组,这也就是说,委托可以同时代表多个方法(此时,invoke委托会将该组方法顺序一个一个执行),这也叫做委托的多路广播。通过+=和-=,我们可以为委托增加和减少方法。我们无需深入研究方法链是如何实现的,但以下几个事情需要知道:
1. 可以重复增加相同的方法,此时该方法将执行两次
2. 可以删除委托所有的方法,即委托可以暂时不代表方法,此时invoke委托将什么都不发生
3. 即使不小心多删除了方法一次,也不会出现异常(如增加了一个方法然后误删除了两次),此时委托暂时不代表任何方法
4. +=和-=是操作符的重载,本质是调用System.Delegate中的Combine和Remove方法
System.MultipleDelegate还重载了==和!=,判断两个委托是否相等仅仅看它们代表的方法链是否相等(即都是指向相同对象上的相同方法)。
3. System.Delegate
System.Delegate中有两个重要的公共成员target和method。其中method代表方法的信息,而如果Method代表一个静态成员,则Target为null,否则,target代表方法所在的对象。通过GetInvocationList()我们可以查看当前委托中方法链的信息。另外这个类还有Combine和Remove方法,其已经被子类重载故不需要直接调用他们。
public class Program { public delegate void GreetPeopleDelegate(string name); public static void Main() { //实例化委托一定要为其指派一个符合要求的方法 GreetPeopleDelegate aGreetPeopleDelegate = new GreetPeopleDelegate(ChineseGreeting); PrintInvocationList(aGreetPeopleDelegate.GetInvocationList()); //增加一个方法 aGreetPeopleDelegate += EnglishGreeting; PrintInvocationList(aGreetPeopleDelegate.GetInvocationList()); anotherClass a = new anotherClass(); //增加一个非静态方法 aGreetPeopleDelegate += a.NonStaticGreeting; PrintInvocationList(aGreetPeopleDelegate.GetInvocationList()); Console.ReadKey(); } //观看当前委托中代表的方法链 public static void PrintInvocationList(Delegate[] aList) { foreach (var delegateMethod in aList) { //Method代表当前维护的方法的详细信息 //如果Method代表一个静态成员,则Target为null,否则,target代表方法所在的对象 Console.WriteLine(string.Format("Method name: {0}, value: {1}", delegateMethod.Method, delegateMethod.Target)); } Console.WriteLine("------------------------------------"); } public static void EnglishGreeting(string name) { Console.WriteLine("Morning, " + name); } public static void ChineseGreeting(string name) { Console.WriteLine("早上好, " + name); } public static void FrenchGreeting(string name) { Console.WriteLine("Bonjour, " + name); } } public class anotherClass { public void NonStaticGreeting(string name) { Console.WriteLine("Bonjour, " + name); } }
动态维护委托的调用列表
上面说了委托都是有一个调用列表的,我们可以动态的操作他,为他添加或者删除成员。如果我们创建一个公共的委托成员列表,则可以很容易的实现多路广播。下面例子来自精通c#第六版。其中调用列表
public CarEngineHandler methodList;
是公共的,并且外部方法main会创建一个新的实例作为订阅者,在适当情形下,调用委托然后执行委托列表中的方法。
public class Program { public static void Main() { //创建了一个新的订阅者 var c = new Car("Mycar", 0, 100); //该订阅者(消费者)订阅了方法OnCarEvent1 c.methodList += OnCarEvent1; //取消注释实现多路广播,此时将会执行两个方法 //c.methodList += OnCarEvent2; for (int i = 0; i < 10; i++) { c.Accel(20); } Console.ReadKey(); } public static void OnCarEvent1(string msg) { Console.WriteLine("***** message from car *****"); Console.WriteLine("=> " + msg); Console.WriteLine("****************************"); } public static void OnCarEvent2(string msg) { Console.WriteLine("=> " + msg.ToUpper()); } } public class Car { public string name { get; set; } public int currentSpeed { get; set; } public int MaxSpeed { get; set; } private bool isDead { get; set; } public delegate void CarEngineHandler(string message); public CarEngineHandler methodList; public Car(string name, int currentSpeed, int MaxSpeed) { this.name = name; this.currentSpeed = currentSpeed; this.MaxSpeed = MaxSpeed; this.isDead = false; } public void Accel(int delta) { //死亡时执行订阅列表中的方法 if (isDead) { if (methodList != null) methodList("Sorry, car is broken"); } else { currentSpeed += delta; if (currentSpeed >= MaxSpeed) isDead = true; else Console.WriteLine("Current speed: " + currentSpeed); } } }
从委托到事件
上个例子中的委托有一个问题,就是其不够安全。调用者可以直接访问委托对象CarEngineHandler,并且还能对其调用列表:
1 invoke,即可以随时使用委托
2 +=或者-=,甚至直接赋值(=)也可以
有时候,我们并不希望用户可以更改委托的成员。而且,我们希望委托不能被用户Invoke,而是在特定的时候被委托的订阅者调用。也就是说我们希望下面两句代码都不通过编译:
//为委托赋以一个全新的对象(我们不希望其他代码可以改变委托指向)c.methodList = OnCarEvent1; //直接调用委托(我们不希望其他代码可以直接调用,除非经过许可)c.methodList.Invoke("test");
此时,一个自然的想法就是将委托本身定义为private,但如果这样做,外部的所有类都无法使用该委托。所以我们还要搞若干公共的方法,作为外部类使用内部私有委托的桥梁。下面代码中,methodList是私有的所以我们不能直接对他操作,我们要通过Car类的两个公共方法操作他。(无关的代码已省略)
public class Program { public static void Main() { //创建了一个新的订阅者 var c = new Car("Mycar", 0, 100);
c.Addmethod(OnCarEvent1); c.Invoke("test"); } public class Car {public delegate void CarEngineHandler(string message); private CarEngineHandler methodList; public CarEngineHandler Addmethod(CarEngineHandler aMethod) { methodList += aMethod; return methodList; } public void Invoke(string msg) { methodList.Invoke(msg); } }
但问题就来了,那对于所有的委托,如果我们要追求安全,岂不是都要弄这些方法,而且方法还比较多,有添加方法,删除方法,方法的同步和异步的调用等。这看上去非常麻烦,要打很多的代码。相信这时候你也想到了,又有一个强大的东西要出场了,它可以解决上面所有的问题,它就是事件。
委托和事件 (1) - 委托简析