[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) 对于绑定了多个方法的委托,在调用时会依次调用所有绑定的方法。一旦出现异常会终止方法列表中后面的方法的调用。

4. 委托的高级知识

(1) 委托的类组成

前面我们介绍过了,委托实际上也是一个类,只不过它的对象不是一个普通的变量,而是一个方法。但是我们实际使用时并不需要定义这个类,而只是声明一下委托即可。这是因为当C#编译器处理委托类型时,会自动产生一个派生自System.MulticastDelegate的密封类,这个类与它的基类System.Delegate一起为委托提供必要的基础设施。
下面我们就以上一节中声明的委托delegate void GreetingDelegate(string s);来看看该类的组成。

public sealed class GreetingDelegate : System.MulticastDelegate
{
    public void Invoke(string s);
    public IAsyncResult BeginInvoke(string s, AsyncCallback cb, object state);
    public void EndInvoke(IAsyncResult result);
}

可以看到,该类中定义了三个公共方法:
(a) Invoke(),它被用来以同步方式调用委托对象维护的每个方法。所谓同步是指调用者必须等待调用完成才能继续执行。Invoke()方法定义的参数和返回值完全匹配我们要定义的类GreetingDelegate。另外,Invoke()不能直接调用,而是在后台调用。
(b) BeginInvoke(),用于异步调用,它最前面的参数列表是GreetingDelegate定义的方法类的参数列表,此外,还有两个参数AsyncCallbackobject用于异步方法调用。
(c) EndInvoke(),与BeginInoke()联合用于异步调用,它的返回值与委托声明的返回值一致,而它的唯一参数则是BeginInvoke()返回的类型IAsyncResult接口。

另外,上面我们定义的委托没有返回值,也可以定义返回值类型,这样Invoke()EndInvoke()对应的返回值就不是void了。委托还可以指向包含任意数量out或者ref参数的方法,按道理只有Invoke()BeginInvoke()方法与委托的参数列表有关,需要加上相应的out或者ref参数,但是由于异步调用时需要通过EndInvoke()来返回结果,所以EndInvoke()的参数列表中需要加上out或者ref参数。

下面简单介绍一下委托类的父类System.MulticastDelegate

public abstract class MulticastDelegate : Delegate
{
    // 返回所指向的方法列表
    public sealed override Delegate[] GetInvocationList();
    // 重载等于和不等于操作符
    public static bool operator ==(MulticastDelegate d1, MulticastDelegate  d2);
    public static bool operator !=(MulticastDelegate d1, MulticastDelegate d2);

    // 用来在内部管理委托所维护的方法列表
    private IntPtr _invocationCount;
    private object _invocationList;
}
public abstract class Delegate : IConeable, ISerializable
{
    // 与函数列表交互的方法
    // 给委托维护的列表添加一个方法,在C#中使用重载+=操作符调用此方法
    public static Delegate Combine(params Delegate[] delegates);
    public static Delegate Combine(Delegate a, Delegate b);
    // 从调用列表中移除一个或所有的方法,在C#中使用-=操作符调用此方法
    public static Delegate Remove(Delegate source, Delegate value);
    public static Delegate RemoveAll(Delegate source, Delegate value);

    // 重载操作符
    public static bool operator ==(Delegate d1, Delegate d2);
    public static bool operator !=(Delegate d1, Delegate d2);
 
    // 扩展委托目标的属性
    public MethodInfo Method { get; } //用以表示委托维护的静态方法的详细信息
    public object Target { get; } // 如果方法调用是定义在对象级别的,
                                  // Target返回表示委托维护的方法的对象;
                                  // 如果调用的方法时一个静态成员,
                                  // Target返回null
}

(2) 泛型委托

C#允许我们定义泛型委托,即当我们定义的委托接受的参数可能会不同时,我们可以通过类型参数来构建。下面我们来改写一下上一节中定义的那个委托,使得它不仅支持传入string,还支持传入整型。

