首页 > 编程语言 >《NET CLR via C#》---第十一章(事件)

《NET CLR via C#》---第十一章(事件)

时间:2024-09-03 16:18:51浏览次数:11  
标签:EventHandler prevHandler via ButtonEventArgs C# --- 事件 onClick public

事件成员的类型提供了以下功能:

  1. 方法能等级它对事件的关注
  2. 方法能注销它对事件的关注
  3. 事件发生时,登记的方法将收到通知

CLR事件模型以委托为基础。委托是调用回调方法的一种类型安全的方式。对象凭借回调方法接受它们订阅的通知。


设计要公开事件的类型

在某些情况下,当某个事件发生时,你可能不仅仅需要通知接收者这个事件发生了,还需要传递一些额外的信息。例如,一个按钮被点击了,你可能还想告诉接收者点击的按钮的具体名称、点击的时间等。

为了方便传递这些额外的信息,我们可以创建一个专门的类型(比如一个类或结构),用来装这些信息。这样,所有需要的附加信息都可以打包在一起,通过事件发送给接收者。

根据约定,这种类应该从System.EventArgs派生,而且类名以EventArgs结束。例如本例中,我们可以这么定义:

public class ButtonEventArgs : EventArgs
{
    private readonly string _buttonName;

    public string buttonName => _buttonName;

    public ButtonEventArgs(string name)
    {
        _buttonName = name;
    }
}

EventArgs的实现极其简单,基本就是一个让其他类型继承的基类型,没有任何附加信息,

[ComVisible(true), Serializable]
public class EventArgs
{
	public static readonly EventArgs Empty = new EventArgs();
	public EventArgs(){}
}

接下来就是第二步就是定义事件成员,我们以泛型System.EventHandler委托类型为例:

public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e);

我们可以这样定义自己的事件成员:

public class ButtonEventArgs : EventArgs
{
    private readonly string _buttonName;

    public string buttonName => _buttonName;

    public ButtonEventArgs(string name)
    {
        _buttonName = name;
    }
}

public class Button
{
    public event EventHandler<ButtonEventArgs> onClick;
}

这意味着,我们注册进onClick的方法原型必须如下:

void MethodName(Object sender, ButtonEventArgs e);

接下来第三步就是,触发事件,正常来讲,我们实现一个方法,在方法触发事件即可:

public class Button
{
    public event EventHandler<ButtonEventArgs> onClick;

    public void ClickButton(ButtonEventArgs e)
    {
        onClick?.Invoke(this, e);
    }
}

但这样写,并不十分保险,因为另一个线程或者委托链中某个方法本身都可能从委托链中移除一个委托,从而使得onClick成了null,这会抛出NullReferenceException异常。为了修复这个竞态问题,可以改用一下触发方式:

public class Button
{
    public event EventHandler<ButtonEventArgs> onClick;

    public void ClickButton(ButtonEventArgs e)
    {
        // 委托是不可变的,所以onClick在触发途中改变也没有问题
        var temp = onClick;
        temp?.Invoke(this, e);
    }
}

这基本上能解决问题,但作者抛出了一个疑虑,他认为编译器“可能”通过完全移除局部变量temp的方式对上述代码进行优化(虽然目前完全没可能),导致版本2的代码与版本1的代码又变得相同,所以他提出了一个一定不存在该问题的版本3写法:

public class Button
{
    public event EventHandler<ButtonEventArgs> onClick;

    public void ClickButton(ButtonEventArgs e)
    {
        var temp = Volatile.Read(ref onClick);
        temp?.Invoke(this, e);
    }
}

这样子,一个事件基本就写好,具体的添加与移除方法先不讲。我们来关注另一个问题,event的本质是什么?

事件实现原理

event onClick在底层实际上是被拆成了一个构造,一个是初始化为null的私有委托字段,一个是公共add方法,一个是公共remove移除方法。

public class Button
{
    public EventHandler<ButtonEventArgs> onClick = null;

