图解C#高级教程(二):事件

news/2024/9/30 10:35:15 标签: c#, 开发语言

在现实生活当中,有一些事情发生时,会连带另一些事情的发生。例如,当某国的总统发生换届时,不同党派会表现出不同的行为。两者构成了“因果”关系,因为发生了A,所以发生了B。在编程语言当中,具有类似的概念。在编程语言中,发生的事情 A 称为事件,因 A 发生的事情 B 称为对事件的处理或者响应(事件处理程序)。

本章主要讲解 C# 语言当中的发布者-订阅者模式的概念、代码实现该模式时的组成部分以及标准事件的用法。

1. 发布者和订阅者的概念

在程序中,我们很多时候会面临这样一个需求:当一个特定的程序事件发生时,程序的其它部分(类、函数或者其它)可以得到该事件已经发生的通知并对此做出相应的处理。

发布者-订阅者模式(publisher/subscriber pattern)可以满足这种需求。在这种模式中,发布者类定义了订阅者类感兴趣的事件。当事件发生时,发布者通知到这些订阅者,然后订阅者执行相应的事件处理函数。

但是,发布者是如何通知到这些订阅者呢?订阅者通过注册函数。其实就是发布者维护了一个关注某个事件的订阅者集合。例如,我在 csdn 博客上开了某个领域的专栏,然后你订阅了这个专栏,在后台会维护关注这个专栏的用户列表。当我更新了这个专栏的文章时,会通知到每一个订阅的用户。

那么,订阅者又是如何在事件发生时执行相应的事件处理函数呢?订阅者向发布者注册事件发生时的事件处理函数(回调函数,我们称函数的参数类型是函数的函数)。

下图说明了发布者-订阅者模式的工作流程:
在这里插入图片描述
下面是发布者-订阅者模式的组件:

  1. 发布者:发布某个事件的类或结构。维护一个订阅者集合(可选)、一个事件处理程序的集合、提供给订阅者注册订阅和回调函数的接口;
  2. 订阅者:关注事件的类或者结构。需要向订阅者提供回调函数名;
  3. 触发事件。本质上是触发事件的代码。

在 C#高级教程(一):委托当中介绍了委托。实际上,事件就像是专门用于某种特殊用途的委托。

2. 源代码组件

为了实现发布者-订阅者模式,在代码中需要完成以下 5 部分:

  • 委托类型声明:事件和事件处理程序必须具有相同的签名和返回类型,它们通过委托类型进行描述;
  • 事件处理程序声明:订阅者类中会在事件触发时执行的方法调用;
  • 事件声明:发布者类必须声明一个订阅者类可以注册的事件成员;
  • 事件注册:订阅者必须订阅事件才能在它被触发时得到通知;
  • 触发事件的代码:发布者类中“触发”事件并执行所有事件处理程序的代码。
    在这里插入图片描述
    下面是一个简单的代码实现:
delegate void Handler();

class Incrementer
{
    public event Handler CountedADozen;     // 事件名

    public void DoCount()
    {
        for (int i = 1; i < 100; i++)
        {
            if (i % 12 == 0 && CountedADozen!= null) // 触发事件的代码
            {
                CountedADozen();        // 调用事件
            }
        }
    }
}

// 订阅者
class Dozens
{
    public int DozensCount { get; set; }

    public Dozens(Incrementer incrementer)
    {
        DozensCount = 0;
        incrementer.CountedADozen += IncrementDozenCount;   // 注册回调函数
    }
    
    // 回调函数
    void IncrementDozenCount()
    {
        DozensCount++;
    }
}

class Programer
{
    static void Main()
    {
        Incrementer incrementer = new Incrementer();
        Dozens dozens = new Dozens(incrementer);

        incrementer.DoCount();
        Console.WriteLine("Dozens count: {0}", dozens.DozensCount);
    }
}

输出:
在这里插入图片描述

3. 标准事件的用法

由于 GUI 编程是事件驱动的,而 Windows GUI 编程广泛地使用了事件,因此 .NET 框架提供了一个标准模式。具体就是在 System 命名空间提供了 EventHandler 委托类型。
在这里插入图片描述
第二个参数 EventArgs 设计为不能用来传递任何数据,它用于不需要传递数据的事件处理程序。如果你希望传递数据,必须声明一个派生自 EventArgs 的类,使用合适的字段来保存需要传递的数据。

接下来,我们就使用标准事件改写第二节的程序:

class Incrementer
{
    public event EventHandler CountedADozen;     // 事件名

    public void DoCount()
    {
        for (int i = 1; i < 100; i++)
        {
            if (i % 12 == 0 && CountedADozen!= null) // 触发事件的代码
            {
                CountedADozen(this, null);        // 调用事件
            }
        }
    }
}
// 订阅者
class Dozens
{
    public int DozensCount { get; set; }

    public Dozens(Incrementer incrementer)
    {
        DozensCount = 0;
        incrementer.CountedADozen += IncrementDozenCount;   // 注册回调函数
    }
    
    // 回调函数
    void IncrementDozenCount(object sender, EventArgs e)
    {
        DozensCount++;
    }
}

class Programer
{
    static void Main()
    {
        Incrementer incrementer = new Incrementer();
        Dozens dozens = new Dozens(incrementer);

        incrementer.DoCount();
        Console.WriteLine("Dozens count: {0}", dozens.DozensCount);
    }
}

通过扩展 EventArgs 来传递数据

为了向自己的事件处理程序的第二个参数传入数据,并且又符合标准惯例,我们需要声明一个派生自 EventArgs 的自定义类,用来保存我们需要传入的数据。

