当前位置: 首页 > news >正文

C#/.NET 当我们在写事件 += 和 -= 的时候,方法是如何转换成事件处理器的

当我们在写 +=-= 事件的时候,我们会在 +=-= 的右边写上事件处理函数。我们可以写很多种不同的事件处理函数的形式,那么这些形式都是一样的吗?如果你不注意,可能出现内存泄漏问题。

本文将讲解事件处理函数的不同形式,理解了这些可以避免编写代码的时候出现内存相关的问题。


本文内容

    • 典型的事件处理函数
    • 变种事件处理函数
    • 编译器类型转换
    • 不是同一个委托实例
    • `+=` `-=` 是怎么做的
    • `-=`

典型的事件处理函数

事件处理函数本质上是一个委托,比如 FileSystemWatcherChanged 事件是这样定义的:

// 这是简化的代码。
public event FileSystemEventHandler Changed;

这里的 FileSystemEventHandler 是一个委托类型:

public delegate void FileSystemEventHandler(object sender, FileSystemEventArgs e);

一个典型的事件的 += 会像下面这样:

void Subscribe(FileSystemWatcher watcher)
{
    watcher.Changed += new FileSystemEventHandler(OnChanged);
}

void OnChanged(object sender, FileSystemEventArgs e)
{
}

+= 的右边传入的是一个 new 出来的委托实例。

变种事件处理函数

除了上面直接创建的目标类型的委托之外,还有其他类型可以放到 += 的右边:

// 方法组。
watcher.Changed += OnChanged;
// Lambda 表达式。
watcher.Changed += (sender, e) => Console.WriteLine(e.ChangeType);
// Lambda 表达式。
watcher.Changed += (sender, e) =>
{
    // 事件引发时,代码会在这里执行。
};
// 匿名方法。
watcher.Changed += delegate (object sender, FileSystemEventArgs e)
{
    // 事件引发时,代码会在这里执行。
};
// 委托类型的局部变量(或者字段)。
FileSystemEventHandler onChanged = (sender, e) => Console.WriteLine(e.ChangeType);
watcher.Changed += onChanged;
// 局部方法(或者局部静态方法)。
watcher.Changed += OnChanged;
void OnChanged(object sender, FileSystemEventArgs e)
{
}

因为我们可以通过编写事件的 addremove 方法来观察事件 += -= 传入的 value 是什么类型的什么实例,所以可以很容易验证以上每一种实例最终被加入到事件中的真实实例。

实际上我们发现,无论哪一个,最终传入的都是 FileSystemEventHandler 类型的实例。

都是同一类型的实例

然而我们知道,只有直接 new 出来的那个和局部变量那个真正是 FileSystemEventHandler 类型的实例,其他都不是。

那么中间发生了什么样的转换使得我们所有种类的写法最终都可以 += 呢?

编译器类型转换

具有相同签名的不同委托类型,彼此之前并没有继承关系,因此在运行时是不可以进行类型转换的。

比如:

FileSystemEventHandler onChanged1 = (sender, e) => Console.WriteLine(e.ChangeType);
Action<object, FileSystemEventArgs> onChanged2 = (sender, e) => Console.WriteLine(e.ChangeType);

这里,onChanged1 的实例不可以赋值给 onChanged2,反过来 onChanged2 的实例也不可以赋值给 onChanged1。于是这里只有 onChanged1 才可以作为 Changed 事件 += 的右边,而 onChanged2 放到 += 右边是会出现编译错误的。

不能转换

然而,我们可以放 Lambda 表达式,可以放匿名函数,可以放方法组,也可以放局部函数。因为这些类型可以在编译期间,由编译器帮助进行类型转换。而转换的效果就类似于我们自己编写 new FileSystemEventHandler(xxx) 一样。

不是同一个委托实例

看下面这一段代码,你认为可以 -= 成功吗?

void Subscribe(FileSystemWatcher watcher)
{
    watcher.Changed += new FileSystemEventHandler(OnChanged);
    watcher.Changed -= new FileSystemEventHandler(OnChanged);
}