    /// <summary>
    /// 允许方法登记对事件的关注
    /// </summary>
    public void add_onClick(EventHandler<ButtonEventArgs> value)
    {
        EventHandler<ButtonEventArgs> prevHandler;
        EventHandler<ButtonEventArgs> newClick = this.onClick;
        do
        {
            prevHandler = newClick;
            EventHandler<ButtonEventArgs> newHandler = (EventHandler<ButtonEventArgs>) Delegate.Combine(prevHandler, value);
            // 最关键的代码,如果此时有另一个线程往onClick中注册了新方法,onClick和prevHandler比较必然不能通过,那么得到的newClick必然不等于preHandler
            // 如果不存在其他线程添加的情况,则onClick和prevHandler必然相等,则onClick替换为newHandler的值,返回的newHandler是替换前的值,与prevHandler必然相等,此时跳出循环
            newClick = Interlocked.CompareExchange<EventHandler<ButtonEventArgs>>(ref this.onClick, newHandler, prevHandler);
        } while (newClick != prevHandler);
    }

    public void remove_onClick(EventHandler<ButtonEventArgs> value)
    {
        EventHandler<ButtonEventArgs> prevHandler;
        EventHandler<ButtonEventArgs> newClick = this.onClick;
        do
        {
            prevHandler = newClick;
            EventHandler<ButtonEventArgs> newHandler = (EventHandler<ButtonEventArgs>)Delegate.Remove(prevHandler, value);
            // 最关键的代码,如果此时有另一个线程往onClick中移除了新方法,onClick和prevHandler比较必然不能通过,那么得到的newClick必然不等于preHandler
            // 如果不存在其他线程移除的情况,则onClick和prevHandler必然相等,则onClick替换为newHandler的值,返回的newHandler是替换前的值,与prevHandler必然相等,此时跳出循环
            newClick = Interlocked.CompareExchange<EventHandler<ButtonEventArgs>>(ref this.onClick, newHandler, prevHandler);
        } while (newClick != prevHandler);
    }

具体解释下Interlocked.CompareExchange<T>函数做了神马东西:

Interlocked.CompareExchange 是 .NET 中用于多线程编程的一个方法,它可以在多个线程之间安全地操作变量。它执行一个原子操作,在比较和交换中实现同步,防止竞态条件(Race Condition)的发生。他的方法定义是:

public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class;

参数解释:

  • location1: 要比较和交换的变量的引用。这个变量是被操作的目标。
  • value: 如果 location1 等于 comparand,那么将 location1 替换为 value。
  • comparand: 用于比较的值。

返回值:

  • 返回原始的 location1 的值。

工作原理:

  • 比较:Interlocked.CompareExchange 首先将 location1 的当前值与 comparand 进行比较。
  • 交换:如果 location1 的当前值等于 comparand,那么 location1 的值会被替换为 value。否则,location1 保持不变。
  • 返回值:方法返回操作前 location1 的值,不论是否进行了替换。

在本例中,add和remove方法的可访问性都是public。这是因为源代码将事件声明为public。如果事件声明为proteced,编译器生成的add和remove方法也会被声明为protected。除此之外,事件成员也可声明为static或virtual。在这种情况下,编译器生成的add和remove方法分别标记为static或virtual。

接下来就是事件调用的展示:

public class Program
{
    static void Main(string[] args)
    {
        var btn = new Button();
        // 注册 C#编译器会翻译成这种形式 btn.add_onClick(new EventHandler<ButtonEventArgs>(Hello));
        btn.onClick += Hello;
        
        // 调用
        btn.ClickButton(new ButtonEventArgs("World"));

        // 移除 C#编译器会翻译成这种形式 btn.remove_onClick(new EventHandler<ButtonEventArgs>(Hello));
        btn.onClick -= Hello;
    }

    public static void Hello(Object o, ButtonEventArgs e)
    {
        Console.WriteLine($"Hello {e.buttonName}");
    }
}

标签:EventHandler,prevHandler,via,ButtonEventArgs,C#,---,事件,onClick,public
From: https://www.cnblogs.com/chenxiayun/p/18393104

相关文章