namespace TestDelegate
{
    delegate void GreetingDelegate<T>(T arg);
    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...)
        }

        public static void GreetingTimes(int n)
        {
            Console.WriteLine("  Greeting {0} times!", n); 
        }

        static void MakeGreeting<T>(T name, GreetingDelegate<T> greeting)
        {
            greeting(name);
        }

        static void Main(string[] args)
        {
            GreetingDelegate<string> d1 = Hello;        //定义委托的一个对象(将方法绑定到委托)
            d1 += Goodbye;                              // 在d1上再绑定一个委托
            GreetingDelegate<int> d2 = GreetingTimes;   //定义委托的另一个对象
            MakeGreeting("April", d1);
            MakeGreeting(99, d2);
        }
    }
}

输出内容:

  Hello, April!
  Goodbye, April!
  Greeting 99 times!

有了泛型委托,很多方法都可以用一个委托模板表示出来,因此C#中提供了两个常用的泛型委托Action<T>Func<T>来避免用户手工构建自定义委托的麻烦。

Action<T>

Action<T>泛型委托定义的方法,参数列表可以多至16个(使用时需要指定各个参数的类型),返回值为void
例如,我们有一个方法为static void DisplayMessage(string msg, ConsoleColor txtColor, int printCount),若把它作为Action<T>委托的一个目标,则委托的实例化时需要这样写:
Action<string, ConsoleColor, int) actionTarget = new Action<string, ConsoleColor, int>(DisplayMessage);
调用委托方法时:
actionTarget("your input string", ConsoleColor.Green, 2);

Func<T>

Func<T>泛型委托也可以指向多至16个参数的方法,但与Action<T>不同的是,它具有自定义的返回值,具体用法类似,不再赘述。

(3) 委托的异步调用

在说委托的异步调用之前,我们先对第一节最早的delegate例子做一个简单的改进,看看它的工作流程。首先,MakeGreeting()方法中除了调用greeting()之外,需要再调用一个额外的方法FuncAfterGreeting()。然后,我们定义了GreetingDelegate的一个对象d1,并在d1上绑定了Hello()Goodbye()方法。最后我们调用MakeGreeting()方法来看看输出结果。

namespace AsyncDelegateTest
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 1 second");
            Thread.Sleep(1000);
            Console.WriteLine("  Finished Hello");
        }

        public static void Goodbye(string s)
        {
            Console.WriteLine("  Goodbye, {0}!", s);
            Console.WriteLine("  Waiting for 2 second");
            Thread.Sleep(2000);
            Console.WriteLine("  Finished Goodbye");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            greeting(name); // 这里相当于是greeting.Invoke(name);
            FuncAfterGreeting();
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;     //定义委托的一个对象(将方法绑定到委托)
            d1 += Goodbye;                  //定义委托的另一个对象
            MakeGreeting("April", d1);
        }
    }
}

输出结果:

  Hello, April!
  Waiting for 1 second
  Finished Hello
  Goodbye, April!
  Waiting for 2 second
  Finished Goodbye
  Do some other things...
  Waiting for 3 second
  Finished FuncAfterGreeting

从输出我们可以看到MakeGreeting()方法中,首先调用了Hello()方法,并运行完毕;然后调用了Goodbye()方法,并运行完毕;最后调用FuncAfterGreeting(),并运行完毕;至此,整个MakeGreeting()方法运行完毕。
这就是采用同步的方式调用委托,这样委托对象绑定的每个方法要依次执行,而且后者必须等前者执行完毕之后才能开始执行。另外,只有把委托对象绑定的所有方法执行完毕后才能回到MakeGreeting()方法中继续往下执行。

而在(1)中我们介绍的BeginInvoke()EndInvoke()函数能使委托实现异步调用,所谓异步调用,就是在上例中MakeGreeting()方法中的线程去执行greeting方法时利用线程池中的线程去实现调用,自己则继续往下执行。有了BeginInvoke()EndInvoke()这两个函数后,异步调用就很简单了,直接先用greeting调用BeginInvoke()函数,然后就可以做其他的事情,结束之间再调用EndInvoke()即可。

