092.event.md 9.1 KB

事件

什么是事件

事件(Event) 基本上说是一个用户操作,如按键、点击、鼠标移动等等,或者是一些提示信息,如系统生成的通知。应用程序需要在事件发生时响应事件。例如,中断。

C# 中使用事件机制实现线程间的通信。

下面我们将一起探讨下事件的由来。

更好的封装性

我们继续思考上面的程序:上面的三个方法都定义在 Programe 类中,这样做是为了理解的方便,实际应用中,通常都是 Hello 在一个类中,ChineseHelloEnglishHello 在另外的类中。现在你已经对委托有了初步了解,是时候对上面的例子做个改进了。假设我们将 Hello() 放在一个叫 HelloManager 的类中,那么新程序应该是这个样子的:


namespace Delegate
{
    public delegate void HelloDelegate(string name);
 
    public class HelloManager
    {
        public void Hello(string name, HelloDelegate MakeHello)
        {
            MakeHello(name);
        }
    }
 
    class Program
    {
        private static void EnglishHello(string name)
        {
            Console.WriteLine("Hello, " + name);
        }
 
        private static void ChineseHello(string name)
        {
            Console.WriteLine("你好, " + name);
        }
 
        static void Main(string[] args)
        {
            HelloManager gm = new HelloManager();
            gm.GreetPeople("Liker", EnglishHello);
            gm.GreetPeople("李志中", ChineseHello);
        }
    }
}

我们运行这段代码,嗯,没有任何问题。

现在,假设我们需要使用上一节学到的知识,将多个方法绑定到同一个委托变量,该如何做呢?让我们再次改写代码:

static void Main(string[] args)
{
    HelloManager gm = new HelloManager();
    HelloDelegate delegate1;
    delegate1 = EnglishHello;
    delegate1 += ChineseHello;
    gm.GreetPeople("Liker", delegate1);
}

输出结果:

Hello, Liker
你好, Liker

到了这里,我们不禁想到:面向对象设计,讲究的是对象的封装,既然可以声明委托类型的变量(在上例中是delegate1),我们何不将这个变量封装到 GreetManager 类中?在这个类的客户端中使用不是更方便么,于是,我们改写GreetManager 类,像这样:

public class HelloManager
{
    /// <summary>
    /// 在 HelloManager 类的内部声明 delegate1 变量
    /// </summary>
    public HelloDelegate delegate1;
 
    public void GreetPeople(string name, HelloDelegate MakeHello)
    {
        MakeHello(name);
    }
}

现在,我们可以这样使用这个委托变量:

static void Main(string[] args)
{
    HelloManager gm = new HelloManager();
    gm.delegate1 = EnglishHello;
    gm.delegate1 += ChineseHello;
    gm.GreetPeople("Liker", gm.delegate1);
}

输出结果也没有问题。

尽管这样做没有任何问题,但我们发现这条语句很奇怪。在调用gm.GreetPeople 方法的时候,再次传递了gm 的delegate1 字段, 既然如此,我们何不修改 HelloManager 类成这样:

public class HelloManager
 {
     /// <summary>
     /// 在 HelloManager 类的内部声明 delegate1 变量
     /// </summary>
     public HelloDelegate delegate1;
 
     public void Hello(string name)
     {
         if (delegate1 != null) // 如果有方法注册委托变量
         { 
             delegate1(name); // 通过委托调用方法
         }
     }
 }

在客户端,调用看上去更简洁一些:

static void Main(string[] args)
{
    HelloManager gm = new HelloManager();
    gm.delegate1 = EnglishHello;
    gm.delegate1 += ChineseHello;
    gm.GreetPeople("Liker"); //注意,这次不需要再传递 delegate1 变量
}

尽管这样达到了我们要的效果,但是还是存在着问题:在这里,delegate1 和我们平时用的string 类型的变量没有什么分别,而我们知道,并不是所有的字段都应该声明成public,合适的做法是应该public 的时候public,应该private 的时候private。

我们先看看如果把 delegate1 声明为 private 会怎样?结果就是:这简直就是在搞笑。因为声明委托的目的就是为了把它暴露在类的客户端进行方法的注册,你把它声明为 private 了,客户端对它根本就不可见,那它还有什么用?

再看看把delegate1 声明为 public 会怎样?结果就是:在客户端可以对它进行随意的赋值等操作,严重破坏对象的封装性。