  • 前端Vue3项目VUE3+TypeScript企业级前端Vue项目
    前端Vue3项目VUE3+TypeScript企业级前端Vue项目‌Vue3+SpringBoot前端项目实战:‌智慧实验室管理平台‌在当今数字化快速发展的时代,‌智慧实验室管理平台的建设成为了提升科研效率与管理水平的关键一环。‌本文将通过一个实战案例,‌详细介绍如何使用Vue3和SpringBoot技术栈构建......
  • 【北京迅为】《stm32mp157开发板嵌入式linux开发指南》第五章 Ubuntu使用apt-get下载
         iTOP-STM32MP157开发板是基于意法半导体STARM双Cortex-A7核加单Cortex-M4核的一款多核异构处理器。Cortex-A7内核提供对开源操作系统Linux的支持,借助Linux系统庞大而丰富的软件组件处理复杂应用。M4内核上运行对于实时性要求严格的应用。         开......
  • SpringBoot项目常用配置文件MybatisPlusConfig、RedisConfig、RedissonConfig、Swagge
    MybatisPlusConfig:@Configuration@MapperScan("com.yupi.usercenter.mapper")publicclassMybatisPlusConfig{@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterc......
  • 不懂AI和Data Cloud,未来会被Salesforce行业淘汰?
    在如今的数字化时代,Salesforce的DataCloud不仅是一次普通的技术升级,更是企业发展的一张新王牌。通过将AI技术深度融合到DataCloud中Salesforce帮助企业更快、更聪明地处理数据,从而在瞬息万变的市场中占得先机。01实时数据与AI:企业成功的关键随着客户需求不断提升,企业必须加......
  • C语言 09 流程控制
    if如果需要判断某个条件,当满足此条件时,才执行某些代码,那这个时候该怎么办呢?可以使用if语句来实现:#include<stdio.h>intmain(){inti=0;//只希望i大于10的时候才执行下面的打印语句if(i>10){printf("该数字大于10");}//后面的代......
  • LeetCode_0028. 找出字符串第一个匹配项的下标,KMP算法的实现
    题目描述  给你两个字符串haystack和needle,请你在haystack字符串中找出needle字符串的第一个匹配项的下标(下标从0开始)。如果needle不是haystack的一部分,则返回-1。示例1:输入:haystack="sadbutsad",needle="sad"输出:0解释:"sad"在下标0和6处匹......
  • CF939D
    比较好的构造题首先数据范围为\(18\),令人浮想联翩:状压有一个性质就是我们可以在一定操作内把一段区间的数全部变成\(0\)~\(r-l\),再全部变成\(r-l+1\)具体的,对于一段区间\(l,r\)变成\(0\)~\((r-l+1)\)先把\(l,r-1\)变为\(0\)~\((r-l)\),然后判断\(a_r\)......
  • Vue3+Vite+Vant-UI+Pinia+VueUse开发双端业务驱动技术栈商用项目
    前言:个人git仓库,全是干货一、本次搭建项目涉及到vue3、vite、pinia、vue-router、typescript、element-plus,下面先简单介绍一下大家比较陌生的框架或库1、vue3vue团队官宣:2022年2月7日,vue3作为vue的默认版本。现在打开vue官网,界面默认显示的是vue3版本的指导文档。vue团队在......
  • 16、DB-DML语言(数据操作语言)-增删改-删除(delete from)(truncate)(drop)
    delete格式:DELETEFROM`表名`WHERE条件 --删除数据1、--删除指定数据DELETEFROM`student`WHEREid=110058 --清空表数据--truncate`表`TRUNCATE`student`--delete`表`不建议使用DELETE`student`  delete与truncate的区别:·相......
  • CSS线性渐变效果
    1、未添加元素前2、添加元素后#实现方法,在父级盒子里面添加背景图片 .box{  position:relative;  margin:0auto;  z-index:index2;  width:736px;  height:414px;  background-image:url(./img/jhk-1723779352440.jpg); }......