C#学习笔记 -- 事件

1、发布者和订阅者

  • 当一个特定的程序事件发生时, 程序的其他部分可以看到该事件已经发生的通知

  • 发布订阅模式可以满足这种需求.

    1. 在这种模式中, 发布者类定义了一些类程序的其他部分感兴趣的事件

    2. 其他类可以注册, 事件发生时, 收到发布者的通知

    3. 这些订阅者类通过向发布者提供一个方法来注册, 以获取通知

    4. 当事件发生时, 发布者触发事件, 然后执行订阅者提交的所有事件

  • 由订阅者提供的方法称为回调方法, 因为发布者通过执行这些方法来“往回调用订阅者的方法”

  • 还可以将他们称为事件处理程序, 因为他们是为处理事件而调用的代码

 

(1)事件

  1. 发布者(publisher): 发布某个事件的类或结构, 其他类可以在该事件发生时得到通知

  2. 订阅者(subsriber): 注册并在事件发生时得到通知的结构或类

  3. 事件处理程序(event handler): 由订阅者注册到事件的方法, 在发布者触发事件时执行

    • 可以定义在事件所在的类或结构中, 也可以定义在不同的类或结构中

  4. 触发(raise)事件: 调用(invoke)或触发(fire)事件, 当事件被触发时, 所有注册到它2的方法都会被依次调用

(2)事件的私有委托

实际上, 事件就像是专门用于某种特殊用途的简单委托, 委托和事件的行为很相似, 是因为事件包含了一个私有委托

  • 事件提供了对它的私有控制委托的结构化访问, 也就是无法直接访问委托

  • 事件中可用的操作比委托少, 对于事件只可以添加、删除或调用事件处理程序

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

  • 上图定义了发布者类, 并在其中定义了一个事件, 订阅者, 每次累计12个项会触发该事件

  • 订阅者Dozens和SomeOtheClass各有一个注册到CountedAdozen事件的事件处理程序

  • 每当触发事件时, 都会调用这些处理程序

2、源代码组件

需要在事件中使用的代码有五部分

  • 委托类型声明: 事件和事件处理程序必须有共同的签名和返回类型, 他们通过委托类型进行描述

  • 事件处理程序声明: 订阅者类会在事件中触发执行的方法声明, 他们不一定是显式命名的方法, 还可以是匿名、lambda

  • 事件声明: 发布者必须声明一个订阅者类可以注册的事件成员, 当类声明的事件为public, 则称为发布了事件

  • 触发事件的代码: 发布者类中“触发”事件并导致调用注册的所有事件处理程序的代码

3、声明事件

class MyClass
{
    public event 委托类型 事件名;
}

发布者类必须提供事件对象, 创建事件比较简单, 只需要委托类型和名称

class Incrementer
{
    public event EvenHandler CountedAdozen;
}
  • 事件声明在一个类中

  • 需要委托类型的名称, 任何附加到事件(如注册)的处理程序都必须与委托类型的签名和返回类型相匹配

  • 声明为public, 这样其他类和结构可以在它上面注册事件处理程序

  • 不能使用对象创建表达式(new关键字)来创建它的对象

  • 我们可以使用逗号分隔的列表在一个声明语句中声明一个以上的事件

    public event EventHandler MyEvent1, MyEvent2, MyEvent3;
  • 使用static关键字让事件变成静态的

    public static event EventHandler CountedADozen;

事件是成员

事件不是类型, 和方法、属性引用是成员, 特性如下

  • 事件是成员, 不能在一段可执行代码中声明事件

  • 事件是成员, 必须声明在类或结构中

  • 事件被隐式自动化为null

事件声明需要委托类型和名称, 我们可以声明一个委托类型或使用已有的委托类型, 如果声明一个委托类型, 它必须指定被事件注册的方法的签名和返回类型

BCL声明了一个叫做EventHandler的委托, 专门用于事件系统

4、订阅事件(订阅者)

订阅者向事件添加事件处理程序, 对于一个要添加到事件的事件处理程序来说, 他必须具有与事件的委托相同的返回类型和签名