namespace AsyncDelegateTest2
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished Hello");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            IAsyncResult result = greeting.BeginInvoke(name, null, null);
            FuncAfterGreeting();
            greeting.EndInvoke(result);
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;     //定义委托的一个对象(将方法绑定到委托)
            MakeGreeting("April", d1);
        }
    }
}

输出结果:

  Do some other things...
  Hello, April!
  Waiting for 3 second
  Waiting for 3 second
  Finished Hello
  Finished FuncAfterGreeting

为什么我这里把Goodbye方法去掉了,这是因为BeginInvoke()只能在绑定了单个方法的delegate上调用,如果我们在d1上还绑定了其他方法,那么去调用BeginInvoke()的时候会出现下面的异常:

Unhandled Exception: System.ArgumentException: The delegate must have only one target.

当然如果你一定要绑定多个方法这样用的话,可以先通过GetInvocationList()获得绑定的方法列表,然后依次调用BeginInvoke()方法。

namespace AsyncDelegateTest3
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished Hello");
        }

        public static void Goodbye(string s)
        {
            Console.WriteLine("  Goodbye, {0}!", s);
            Console.WriteLine("  Waiting for 2 second");
            Thread.Sleep(2000);
            Console.WriteLine("  Finished Goodbye");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            Delegate[] delArray = greeting.GetInvocationList();
            foreach (var d in delArray)
            {
                var del = (GreetingDelegate)d;
                IAsyncResult result = del.BeginInvoke(name, null, null);
            }
            FuncAfterGreeting();
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;     // 定义委托的一个对象(将方法绑定到委托)
            d1 += Goodbye;                  // 定义委托的另一个对象
            MakeGreeting("April", d1);
            Console.ReadLine(); // 如果不加这行的话,很可能Hello方法还没执行完程序就退出了,
                                // 因为我们没有调用EndInvoke()去检查它们的状态
        }
    }
}

输出结果:

  Do some other things...
  Hello, April!
  Goodbye, April!
  Waiting for 2 second
  Waiting for 3 second
  Waiting for 3 second
  Finished Goodbye
  Finished FuncAfterGreeting
  Finished Hello

回到AsyncDelegateTest2,这里其实还存在一个问题。那就是MakeGreeting()方法中的这句话greeting.EndInvoke(result);,如果Hello()方法需要执行30s,那么3s后FuncAfterGreeting()方法就执行完毕了,主线程执行到EndInvoke()这句话。而这句话就相当于让主线程一直去查询Hello()方法是否执行完毕。那么问题来了,能不能不要这么麻烦主线程,而是让Hello()方法执行完毕后自动告诉主线程呢?这就是异步回调。

namespace AsyncDelegateTest4
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 5 second");
            Thread.Sleep(5000);
            Console.WriteLine("  Finished Hello");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            IAsyncResult result = greeting.BeginInvoke(name, new AsyncCallback(FuncForCallBack), "AsycState:OK");
            FuncAfterGreeting();
        }

        static void FuncForCallBack(IAsyncResult result)
        {
            // AsyncResult should using System.Runtime.Remoting.Messaging
            GreetingDelegate handler = (GreetingDelegate)((AsyncResult)result).AsyncDelegate;
            handler.EndInvoke(result);
            Console.WriteLine(result.AsyncState);
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;
            MakeGreeting("April", d1);
            Console.ReadLine();
        }
    }
}

输出结果:

  Do some other things...
  Hello, April!
  Waiting for 5 second
  Waiting for 3 second
  Finished FuncAfterGreeting
  Finished Hello
  AsycState:OK

这里我们定义了一个回调函数FuncForCallBack(),这样就不需要在MakeGreeting()方法的最后显示地去调用EndInvoke()去检查委托方法的执行状态了。

5. 什么是事件?怎么写一个事件?

An event in C# is a way for a class to provide notifications to clients of that class when some interesting thing happens to an object.

所以这里的事件,是指当某件有意思的事情发生时一个类给它的客户端提供通知的方法。比如说电热壶的温度被警报器订阅了,当温度达到某一高度时就会触发事件通知警报器发出报警声音。