void OnChanged(object sender, FileSystemEventArgs e)
{
}

实际上这是可以 -= 成功的。

我们平时编写代码的时候,下面的情况可能会多一些,于是自然而然以为 +=-= 可以成功,因为他们“看起来”是同一个实例:

watcher.Changed += OnChanged;
watcher.Changed -= OnChanged;

在读完刚刚那一段之后,我们就可以知道,实际上这一段和上面 new 出来委托的写法在运行时是一模一样的。

如果你想测试,那么在 += 的时候为对象加上一个 Id,在 -= 的时候你就会发现这是一个新对象(因为没有 Id)。

不是同一个对象

然而,你平时众多的编码经验会告诉你,这里的 -= 是一定可以成功的。也就是说,+=-= 时传入的委托实例即便不是同一个,也是可以成功 +=-= 的。

+= -= 是怎么做的

+=-= 到底是怎么做的,可以在不同实例时也能 +=-= 成功呢?

+=-= 实际上是调用了 DelegateCombineRemove 方法,并生成一个新的委托实例赋值给 += -= 的左边。

public event FileSystemEventHandler Changed
{
    add
    {
        onChangedHandler = (FileSystemEventHandler)Delegate.Combine(onChangedHandler, value);
    }
    remove
    {
        onChangedHandler = (FileSystemEventHandler)Delegate.Remove(onChangedHandler, value);
    }
}

而最终的判断也是通过 DelegateEquals 方法来比较委托的实例是否相等的(==!= 也是调用的 Equals):

public override bool Equals(object? obj)
{
    if (obj == null || !InternalEqualTypes(this, obj))
        return false;

    Delegate d = (Delegate)obj;

    // do an optimistic check first. This is hopefully cheap enough to be worth
    if (_target == d._target && _methodPtr == d._methodPtr && _methodPtrAux == d._methodPtrAux)
        return true;

    // even though the fields were not all equals the delegates may still match
    // When target carries the delegate itself the 2 targets (delegates) may be different instances
    // but the delegates are logically the same
    // It may also happen that the method pointer was not jitted when creating one delegate and jitted in the other
    // if that's the case the delegates may still be equals but we need to make a more complicated check

    if (_methodPtrAux == IntPtr.Zero)
    {
        if (d._methodPtrAux != IntPtr.Zero)
            return false; // different delegate kind
        // they are both closed over the first arg
        if (_target != d._target)
            return false;
        // fall through method handle check
    }
    else
    {
        if (d._methodPtrAux == IntPtr.Zero)
            return false; // different delegate kind

        // Ignore the target as it will be the delegate instance, though it may be a different one
        /*
        if (_methodPtr != d._methodPtr)
            return false;
            */

        if (_methodPtrAux == d._methodPtrAux)
            return true;
        // fall through method handle check
    }

    // method ptrs don't match, go down long path
    //
    if (_methodBase == null || d._methodBase == null || !(_methodBase is MethodInfo) || !(d._methodBase is MethodInfo))
        return Delegate.InternalEqualMethodHandles(this, d);
    else
        return _methodBase.Equals(d._methodBase);
}

于是可以看出来,判断相等就是两个关键对象的判断相等:

  1. 方法所在的对象
  2. 方法信息(对应到反射里的 MethodInfo)

继续回到这段代码:

void Subscribe(FileSystemWatcher watcher)
{
    watcher.Changed += new FileSystemEventHandler(OnChanged);
    watcher.Changed -= new FileSystemEventHandler(OnChanged);
}

void OnChanged(object sender, FileSystemEventArgs e)
{
}

这里的对象就是 this,方法信息就是 OnChanged 的信息,也就是:

// this 就是对象,OnChanged 就是方法信息。
this.OnChanged

-=

于是什么样的 -= 才可以把 += 加进去的事件处理函数减掉呢?

  • 必须是同一个对象的同一个方法