//方法引用形式订阅事件
类的对象.事件成员 += 实例事件处理方法/静态事件处理方法;
//委托形式订阅事件
类的对象.事件成员 += new 委托类型(事件处理方法);
//使用匿名方法
类的对象.事件成员 += delegate {事件处理程序};
//使用lambda表达式
类的对象.事件成员 += (参数) => {事件处理程序}
  • 使用+=运算符来为事件添加事件处理程序

  • 事件处理程序的规范可以是以下任意一种

    • 实例方法的名称

    • 静态方法的名称

    • 匿名方法

    • Lambda表达式

5、触发事件(发布者)

事件本身只是保存了需要被调用的事件处理程序, 事件没有被触发, 什么都不会发生

if(事件 != null)
{
    事件名称(参数列表);
}
  • 在触发事件之前需要判断事件是否为空, 如果事件为空代表没有处理程序, 不能执行

  • 触发事件的语法和调用语法引用

    • 使用事件名称, 后面必须跟着参数列表

    • 参数列表必须与事件的委托类型匹配

class Incrementer
{
    public event EventHandler CountedADozen; //声明事件
​
    void DoCount(object source, EventArgs ars)
    {
        for (int i = 1; i < 100; i++)
        {
            if (i % 12 == 0) 
            {
                if (CountedADozen != null)
                {
                    //触发事件
                    CountedADozen(source, ars);
                }
            }
        }
    }
}
例子
  • Dozen类订阅事件, Incrementer发布事件

  • 订阅者构造器中将IncrementDozensCount作为事件处理程序

  • 在Incrementer类的DoCount方法中, 每增加12个技术就触发CountedADozen事件

//委托
public delegate void Handler();
//发布者
class Incrementer
{
    public event Handler CountedADozen; //声明事件
​
    //每12次发布一次事件
    public void DoCount()
    {
        for (int i = 1; i < 100; i++)
        {
            if (i % 12 == 0) 
            {
                if (CountedADozen != null)
                {
                    //发布事件, 参数列表与委托的参数列表一致
                    CountedADozen();
                }
            }
        }
    }
}
//订阅者
class Dozens
{
    public int DozensCount { get; private set; }
​
    public Dozens(Incrementer incrementer)
    {
        DozensCount = 0;
        //订阅事件
        incrementer.CountedADozen += IncrementDozensCount;
    }
​
    //声明时间按处理程序, 没触发一次事件, 订阅者属性DozensCount+1
    void IncrementDozensCount()
    {
        DozensCount++;
    }
}
static void Main(string[] args)
{
    //初始化发布者
    Incrementer incrementer = new Incrementer();
    //初始化订阅者, 并使用构造器订阅
    Dozens dozensCounter = new Dozens(incrementer);
    //调用触发事件函数
    incrementer.DoCount();
    Console.WriteLine($"Dozens的属性DozensCount = {dozensCounter.DozensCount}");//8
}

6、标准事件方法

  • 程序的异步处理是C#事件的最佳场景

  • doNET框架提供了一个标准模式, 该标准模式的基础是System命名空间中声明的EventHandler委托类型, 声明如下

public delegate void EventHandler(object sender, EventArgs e)
  • 第一个参数用来保存触发事件的对象引用, 是object类型的, 所以可以匹配任何类型的实例

  • 第二个参数用来保存状态信息, 指明什么类型适用与该应用程序

    • EventArgs类型声明在System命名空间中

    • EventArgs不能传递任何参数, 用于不需要传递数据的事件处理程序

    • 如果你希望传递数据, 必须声明一个派生自EventArgs的类, 并使用合适的字段来保存需要传递的数据

    • 尽管EventArgs类实际上不传递数据, 但是他是EventHandler委托模式的重要部分, 不管参数的实际类型是什么, objetEventArgs类型的参数总是基类. 这样EventHandler就能提供一个对所有事件和事件处理器都通用的签名, 让所有事件正好都有两个参数, 而不是各自都有不同的签名

  • 返回类型为void

