[C#] 委托与事件(1)

网上讲C#委托和事件的博文已经非常多了,其中也不乏一些深入浅出、条理清晰的文章。我之所以还是继续写,主要是借机整理学习笔记、归纳总结从而理解更透彻,当然能够以自己的理解和思路给其他人讲明白更好。
另外,太长的文章会让很多读者失去兴趣,所以我决定把这篇分成四个部分来介绍。分别是委托的基础、委托的进阶、事件的基础和事件的进阶。对使用委托与事件要求不高的同学可以跳过进阶部分。

首先,本小节我们来介绍一下委托最最基础的部分,在列举这些基础知识之前,我们先从实例出发看看为什么要使用委托,以及什么情况下需要使用委托。


1. 为什么要使用委托?

假设我们有一个这样的需求,写一个MakeGreeting函数,这个函数在被调用的时候需要告诉它两点:跟谁greet怎么greet
我们的第一反应可能是,很简单呀,给这个函数传两个参数,就传跟谁greet怎么greet。如果怎么greet只是一个string,当然可以这样做,可万一它们没那么简单呢?
继续假设,假设怎么greet只有两种情况HelloGoodbye,分别是下面代码中的两个函数。(虽然函数里只写了一句输出,但是我们假设它们还要做一些其他事情,只是没有写出来而已。要不然可能有人会疑问为什么要搞这么复杂啦)

根据上面的需求描述,完成第一版程序:

namespace TestDelegate
{
    public enum Greeting
    {
        Hello, Goodbye
    }
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            // do something (hug or shake hand...)
        }
        public static void Goodbye(string s)
        {
            Console.WriteLine("  Goodbye, {0}!", s);
            // do something (hug or wave hand...)
        }
        static void MakeGreeting(string name, Greeting greeting)
        {
            switch (greeting)
            {
                case Greeting.Hello: Hello(name); break;
                case Greeting.Goodbye: Goodbye(name); break;
            }
        }
        static void Main(string[] args)
        {
            MakeGreeting("May", Greeting.Hello);
            MakeGreeting("April", Greeting.Goodbye);
        }
    }
}

输出内容:

 Hello, May!
 Goodbye, April! 

这样写当然是可以的,只是扩展性并不好,如果需要再加更多的Greeting就需要改三个地方:(1) 新增Greeting相关的方法、(2) Greeting枚举里添加值、(3) 在MakeGreeting函数的switch语句里添加对新增Greeting的处理。
也就是说每增加一个Greeting方法时,还需要增加枚举并在MakeGreeting里面把新增的方法与枚举值关联起来。

那么问题来了,我们可不可以直接把Greeting方法(如Hello, Goodbye)传进MakeGreeting函数里呢?像C++里的函数指针那样。这样就不需要Greeting枚举,也不需要在MakeGreeting函数里面进行switch选择了。
答案当然是可以的,委托就可以是一系列类似方法(这里类似是指参数值列表和返回值可以用一个模板表示出来)的类,它的对象就是不同的方法,所以可以用委托把这一系列Greeting方法(对象)的共性(类)定义出来,然后给MakeGreeting函数传递一个该委托(共性类)的对象(就是一个Greeting方法)。

于是,就有了下面利用委托来完成上述需求的第二版程序:

namespace TestDelegate
{
    delegate void GreetingDelegate(string s); //声明委托,定义Greeting方法的类
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            // do something (hug or shake hand...)
        }
        public static void Goodbye(string s)
        {
            Console.WriteLine("  Goodbye, {0}!", s);
            // do something (hug or wave hand...)
        }
        static void MakeGreeting(string name, GreetingDelegate d)
        {
            d(name);
        }
        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello; //定义委托的一个对象(将方法绑定到委托)
            GreetingDelegate d2 = Goodbye; //定义委托的另一个对象
            MakeGreeting("May", d1);
            MakeGreeting("April", d2);
        }
    }
}

输出内容:

 Hello, May!
 Goodbye, April! 

小结:如何实现一个委托
(1) 声明一个delegate对象,它与我们想要定义的一系列方法具有相同的参数和返回值类型。如:
public delegate void GreetingEventHandler(string name)
(2) 委托的实例化。创建delegate对象,并将我们想要使用的方法绑定到委托。(下面会在基础知识里面细讲委托实例化与类实例化的区别,以及讲方法绑定到委托的不同方法)
(3) 使用委托。使用委托中绑定的方法时,直接传递绑定有该方法的委托对象,或者直接通过委托对象调用绑定的方法。
例如:上例中我们可以把绑定有Hello方法的委托对象d1传递给MakeGreeting函数,在函数内实现方法的调用。还可以直接通过d1来调用方法,即d1("May");会直接输出Hello, May!

2. 什么情况下使用委托?

上面的例子已经给出了一种情况,就是我们需要在运行时动态地确定具体的调用方法。其实这种简单的情况用接口也可以实现,因为接口也是一系列相似方法的抽象,类的继承与多态也可以实现运行时调用不同的方法。所以像上例中的情况下,何时该用委托呢?