对,关于什么是事件就是这么简单。那么,怎么写一个事件呢?
定义一个事件需要两步:
(1) 定义一个委托类型,它包含在事件触发时需要调用的方法。
(2) 通过C# event关键字用相关委托声明这个事件。
就以上面我们举得例子来写一个简单的事件吧。

namespace MyFirstEvent
{
    public delegate void BoilHandler(int p); //声明委托

    public class Heater 
    {
        private int temperature;
        public event BoilHandler BoilEvent; //声明事件

        public void BoilWater()
        {
            for (int i = 0; i <= 100; ++i)
            {
                temperature = i;
                if (temperature >= 95)
                {
                    //调用在该事件上注册的方法
                    if (BoilEvent != null) BoilEvent(temperature);
                }
            }
        }
    }

    public class Alarm
    {
        public void MakeAlert(int t)
        {
            Console.WriteLine("The temperature of water is {0} ℃", t);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Heater h = new Heater();
            Alarm a = new Alarm();

            h.BoilEvent += a.MakeAlert; //注册方法
            h.BoilWater();
        }
    }
}

输出结果:

The temperature of water is 95 ℃
The temperature of water is 96 ℃
The temperature of water is 97 ℃
The temperature of water is 98 ℃
The temperature of water is 99 ℃
The temperature of water is 100 ℃

6. 为什么要用事件?

从上面的例子看来事件和委托差不多嘛,上面的事情用委托完全可以实现,跟我们之前将的例子类似,把上面声明的事件对象换成委托对象不就可以了吗?为什么又大费周折地引入事件这么个东西呢?

那我们就来看看直接使用委托对象有什么问题吧。如果我们没有把委托成员变量定义为私有的,调用者就可以直接访问委托对象。这样,调用者不仅可以直接查看委托对象上绑定的所有方法,还可以随意地把变量重新赋值为新的委托对象。也就是说,在类中写公共的委托成员会打破类的封装,不仅会导致代码难以维护和调试,还会带来应用程序的安全风险。不要问我为什么不把委托成员声明称私有的,如果声明成私有的,那怎么在外面把客户端的方法注册到委托呢?

既然直接用委托成员存在破坏封装的问题,那为什么事件可以解决这个问题呢?
C#event关键字在编译的时候会自动提供注册和注销方法以及任何必要的委托类型成员的变量。这些委托成员变量总是声明为私有的,所有触发事件的对象不能直接访问它们。

namespace MyFirstEvent
{
    public delegate void BoilHandler(int p);

    public class Heater 
    {
        private int temperature;
        public BoilHandler BoilEvent;

        public void BoilWater()
        {
            for (int i = 0; i <= 100; ++i)
            {
                temperature = i;
                if (temperature >= 95)
                {
                    if (BoilEvent != null) BoilEvent(temperature);
                }
            }
        }
    }

    public class Alarm
    {
        public void MakeAlert(int t)
        {
            Console.WriteLine("The temperature of water is {0} ℃", t);
        }
    }

    static public class SomethingShouldNotHappen
    {
        static public void Display(int t)
        {
            Console.WriteLine("Test temperature is {0} ℃", t);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Heater h = new Heater();
            Alarm a = new Alarm();

            h.BoilEvent += a.MakeAlert;

            // get invocation list and replace it
            foreach (var d in h.BoilEvent.GetInvocationList())
            {
                Console.WriteLine(d);
            }
            h.BoilEvent = SomethingShouldNotHappen.Display;

            h.BoilWater();
        }
    }
}

输出结果:(你看,我们轻易地看到了本不该看到的invocation list,并且对委托成员上绑定的方法进行了篡改)

如果我们用上面的event呢?
首先,MyFirstEvent.Heater.BoilEvent不提供公开的GetInvocationList方法。实际上它什么公开的方法都不提供,你如果在VS里面输入h.BoilEvent.,你会发现VS根本就不会有任何方法提示。
其次,event扩展了两个隐藏的公共方法,一个带add_前缀,一个带remove_前缀。使用的时候可以用h.BoilEvent += x或者h.BoilEvent -= x,你想直接用=是不可能编译通过的,下面是错误信息。

