02011501 事件

02011501 事件

1. 发布者和订阅者

  • 很多程序都有一个共同的需求,即当一个特点的程序事件发生时,程序的其它部分可以得到该事件已经发出的通知。
  • 发布者/订阅者模式:订阅者类通过向发布者提供一个方法来“注册”以获取通知,当事件发生时,发布者“触发事件”,然后执行订阅者提交的所有事件。
  • 由订阅者提供的方法称为回调方法。因为发布者通过执行这些方法来“往回调用订阅者的方法”。回调方法还可以称为事件处理程序,因为它们是为处理事件而调用的代码。
    • 发布者(Publicsher):发布某个事件的类或结构,其它类可以在该事件发生时得到通知。
    • 订阅者(Subscriber):注册并在事件发生时得到通知的类或结构。
    • 事件处理程序(Event Handler):由订阅者注册到事件的方法,在发布者触发事件时执行。
      • 事件处理程序方法可以定义在事件所在的类或结构中,也可以定义在不同的类或结构中。
    • 触发(Raise)事件,是调用(Invoke)或触发(Fire)事件的术语。所有注册到它的方法都会被依次调用。

2. 事件与委托的关系

  • 事件的很多部分都与委托类似。实际上,事件就像是专门用于某种特殊用途的简单委托。
  • 委托和事件的行为之所以相似,是有充分理由的。事件包含了一个私有的委托。
图片链接丢失
事件有被封装的委托
  • 有关事件的私有委托相关重要事项如下。
    • 事件提供了对它的私有控制委托的结构化访问。

      • 也就是说,你无法直接访问委托。
    • 事件中可用的操作比委托要少,对于事件我们只可以添加、删除或调用事件处理程序。

    • 事件被触发时,它调用委托来一次调用列表中的方法。

    • 在上图中,只有+=和-=运算符在事件框的左边,这是因为它们是事件唯一允许的操作。

3. 具有一个事件的类的结构和术语

图片链接丢失
  • 上图演示了一个叫做Incrementer的类,它按照某种方式进行计数。
  • Incrementer定义了一个CountedADozen事件,每次积累到12个项时将会触发该事件。
  • 订阅者Dozens和SomeOtherClass各自有一个注册到CountedADozen事件的事件处理程序。
  • 每当事件触发时,都会调用这些处理程序。

4. 源代码组件概览

图片链接丢失
  • 需要在事件中使用的代码有5部分,如下所示。
    • 委托类型声明 → 事件和事件处理程序必须有共同的签名和返回类型。
      • 它们通过委托类型进行描述。
    • 事件处理程序声明 → 订阅者类中会在事件触发时执行的方法声明。
      • 它们不一定是显式命名的方法,还可以是匿名方法和Lambda表达式。
    • 事件声明 → 发布者类必须声明一个订阅者类可以注册的事件成员。
      • 当类声明的事件为public时,称为发布了事件。
    • 事件注册 → 订阅者必须注册事件才能在事件被触发时得到通知。
      • 这是将事件处理程序与事件相连的代码。
    • 触发事件的代码 → 发布者类中“触发”事件并导致调用注册的所有事件处理程序的代码。

5. 声明事件

  • 发布者类必须提供事件对象。创建事件只需要委托类型和名称。
// 事件声明的基础语法
class Incrementer
{
          关键字    委托类型       事件名
            ↓         ↓            ↓
    public event EventHanlder CounterADozen;
}

说明:
1. 上述代码中声明了一个叫做CounterADozen的事件。
2. 事件声明在一个类中。
3. 事件需要委托类型的名称,任何附加到事件(如注册)的处理程序都必须与委托的签名和返回类型匹配。
4. 事件声明为public,这样其它类和结构可以在它上面注册事件处理程序。
5. 事件不能使用对象创建表达式(new表达式)来创建对象。
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 通过使用“,”分隔的列表在一个声明语句中声明一个以上的事件
publci event EventHanlder MyEvent1, MyEvent2, OtherEvent; // 声明3个事件
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 可以使用static关键字让事件变成静态的
public static event EventHanlder CountedADozen;

