开头很简单,最难的是坚持。|

陈侠云

园龄:2年10个月粉丝:1关注:1

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

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

  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}");
    }
}

本文作者:陈侠云

本文链接:https://www.cnblogs.com/chenxiayun/p/18393104

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   陈侠云  阅读(18)  评论(0编辑  收藏  举报
//雪花飘落效果
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起