Error   1   The event 'MyFirstEvent.Heater.BoilEvent' can only appear on the left hand side of += or -= (except when used from within the type 'MyFirstEvent.Heater')   c:\users\xxx\documents\visual studio 2012\Projects\MyFirstEvent\MyFirstEvent\Program.cs 54  15  MyFirstEvent

7. 事件的高级知识

(1) 揭开事件的神秘面纱

回到上一节中我们声明的事件public event BoilHandler BoilEvent;,这里的事件虽然被声明为公开的,但上一节最后我们试图对h.BoilEvent进行赋值操作会出现编译错误。为什么会这样呢?看看编译后的BoilEvent你就明白了。

private BoilHandler BoilEvent; // 事件实际上被声明为私有的委托变量

[MethodImpl(MethodImplOptions.Synchronized)]
public void add_BoilEvent(BoilHandler value)
{   // 在事件上绑定方法实际上是间接调用Delegate.Combine()
    this.BoilEvent= (BoilHandler) Delegate.Combine(this.BoilEvent, value);
}

[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_BoilEvent(BoilHandler value)
{   // 在事件上解除绑定方法实际上是间接调用Delegate.Remove()
    this.BoilEvent = (BoilHandler) Delegate.Remove(this.BoilEvent, value);
}

上面的代码进一步说明,实际上事件就是封装了的委托,而且我们只需要键入event关键字,编译器就可以帮我们把相关的方法自动生成。另外,代表事件本身的CIL代码将使用.addon.removeon指令对应要调用的add_XXX()remove_XXX()方法的名称。

.event MyFirstEvent.Heater/BoilHandler BoilEvent
{
    .addon instance void MyFirstEvent.Heater::add_BoilEvent(class MyFirstEvent.Heater/BoilHandler)
    .removeon instance void MyFirstEvent.Heater::remove_BoilEvent(class MyFirstEvent.Heater/BoilHandler)
}

(2) 重申事件的必要性

我们在第6节从封装性的角度分析了“为什么要用事件”,上面部分又说明了实际上event就是封装的delegate,所以事件在易用性上又占了上风,因为event关键字能够帮助我们节省很多代码的键入。另外,张子阳又从设计模式的角度解释了“为什么要使用事件而不是委托”。我觉得很有道理,所以将借用他的例子来重申一下他的观点。

首先我们定义一个带有一个int参数的委托NumberChangedEventHandler,然后再定义一个发布者和订阅者。其中发布者类中可以声明上述委托的变量或者对应的事件,发布者通过DoSomething()方法来执行委托变量或事件上绑定的方法。而订阅者类中则声明了订阅的方法即实际上应该绑定到委托的方法。所以,用通俗的语言来解释就是,订阅者订阅了某种消息,发布者在它的某种行为中满足了某种条件后触发某件事情的发生,而这件事情的发生正是订阅者订阅的东西,所以发布者触发某事件后即会通知到所有的订阅者。

(a) 如果声明委托变量,那么在Main函数也就是客户端中可以通过pub.NumberChanged(100);来触发事件,这不满足事件由发布者在其内部的某种行为下触发的设计模式。
(b) 如果声明为事件,则它的封装性限制了客户端的行为(直接调用pub.NuberChanged会出现编译错误),从而保证了事件只能供其他类型订阅。

namespace PubSub
{
    public delegate void NumberChangedEventHandler(int count); //定义委托

    public class Publishser
    {
        private int count;
        //public NumberChangedEventHandler NumberChanged;     // 声明委托变量
        public event NumberChangedEventHandler NumberChanged; // 声明一个事件

        public void DoSomething()
        {
            // todo: do something

            if (++count > 0)
            {
                if (NumberChanged != null)
                {
                    NumberChanged(count);
                }
            }
        }
    }

    public class Subscriber
    {
        public void OnNumberChanged(int count)
        {
            Console.WriteLine("Subscriber notified: count = {0}", count);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            Subscriber sub = new Subscriber();

            pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);
            pub.DoSomething();          // 通过Pub DoSomething()来触发事件
            //pub.NumberChanged(100);   // 委托变量可以被这样直接(不恰当地)调用,但事件不行
        }
    }
}

