首页 > 代码库 > 委托和事件 (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) - 委托简析