a) 当使用事件设计模式时。

就是Observer设计模式,它定义了对象之间一对多的关系,并且通过事件触发机制关联它们。当一个对象中发生了某事件后,依赖它的其他对象会被自动触发并更新。在事件部分我们会更细致地介绍。

b) 当需要封装静态方法时。

委托绑定的方法可以是静态方法、非静态方法和匿名方法,而C#中接口不能是静态的。

c) 当调用方不需要访问实现该方法的对象中的其他属性、方法或接口时。

我们可以把一个类中的某个成员函数绑定到委托,调用该方法时只与这个成员函数有关,与该类里的其他属性无关。

d) 当需要方便的组合时。

一个委托绑定的多个方法可以自由组合。一个委托的对象可以绑定多个方法,而且这多个方法是没有限制可以任意组合的,委托灵活的绑定和解绑定策略使得使用非常方便。

e) 当类可能需要该方法的多个实现时。

一个委托的对象可以绑定多个方法,当我们运行时需要的不是单一的方法时,接口很难实现。

举例说明使用委托发送对象状态通知。我直接以《精通C#》中的一个例子来说明这种用法。
我们有一个Car类型,在Car中定义一个委托并封装一个委托的对象(这可以用事件实现,目前先这样写,实际上这样做是不对的)。然后我们通过委托来向外界发送对象状态的通知。

namespace TestDelegate2
{
    public class Car 
    {
        public int CurrentSpeed { get; set; }
        public int MaxSpeed { get; set; }
        public string PetName { get; set; }
        public Car() { MaxSpeed = 100; }
        public Car(string name, int maxSp, int currSp)
        {
            CurrentSpeed = currSp;
            MaxSpeed = maxSp;
            PetName = name;
        }
        // declare a delegate type
        public delegate void CarEngineHandler(string message);
        // Create a new delegate object*
        private CarEngineHandler listOfHandlers;
        // associate with method*
        public void RegisterWithCarEngine(CarEngineHandler methodToCall)
        {
            listOfHandlers += methodToCall;
        }
        public void Accelerate(int delta)
        {
            if(CurrentSpeed >= MaxSpeed)
            {
                if(listOfHandlers != null)
                {
                    listOfHandlers("Error: Current speed is greater than the max speed!");
                }
            } else {
                CurrentSpeed += delta;
                Console.WriteLine("Current speed is : {0}", CurrentSpeed);
                if (MaxSpeed - CurrentSpeed <= 10 && listOfHandlers != null)
                {
                    listOfHandlers("Warning: Current speed is closing to the max speed!");
                }
            }            
        }
    }
    class Program
    {
        public static void OnCarEngineEvent(string message)
        {
            Console.WriteLine("=> {0}", message);
        }
        static void Main(string[] args)
        {
            Car c = new Car("Test", 100, 10);
            c.RegisterWithCarEngine(new Car.CarEngineHandler(OnCarEngineEvent));
            for (int i = 0; i < 6; ++i)
            {
                c.Accelerate(20);
            }
            Console.ReadLine();
        }
    }
}

输出:

Current speed is : 30
Current speed is : 50
Current speed is : 70
Current speed is : 90
=> Warning: Current speed is closing to the max speed!
Current speed is : 110
=> Warning: Current speed is closing to the max speed!
=> Error: Current speed is greater than the max speed!

3. 委托的基础知识

(1) 委托所实现的功能与C/C++中的函数指针十分相似。Using a delegate allows the programmer to encapsulate a reference to a method inside a delegate object. 从实际使用的角度来看,委托的作用是将方法作为方法的参数。
(2) 与C/C++中函数指针的不同:函数指针只能指向静态函数,而委托既可以引用静态函数,又可以引用非静态成员函数;与函数指针相比,委托是面向对象、类型安全、可靠的受控(managed)对象。
(3) 委托的声明。Delegates run under the caller's security permissions, not the declarer's permissions.
(4) 委托的实例化。委托可以像类一样直接定义对象,也可以通过关键字new创建新的对象。
(5) 将方法绑定到委托。可以直接采用赋值符号=,也可以在new的时候将方法名作为创建委托对象的参数,但与类不同的是a)委托对象一旦创建就要绑定方法,不能创建空的委托对象,b)委托可以通过+=来绑定多个方法,还可以通过-=来解除对某个方法的绑定。
(6) 对于绑定了多个方法的委托,在调用时会依次调用所有绑定的方法。一旦出现异常会终止方法列表中后面的方法的调用。

参考文献:

  1. 《精通C#》
  2. 张子阳的《C# 中的委托和事件》
  3. C# 中的委托和事件
  4. Delegates Tutorial
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,194评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,058评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,780评论 0 346
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,388评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,430评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,764评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,907评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,679评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,122评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,459评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,605评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,270评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,867评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,734评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,961评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,297评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,472评论 2 348

推荐阅读更多精彩内容