(3) 自定义事件的参数与返回值

(a) 事件的参数

微软推荐的事件模式带有两个参数objectEventArgs,其中object参数表示一个队发送事件的对象的引用,EventArgs则表示与该事件相关的信息,它可以是表示不发送任何信息的基类EventArgs,也可以是派生自EventArgs的类的实例。

To declare an event inside a class, first a delegate type for the event must be declared, if none is already declared.
public delegate void ChangedEventHandler(object sender, EventArgs e);
The delegate type defines the set of arguments that are passed to the method that handles the event.

所以,接下来我们按照规范的方式改写上面的程序,为了增加其他类型的参数,我们还会在发布者中传递一些字符串类型的message给订阅者。

namespace PubSub
{
    public delegate void NumberChangedEventHandler(object sender, ChangedEventArgs e); //定义委托

    public class ChangedEventArgs : EventArgs
    {
        public readonly int count;
        public readonly string msg;
        public ChangedEventArgs(int cnt, string str)
        {
            count = cnt;
            msg = str;
        }
    }

    public class Publishser
    {
        private int count;
        public event NumberChangedEventHandler NumberChanged; // 声明一个事件

        public void DoSomething()
        {
            // todo: do something
            if (++count > 0)
            {
                if (NumberChanged != null)
                {
                    NumberChanged(this, new ChangedEventArgs(count, "Something happened"));
                }
            }
        }
    }

    public class Subscriber
    {
        public void OnNumberChanged(object sender, ChangedEventArgs e)
        {
            Console.WriteLine("Subscriber notified from [{0}]: \ncount = [{1}], msg = [{2}]", sender, e.count, e.msg);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            Subscriber sub = new Subscriber();

            pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);
            pub.DoSomething();
        }
    }
}

输出结果:

Subscriber notified from [PubSub.Publishser]:
count = [1], msg = [Something happened]

(b) 事件的返回值

一般情况下,最好让事件的返回值为空。因为
首先,事件上可以绑定多个方法,这些方法是依次执行的,如果每个方法都有返回值,则需要发布者显示地依次执行每个方法,并处理相应的返回值。否则的话只有最后一个方法的返回值能被发布者获得。
其次,发布者与订阅者之间是松耦合的,发布者并不关心有谁订阅它的事件,更不会关心订阅者返回的东西。

(4) 事件的异常处理

还是上面的例子,我们再给发布者多加两个订阅者,并让其中一个订阅者的方法抛出异常。我们可以回顾一下,订阅者的方法被绑定到事件后,发布者一旦满足某种条件后触发该事件。而触发该事件时发布者要做的事情就是依次执行该事件上绑定的方法,即执行NumberChanged(this, EventArgs.Empty);这句话。所以我们尝试对这句话进行结构化异常处理。

namespace PubSub
{
    public delegate void NumberChangedEventHandler(object sender, EventArgs e); //定义委托

    public class Publishser
    {
        private int count;
        public event NumberChangedEventHandler NumberChanged; // 声明一个事件

        public void DoSomething()
        {
            // todo: do something
            if (++count > 0)
            {
                if (NumberChanged != null)
                {
                    try
                    {
                        NumberChanged(this, EventArgs.Empty);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Exception: {0}", e.Message);
                    }
                }
            }
        }
    }

    public class Subscriber1
    {
        public void OnNumberChanged(object sender, EventArgs e)
        {
            Console.WriteLine("Subscriber1 invoked");
        }
    }

    public class Subscriber2
    {
        public void OnNumberChanged(object sender, EventArgs e)
        {
            throw new Exception("Subscriber2 threw exception");
        }
    }