6. 事件是成员

  • 一个常见的误解是把事件视为类型。事件和方法、属性一样,事件是类或结构的成员。
  • 事件是成员,因此引出了如下几点重要内容。
    • 由于事件是成员,我们不能在一段可执行代码中声明事件。
    • 事件必须声明在类或结构中,和其它成员一样。
    • 事件成员被隐式的自动初始化为null。
  • 事件声明需要委托类型的名称,我们可以声明一个委托类型或使用已有的委托类型。
    • 如果声明一个委托类型,它必须指定将事件注册的方法的签名和返回类型。
  • BLC声明了一个叫做EventHandler的委托,专门用于系统事件。后续会介绍。

7. 订阅事件

  • 订阅者向事件添加事件处理程序。对于一个要添加到事件的事件处理程序来说,它必须具有与事件的委托相同的返回类型和签名。
    • 使用+=运算符来为事件添加事件处理程序,事件处理程序应该位于该运算符的右边。
    • 事件处理程序的规范可以是以下任意一种。
      • 实例方法的名称。
      • 静态方法的名称。
      • 匿名方法。
      • Lambda表达式。
// 为事件添加方法基础语法
    类        事件成员               实例方法
  	↓           ↓					 ↓
incrementer.CounterADozen += IncrementDozensCount; // 实例方法引用形式
incrementer.CounterADozen += ClassB.CounterHandlerB; // 静态方法引用形式
mc.CounterADozen += new EventHandler(cc.CounterHandlerC); // 委托形式
incrementer.CountedADozen += () => DozensCount++; // Lambda表达式
incrementer.CountedADozen += delegate {DozensCount++;}; // 匿名方法

8. 触发事件

  • 事件成员本身只是保存了需要被调用的事件处理程序。如果事件没有被触发,什么都不会发生。
if(CounterADozen != null) // 确认有方法可以执行
	CountedADozen(source, args); // 触发事件
          ↑              ↑
       事件名称        参数列表

说明:
1. 上述代码触发了CountedADozen事件。
2. 在触发事件之前和null进行比较,从而查看事件是否包含事件处理程序。如果事件是null,则表示没有事件处理程序,不能执行。
3. 触发事件的语法和调用方法一样。
3.1 使用事件名称,后面跟着参数列表。
3.2 参数列表必须与事件的委托类型相匹配。
  • 把事件声明和触发事件的代码放在一起便有了发布者类声明。
// 发布者类声明
class Incrementer
{
    public event EventHandler CountedADozen; // 声明事件
    
    void DoCount(object source, EventArgs args)
    {
        for(int i = 1; i < 100; i++)
            if(1 % 12 == 0)
                if(CountedADozen != null) // 确认有方法可以执行
                CountedADozen(source, args); // 触发事件
    }
}

9. 声明事件和触发事件整个程序示例

using System;

namespace Demo01
{
    delegate void Handler(); // @1 声明委托
    class Incrementer // @2 声明发布者类
    {
        public event Handler CountedADozen; // @3 声明事件并发布
        public void DoCount()
        {
            for (int i = 1; i < 100; i++)
                if (i % 12 == 0 && CountedADozen != null)
                    CountedADozen(); // 每增加12次,计数器触发事件一次
        }

    }