例子
//发布者
class Incrementer
{
    public event EventHandler CountedADozen; //声明事件
​
    //每12次发布一次事件
    public void DoCount()
    {
        for (int i = 1; i < 100; i++)
        {
            if (i % 12 == 0) 
            {
                if (CountedADozen != null)
                {
                    //发布事件, 参数列表与委托的参数列表一致
                    CountedADozen(this, null);
                }
            }
        }
    }
}
//订阅者
class Dozens
{
    public int DozensCount { get; private set; }
​
    public Dozens(Incrementer incrementer)
    {
        DozensCount = 0;
        //订阅事件
        incrementer.CountedADozen += IncrementDozensCount;
    }
​
    //声明时间按处理程序, 没触发一次事件, 订阅者属性DozensCount+1
    void IncrementDozensCount(object source, EventArgs e)
    {
        DozensCount++;
    }
}
static void Main(string[] args)
{
    //初始化发布者
    Incrementer incrementer = new Incrementer();
    //初始化订阅者, 并使用构造器订阅
    Dozens dozensCounter = new Dozens(incrementer);
    //调用触发事件函数
    incrementer.DoCount();
    Console.WriteLine($"Dozens的属性DozensCount = {dozensCounter.DozensCount}");//8
}

(1)通过扩展EventArgs来传递数据

为了向自己的事件处理程序的第二个参数传入数值, 同时遵循标准管理, 需要声明一个派生自EventArgs的自定义类, 可以保存我们需要传入的数据. 类的名称应该以EventArgs结尾

public class IncrementerEventArgs : EventArgs
{
    public intIterationCount{get; set;}
}

现在有了一个自定义的类, 可以向事件处理程序的第二个参数传递数据, 所以你需要一个新自定义的委托类型, 为此, 可以使用泛型版本的委托EventHandler<>

  • 将自定义类的名称放在尖括号里面

  • 在需要使用自定义委托类型的地方使用整个字符串

public event EventHandler<IncrementerEventArgs> 事件名称;
例子
//继承EventArgs类型生成自定义参数
public class IncrementerEventArgs : EventArgs
{
    public int IterationCount { get; set; }
}
//发布者
class Incrementer
{
    //使用泛型版本的委托
    public event EventHandler<IncrementerEventArgs> CountedADozen;
    public void DoCount()
    {
        //自定义参数
        IncrementerEventArgs customArgs = new IncrementerEventArgs();
        for (int i = 1; i < 100; i++)
        {
            if (i % 2 == 0 && CountedADozen != null)
            {
                customArgs.IterationCount = i;
                CountedADozen(this, customArgs); //触发事件
            }
        }
    }
}
//订阅者
class Dozens
{
    public int DozensCount { get; private set; }
​
    public Dozens(Incrementer incrementer)
    {
        DozensCount = 0;
        //订阅事件
        incrementer.CountedADozen += IncrementDozensCount;
    }
​
    //声明时间按处理程序, 没触发一次事件, 订阅者属性DozensCount+1
    void IncrementDozensCount(object source, IncrementerEventArgs e)
    {
        Console.WriteLine($"Incremented at iteration: {e.IterationCount} in {source.ToString()}");
        DozensCount++;
    }
}

(2)移除事件处理程序

在用完事件处理程序之后, 可以从事件中把它移除, 可以利用-=运算符把事件处理程序从事件中移除

//移除事件处理程序MethodB
p.SimpleEvent -= s.MthodB;
例子
class Publisher
{
    public event EventHandler SimpleEvent;
    public void RaiseTheEvent() { SimpleEvent(this, null); }
}
class Subscriber
{
    public void MethodA(object o, EventArgs e) { Console.WriteLine("aaa"); }
    public void MethodB(object o, EventArgs e) { Console.WriteLine("bbb"); }
}
static void Main(string[] args)
{
    Publisher p = new Publisher();
    Subscriber s = new Subscriber();
    p.SimpleEvent += s.MethodA;
    p.SimpleEvent += s.MethodB;
    p.RaiseTheEvent();
​
    Console.WriteLine("Remove MethodB");
    p.SimpleEvent -= s.MethodB;
    p.RaiseTheEvent();
}

7、事件访问器

+=; -=运算符

  • 可以修改这两个运算符的行为, 在使用它们时让事件执行任何我们希望执行的自定义代码

  • 要改变这两个运算符的操作, 必须为事件定义访问器

    • 有两个访问器: addremove

    • 声明事件的访问器看上去和声明一个属性差不多

public event EventHandler CountedADozen
{
    add
    {
        //执行+=运算符的代码
    }
    
    remove
    {
        //执行-=运算符的代码
    }
}

声明了事件访问器之后, 事件不包含任何内嵌委托对象, 必须实现自己的机制来储存和移除事件注册的方法

事件访问器表现为void, 也就是不能使用返回值的return语句.

posted on 2023-05-30 00:42  老菜农  阅读(35)  评论(0编辑  收藏  举报

导航