下面是改写后的代码:

// 派生自EventArgs的自定义类
public class IncrementerArgs: EventArgs
{
    public int IterationCount { get; set; }
}

class Incrementer
{
    // 使用自定义类的泛型委托
    public event EventHandler<IncrementerArgs> CountedADozen;     // 事件名

    public void DoCount()
    {
        IncrementerArgs args = new IncrementerArgs();
        for (int i = 1; i < 100; i++)
        {
            if (i % 12 == 0 && CountedADozen!= null) // 触发事件的代码
            {
                args.IterationCount = i;
                CountedADozen(this, args);        // 调用事件
            }
        }
    }
}
// 订阅者
class Dozens
{
    public int DozensCount { get; set; }

    public Dozens(Incrementer incrementer)
    {
        DozensCount = 0;
        incrementer.CountedADozen += IncrementDozenCount;   // 注册回调函数
    }
    
    // 回调函数
    void IncrementDozenCount(object sender, IncrementerArgs e)
    {
        Console.WriteLine("Incremented at iteration: {0} and {1}",
                            e.IterationCount, sender.ToString());
        DozensCount++;
    }
}

class Programer
{
    static void Main()
    {
        Incrementer incrementer = new Incrementer();
        Dozens dozens = new Dozens(incrementer);

        incrementer.DoCount();
        Console.WriteLine("Dozens count: {0}", dozens.DozensCount);
    }
}

输出:
在这里插入图片描述

移除事件处理程序

在用完了事件处理程序之后,可以从事件中把它移除。下面是一个例子:

class Publisher
{
    public event EventHandler SimpleEvent;

    public void RaiseTheEvent() { SimpleEvent(this, null); }
}

class Subsriber
{
    public void MethodA(object o, EventArgs e) { Console.WriteLine("MethodA called"); }
    public void MethodB(object o, EventArgs e) { Console.WriteLine("MethodB called"); }

}

class Program
{
    static void Main()
    {
        Publisher p = new Publisher();
        Subsriber s = new Subsriber();

        p.SimpleEvent += s.MethodA;
        p.SimpleEvent += s.MethodB;
        p.RaiseTheEvent();

        Console.WriteLine("\r\nRemove MethodB");
        p.SimpleEvent -= s.MethodB;
        p.RaiseTheEvent();
    }
}

程序的输出:
在这里插入图片描述

小结:本章介绍了 C# 语言当中的发布者-订阅者模式的概念,源代码组件的五个部分,以及标准事件的用法。

各位道友,码字不易。如有收获,记得一键三连。


http://www.niftyadmin.cn/n/5684913.html

相关文章

关于git stash指令在切换到另一个分支处理紧急任务的场景使用

使用场景 临时保存更改&#xff1a;当你正在进行一些工作&#xff0c;但需要切换到另一个分支或处理其他任务时&#xff0c;可以使用 git stash 将当前的更改暂存起来。恢复更改&#xff1a;完成其他任务后&#xff0c;可以使用 git stash pop 将之前暂存的更改恢复到工作目录…

mit6824-01-MapReduce详解

文章目录 MapReduce简述编程模型执行流程执行流程排序保证Combiner函数Master数据结构 容错性Worker故障Master故障 性能提升定制分区函数局部性执行缓慢的worker(slow workers) 常见问题总结回顾参考链接 MapReduce简述 MapReduce是一个在多台机器上并行计算大规模数据的软件架…

【数据结构】图的最小生成树

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《C游记》《进击的C》《Linux迷航》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、最小生成树的概念二、Kruskal算法2.1 思想2.2 实现 三、Prim算法3.1 思想3.2 实现 四、Kruskal和Prim的对比…

【C++单调队列】1438. 绝对差不超过限制的最长连续子数组|1672

本文时间知识点 C队列、双向队列 LeetCode1438. 绝对差不超过限制的最长连续子数组 给你一个整数数组 nums &#xff0c;和一个表示限制的整数 limit&#xff0c;请你返回最长连续子数组的长度&#xff0c;该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。 如…

Linux中安装ffmpeg

Linux中安装ffmpeg 一、下载二、安装三、测试 一、下载 先到这里下载ffmpeg。 二、安装 先将上传到服务器的某一目录&#xff0c;我这里是&#xff1a; /usr/local/ffmpeg 然后解压&#xff0c;解压命令如下&#xff1a; tar -xvf “你的安装包名称”我的是&#xff1a; ta…

二叉搜索树(c++版)

前言 在前面我们介绍过二叉树这个数据结构&#xff0c;今天我们更进一步来介绍二叉树的一种在实现中运用的场景——二叉搜索树。二叉搜索树顾名思义其在“搜索”这个场景下有不俗的表现&#xff0c;之所以会这样是因为它在二叉树的基础上添加了一些属性。下面我们就来简单的介…

矩阵奇异值

一、ATA 任给一个矩阵A&#xff0c;都有&#xff1a; ATA 为一个对称矩阵 例子&#xff1a;A为一个mn的矩阵&#xff0c;A的转置为一个nm的矩阵 对称矩阵的重要性质如下&#xff1a; ① 对称矩阵的特征值全为实数&#xff08;实数特征根&#xff09; ② 任意一个n阶对称矩阵…

ubuntu切换源方式记录(清华源、中科大源、阿里源)

文章目录 前言一、中科大源二、清华源三、阿里源 前言 记录ubunut切换各个源的方式。 备注&#xff1a;更换源之后使用sudo apt-get update更新索引。 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、中科大源 地址&#xff1a;https://mirrors.u…