最后,第一个方法注册用“=”,是赋值语法,因为要进行实例化,第二个方法注册则用的是“+=”。但是,不管是赋值还是注册,都是将方法绑定到委托上,除了调用时先后顺序不同,再没有任何的分别,这样不是让人觉得很别扭么?

现在我们想想,如果delegate1 不是一个委托类型,而是一个string 类型,你会怎么做?答案是使用属性对字段进行封装。

于是,Event 出场了,它封装了委托类型的变量。我们改写HelloManager 类,它变成了这个样子

public class HelloManager
{
    //这一次我们在这里声明一个事件
    public event HelloDelegate MakeHello;
 
    public void Hello(string name)
    {
        MakeHello(name);
    }
}

很容易注意到:MakeGreet 事件的声明与之前委托变量 delegate1 的声明唯一的区别是多了一个 event 关键字。看到这里,在结合上面的讲解,你应该明白到:事件其实没什么不好理解的,声明一个事件不过类似于声明一个进行了封装的委托类型的变量而已。

为了证明上面的推论,如果我们像下面这样改写Main 方法:

static void Main(string[] args)
{
    HelloManager gm = new HelloManager();
    gm.MakeGreet = EnglishHello; // 编译错误1
    gm.MakeGreet += ChineseHello;
    gm.GreetPeople("Liker");
}

会报编译错误的提示信息。

限制类型能力

使用事件不仅能获得比委托更好的封装性,还能限制含有事件的类型的能力。这是什么意思呢?它的意思是说:事件应该由事件发布者触发,而不应该由事件的客户程序来触发。请看下面的范例:

using System;
 
class Program
{
    static void Main(string[] args)
    {
        Publishser pub = new Publishser();
        Subscriber sub = new Subscriber();
        pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);
        pub.DoSomething(); // 应该通过DoSomething()来触发事件
        pub.NumberChanged(100); // 但可以被这样直接调用,对委托变量的不恰当使用
    }
}
 
/// <summary>
/// 定义委托
/// </summary>
/// <param name="count"></param>
public delegate void NumberChangedEventHandler(int count);
 
/// <summary>
/// 定义事件发布者
/// </summary>
public class Publishser
{
    private int count;
 
    public NumberChangedEventHandler NumberChanged; // 声明委托变量
 
    //public event NumberChangedEventHandler NumberChanged; // 声明一个事件
 
    public void DoSomething()
    {
        // 在这里完成一些工作 ...
 
        if (NumberChanged != null) // 触发事件
        { 
            count++;
            NumberChanged(count);
        }
    }
}
 
/// <summary>
/// 定义事件订阅者
/// </summary>
public class Subscriber
{
    public void OnNumberChanged(int count)
    {
        Console.WriteLine("Subscriber notified: count = {0}", count);
    }
}
 

上面代码定义了一个NumberChangedEventHandler 委托,然后我们创建了事件的发布者Publisher 和订阅者Subscriber。当使用委托变量时,客户端可以直接通过委托变量触发事件,也就是直接调用pub.NumberChanged(100),这将会影响到所有注册了该委托的订阅者。而事件的本意应该为在事件发布者在其本身的某个行为中触发,比如说在方法DoSomething()中满足某个条件后触发。通过添加event关键字来发布事件,事件发布者的封装性会更好,事件仅仅是供其他类型订阅,而客户端不能直接触发事件(语句pub.NumberChanged(100)无法通过编译),事件只能在事件发布者Publisher类的内部触发(比如在方法pub.DoSomething()中),换言之,就是NumberChanged(100)语句只能在Publisher 内部被调用。大家可以尝试一下,将委托变量的声明那行代码注释掉,然后取消下面事件声明的注释。此时程序是无法编译的,当你使用了event 关键字之后,直接在客户端触发事件这种行为,也就是直接调用pub.NumberChanged(100),是被禁止的。事件只能通过调用DoSomething() 来触发。这样才是事件的本意,事件发布者的封装才会更好。

就好像如果我们要定义一个数字类型,我们会使用int 而不是使用object 一样,给予对象过多的能力并不见得是一件好事,应该是越合适越好。尽管直接使用委托变量通常不会有什么问题,但它给了客户端不应具有的能力,而使用事件,可以限制这一能力,更精确地对类型进行封装。