所以:

  1. 使用方法组、静态局部函数、委托字段的方式创建的委托实例,在 +=-= 的时候无视哪个委托实例,都是可以减掉的;
  2. 使用局部函数、委托变量,在同一个上下文中,是可以减掉的,如果调用是再次进入此函数,则不能减掉(因为委托方法所在的对象实例不同)
  3. 使用 Lambda 表达式、匿名函数是不能减掉的,因为每次编写的 Lambda 表达式和匿名函数都会创建新的包含此对象的实例。

我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

相关文章:

  • 清理 git 仓库太繁琐?试试 bfg!删除敏感信息删除大文件一句命令搞定(比官方文档还详细的使用说明)
  • 可集成到文件管理器,一句 PowerShell 脚本发布某个版本的所有 NuGet 包
  • Windows 系统的默认字体是什么?应用的默认字体是什么?
  • C# 8.0 的可空引用类型,不止是加个问号哦!你还有很多种不同的可空玩法
  • 一个简单的方法:截取子类名称中不包含基类后缀的部分
  • 使用 MSBuild Target 复制文件的时候如何保持文件夹结构不变
  • 如何在 MSBuild 中正确使用 % 来引用每一个项(Item)中的元数据
  • 如何将一个 .NET 对象序列化为 HTTP GET 的请求字符串
  • 屏幕边缘上有趣的 1 个像素,看不见、摸不到
  • 在 MSBuild 编译过程中操作文件和文件夹(检查存在/创建文件夹/读写文件/移动文件/复制文件/删除文件夹)
  • 在 WPF 程序中应用 Windows 10 真•亚克力效果
  • 推荐 .NET/C# 开发者安装的几款代码分析插件或对应的代码分析 NuGet 包
  • 在 HTML 超链接上添加可交互的 ToolTip
  • 在移动端打开 Google 的网页快照
  • 为自己搭建的博客添加可切换的暗色和亮色主题
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • CSS相对定位
  • Hibernate最全面试题
  • JavaScript标准库系列——Math对象和Date对象(二)
  • Java多态
  • jquery cookie
  • Laravel Telescope:优雅的应用调试工具
  • miniui datagrid 的客户端分页解决方案 - CS结合
  • PAT A1050
  • Python语法速览与机器学习开发环境搭建
  • Spring-boot 启动时碰到的错误
  • 从零开始的webpack生活-0x009:FilesLoader装载文件
  • 翻译:Hystrix - How To Use
  • 两列自适应布局方案整理
  • 说说动画卡顿的解决方案
  • 探索 JS 中的模块化
  • 腾讯优测优分享 | 你是否体验过Android手机插入耳机后仍外放的尴尬?
  • 栈实现走出迷宫(C++)
  • # 学号 2017-2018-20172309 《程序设计与数据结构》实验三报告
  • #{}和${}的区别?
  • $refs 、$nextTic、动态组件、name的使用
  • (3)llvm ir转换过程
  • (Java岗)秋招打卡!一本学历拿下美团、阿里、快手、米哈游offer
  • (k8s中)docker netty OOM问题记录
  • (Redis使用系列) Springboot 在redis中使用BloomFilter布隆过滤器机制 六
  • (第61天)多租户架构(CDB/PDB)
  • (终章)[图像识别]13.OpenCV案例 自定义训练集分类器物体检测
  • (转载)虚幻引擎3--【UnrealScript教程】章节一:20.location和rotation
  • . Flume面试题
  • .NET Core实战项目之CMS 第十二章 开发篇-Dapper封装CURD及仓储代码生成器实现
  • .NET DevOps 接入指南 | 1. GitLab 安装
  • .NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况
  • .NET高级面试指南专题十一【 设计模式介绍,为什么要用设计模式】
  • .NET中winform传递参数至Url并获得返回值或文件
  • ;号自动换行
  • @RequestMapping-占位符映射
  • [ C++ ] STL---string类的使用指南
  • [ linux ] linux 命令英文全称及解释
  • [2021]Zookeeper getAcl命令未授权访问漏洞概述与解决
  • [BZOJ1040][P2607][ZJOI2008]骑士[树形DP+基环树]