    class Dozens // @4 声明订阅者类
    {
        public int DozensCount { get; private set; }
        public Dozens(Incrementer incrementer)
        {
            DozensCount = 0;
            incrementer.CountedADozen += IncrementDozensCount; // @6 订阅事件

            void IncrementDozensCount() // @5 声明事件处理程序
            {
                DozensCount++;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            Incrementer incrementer = new Incrementer();
            Dozens dozensCounter = new Dozens(incrementer);

            incrementer.DoCount();
            Console.WriteLine($"Number of dozens = {dozensCounter.DozensCount}");

            Console.ReadLine();
        }
    }
}

控制台输出:
Number of dozens = 8

说明:
1. Incrementer是发布者类,Dozens是订阅者类。
2. 在构造函数中,Dozen类订阅事件,将IncrementDozensCount作为事件处理程序。

10 标准事件的用法

10.1 采用系统定义的EventHandler委托
  • GUI编程是事件驱动的,也就是说在程序运行时,它可以在任何时候被事件打断。
    • 比如按钮点击,按下按键或系统定时器。在这些情况发生时,程序需要处理事件然后继续做其它事情。
  • 程序事件的一部处理是使用C#事件的绝佳场景。
    • Windows GUI编程如此广泛地使用了事件,以至于对于事件的使用,.NET框架提供了一个标准模式。
    • 该标准模式的基础就是System命名空间中声明的EventHanlder类型。
  • EventHandler委托类型的声明如以下代码所示。
public delegate void EventHanlder(object sender, EventArgs e);

注意,使用EventHanlder委托的重点事项:
1. 第一个参数用来保存触发事件的对象的引用,由于是object类型的,因此可以匹配任何类型的实例。
2. 第二个参数用来保存状态信息,指明什么类型适用于该应用程序。
3. 返回类型是void。

补充说明:
1. EventHandler委托类型的第二个参数是EventArgs类的对象,EventArgs类声明在System命名空间中。
2. 既然第二个参数用来传递数据,EventArgs类的对象应该可以保存某种类型的数据。这个观点是错误的,理由如下:
2.1 EventArgs不能传递任何数据,它用于不需要传递数据的事件处理程序。
2.2 如果你需要传递数据,必须声明一个派生自EventArgs的类,并且使用合适的字段来保存需要传递的数据。
3. 尽管EventArgs类实际上不传递数据,但它是使用EventHandler委托模式的重要组成部分。
3.1 不管参数的实际类型是什么,object和EventArgs类型的参数总是基类。
3.2 这样EventHandler就能提供一个对所有事情和事件处理程序都通过的签名,让所有事件都正好有两个参数,而不是各自都有不同的签名。
10.2 标准事件的用法
using System;

namespace Demo01
{
    class Incrementer
    {
        public event EventHandler CountedADozen; // @1 使用系统定义的EventHandler委托来声明事件
        public void DoCount()
        {
            for (int i = 1; i < 100; i++)
                if (i % 12 == 0 && CountedADozen != null)
                    CountedADozen(this, null); // @2 this在类中表示当前类的对象,null表示忽略第二个形参。
        }

    }

    class Dozens 
    {
        public int DozensCount { get; private set; }
        public Dozens(Incrementer incrementer)
        {
            DozensCount = 0;
            incrementer.CountedADozen += IncrementDozensCount; // @4 订阅事件

            void IncrementDozensCount(object source, EventArgs e) // @3 声明事件处理程序,必须与系统定义的EventHandler委托签名匹配。
            {
                DozensCount++;
            }
        }
    }

    class Program
    {
        static void Main()
        {
            Incrementer incrementer = new Incrementer();
            Dozens dozensCounter = new Dozens(incrementer);

            incrementer.DoCount();
            Console.WriteLine($"Number of dozens = {dozensCounter.DozensCount}");

            Console.ReadLine();
        }
    }
}

控制台输出:
Number of dozens = 8
10.3 通过EventArgs的派生类传递数据
// 第1步 → 声明一个派生自EventArgs的类
public class IncrementerEvents : EventArgs // 派生类以基类名称结尾,便于观察
{
    public int IterationCount(get; set;) // 定义属性,用于存储数据
}
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 第2步 → 有了派生的自定义类,可以向事件处理程序的第二个参数传递数据了,所以需要一个使用新自定义类的委托类型,此时可以使用泛型版本的委托EventHandler<>。
public event EventHandler<IncrementerEventArgs> CounterADozen;
                     ↑                               ↑
                  泛型委托                          事件名称

说明:
1. 泛型委托将自定义类的名称放到<>内。
2. 在需要使用泛型委托的地方使用整个字符串,如:EventHandler<IncrementerEventArgs>
—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—·—
// 通过EventArgs的派生类传递数据示例
using System;

namespace Demo01
{
    class IncrementerEventArgs : EventArgs // @1 创建自定义类
    {
        public int IterationCount { get; set; } // @2 接受数据 
    }