    public class Subscriber3
    {
        public void OnNumberChanged(object sender, EventArgs e)
        {
            Console.WriteLine("Subscriber3 invoked");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var pub = new Publishser();
            var sub1 = new Subscriber1();
            var sub2 = new Subscriber2();
            var sub3 = new Subscriber3();

            pub.NumberChanged += new NumberChangedEventHandler(sub1.OnNumberChanged);
            pub.NumberChanged += new NumberChangedEventHandler(sub2.OnNumberChanged);
            pub.NumberChanged += new NumberChangedEventHandler(sub3.OnNumberChanged);
            pub.DoSomething();
        }
    }
}

输出结果:

Subscriber1 invoked
Exception: Subscriber2 threw exception

结果显示,第2个订阅者抛出异常后发布者确实捕捉到了该异常,但是遗憾的是第3个订阅者的方法并没有被执行。也就是说事件上绑定的方法一旦出现了异常就终止了整个事件的处理,这样就影响到了后面的订阅者。所以对于这种情况,我们需要先获得事件上的委托链表,然后再在遍历链表的循环中处理异常。修改上面的DoSomething()方法如下:

public void DoSomething()
{
    // todo: do something
   if (++count > 0)
   {
       if (NumberChanged != null)
        {
            Delegate[] delArray = NumberChanged.GetInvocationList();
            foreach (Delegate del in delArray)
            {
                NumberChangedEventHandler method = (NumberChangedEventHandler)del; // 强制转换为具体的委托类型
                try
                {
                    method(this, EventArgs.Empty);
                }
                catch (Exception e)
                {
                    Console.WriteLine("Exception: {0}", e.Message);
                }
            }
        }
    }
}

输出结果:

Subscriber1 invoked
Exception: Subscriber2 threw exception
Subscriber3 invoked

(5) 泛型EventHandler<T>委托

既然有了上面提到的第一个参数为object,第二个参数为EventArgs派生类型的规范来书写自定义委托,我们肯定大多时候都是定义类似这样的委托。所以C#中还提供了EventHandler<T>泛型类型来简化我们得自定义过程。我们直接将它应用到上面的例子(3)(a)中即可得到:

namespace PubSub
{
    //public delegate void NumberChangedEventHandler(object sender, ChangedEventArgs e); //不再需要定义委托

    public class ChangedEventArgs : EventArgs
    {
        public readonly int count;
        public readonly string msg;
        public ChangedEventArgs(int cnt, string str)
        {
            count = cnt;
            msg = str;
        }
    }

    public class Publishser
    {
        private int count;
        public event EventHandler<ChangedEventArgs> NumberChanged; // 利用EventHanler泛型声明一个事件

        public void DoSomething()
        {
            // todo: do something
            if (++count > 0)
            {
                if (NumberChanged != null)
                {
                    NumberChanged(this, new ChangedEventArgs(count, "Something happened"));
                }
            }
        }
    }

    public class Subscriber
    {
        public void OnNumberChanged(object sender, ChangedEventArgs e)
        {
            Console.WriteLine("Subscriber notified from [{0}]: \ncount = [{1}], msg = [{2}]", sender, e.count, e.msg);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Publishser pub = new Publishser();
            Subscriber sub = new Subscriber();

            pub.NumberChanged += new EventHandler<ChangedEventArgs>(sub.OnNumberChanged);
            pub.DoSomething();
        }
    }
}

8. 委托与事件的编码规范

终于把委托与事件学习完了,但是整篇文章中的代码并不规范,尤其是前面部分。因此,最后我们再来简单谈谈.Net Framework的编码规范,以免我的code误导了大家。
(1) 委托类型的名称应以EventHandler结束,如果没有什么特征要求,则可以直接使用泛型委托定义EventHandler<T>
(2) 委托的原型定义:有一个void返回值,并接受两个输入参数:一个是object 类型,一个是EventArgs类或其派生类。
(3) 继承自EventArgs的类型应该以EventArgs结尾。
(4) 事件的命名为: 委托定义名去掉EventHandler之后剩余的部分。
(5) 订阅事件的方法的命名通常为: On事件名

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值