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

643 查看

网上讲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

2018 - 知识虫 - 我的知识库 渝ICP备16002641号-2

渝公网安备 50010702501581号