    class Incrementer
    {
        public event EventHandler<IncrementerEventArgs> CountedADozen; // @3 使用泛型委托来声明事件
        public void DoCount()
        {
            IncrementerEventArgs args = new IncrementerEventArgs(); // @4 创建自定义类对象
            for (int i = 1; i < 100; i++)
                if (i % 12 == 0 && CountedADozen != null)
                {
                    args.IterationCount = i; // @5 通过自定义类的对象接收数据
                    CountedADozen(this, args); // @6 this在类中表示当前类的对象,null表示忽略第二个形参。
                }
        }

    }

    class Dozens 
    {
        public int DozensCount { get; private set; }
        public Dozens(Incrementer incrementer)
        {
            DozensCount = 0;
            incrementer.CountedADozen += IncrementDozensCount; // @8 订阅事件

            void IncrementDozensCount(object source, IncrementerEventArgs e) // @7 声明事件处理程序,必须与系统定义的EventHandler委托签名匹配。
            {
                Console.WriteLine($"展示接受的数据:{e.IterationCount} in {source.ToString()}"); // @9 展示自定义类接受的数据
                DozensCount++;
            }


        }
    }

    class Program
    {
        static void Main()
        {
            Incrementer incrementer = new Incrementer();
            Dozens dozensCounter = new Dozens(incrementer);

            incrementer.DoCount();
            Console.WriteLine($"Number of dozens = {dozensCounter.DozensCount}");

            Console.ReadLine();
        }
    }
}

控制台输出:
展示接受的数据:12 in Demo01.Incrementer
展示接受的数据:24 in Demo01.Incrementer
展示接受的数据:36 in Demo01.Incrementer
展示接受的数据:48 in Demo01.Incrementer
展示接受的数据:60 in Demo01.Incrementer
展示接受的数据:72 in Demo01.Incrementer
展示接受的数据:84 in Demo01.Incrementer
展示接受的数据:96 in Demo01.Incrementer
Number of dozens = 8

说明:通过ToString()方法展示了完全限定名,后续章节介绍。

11. 移除事件处理程序

  • 在用完事件处理程序之后,可以使用“-=”运算符将事件处理程序从事件中将其移除。
// 移除事件处理程序语法格式
p.SimpleEvent -= s.MethodB;

12. 事件访问器

  • 事件只允许使用“+=”和“-=”运算符,这个两个运算符具有定义良好的行为。
    • 然而,我们可以修改这两个运算符的行为,在使用它们时让事件执行任何我们希望执行的自定义代码。这是高级主体,本节只是简单介绍。
  • 要改变“+=”和“-=”运算符的操作,必须为事件定义事件访问器。
    • 有两个访问器,add和remove。
    • 声明事件的访问器看上去和声明一个属性差不多。
// 具有访问器的事件声明
publci event EventHandler CountedADozen
{
	add
    {
        ... // 执行+=运算符的代码
    }
    remove
    {
        ... // 执行-=运算符的代码
    }
}

说明:
1. add和remove两个事件访问器都有叫做value的隐式值参数,它接受实例或静态方法的引用。
2. 声明了事件访问器之后,事件不包含任何内嵌委托对象。我们必须实现自己的机制来存储和移除事件注册的方法。
3. 事件访问器表现为void方法,也就是不能使用返回值的return语句。

结尾

书籍:C#图解教程

著:【美】丹尼尔 · 索利斯;卡尔 · 施罗坦博尔

译:窦衍森;姚琪琳

ISBN:978-7-115-51918-4

版次:第5版

发行:人民邮电出版社

※敬请购买正版书籍,侵删请联系85863947@qq.com※

※本文章为看书或查阅资料而总结的笔记,仅供参考,如有错误请留言指正,谢谢!※

posted @ 2025-08-15 23:12  qinway  阅读(14)  评论(0)    收藏  举报