Unity 委托与事件、装箱和拆箱

文章目录


前言


一、委托与事件

1、委托的概念

不知道大家在学习C#之前有没有学习过C/C++,在中后期会接触到指针。她不仅能指向变量的地址,还能指向函数的地址。本质上,指向的都是内存的地址。

而在C#中,万物皆是类,指针被封装到内部函数中,因此并不常见。所有函数指针的功能都以委托的方式完成。委托可以被视为更高级的函数指针,它不仅能将地址指向另一个函数,而且还能传递参数、获取返回值等多种信息。

委托具有以下属性:

委托类似于 C++ 函数指针,但委托完全面向对象,不像 C++ 指针会记住函数,委托会同时封装对象实例和方法。

委托允许将方法作为参数进行传递。

委托可用于定义回调方法。

委托可以链接在一起;例如,可以对一个事件调用多个方法。

方法不必与委托类型完全匹配。 有关详细信息,请参阅使用委托中的变体。

使用 Lambda 表达式可以更简练地编写内联代码块。 Lambda 表达式(在某些上下文中)可编译为委托类型。 若要详细了解 lambda 表达式,请参阅 lambda 表达式。
官方解释

2、委托是什么

委托并不是一个语言类型,而是一个实例。大多数语言实现delegate关键字 (keyword) ,这些语言的编译器能够派生自 MulticastDelegate 类。MulticastDelegate 类显式Delegate派生。 类 Delegate 不被视为委托类型;它是用于派生委托类型的类。

什么意思呢,我们不能自己写一个类继承MulticastDelegate类,只有编辑器或其他工具可以。

Delegate类中有一个变量是用来存储函数地址的,当变量操作=(等号)时,把函数地址赋值给变量保存起来。不过这个存储函数地址的变量是一个可变数组,你可以认为它是一个链表,每次直接赋值时会换一个链表。 Delegate委托类还重写了+=、-=这两个操作符,其实就是对应MulticastDelegate类的Combine()和Remove()方法,当对函数进行+=和-=操作时,相当于把函数地址推入链表尾部,或者移出链表。

下面是官方的解释,意思是C#编辑器设计了一个列表来执行delegate,虽然我们在代码中使用了 delegate 关键字来定义委托类型,但实际上编译器在编译时会将其重写成 Delegate 类。换句话说,delegate 关键字只是一种修饰用词,用来告诉编译器我们正在定义一个委托类型,但最终在编译后的代码中,委托类型会被转换成一个类,这个类是 System.MulticastDelegate 类的子类。

MulticastDelegate类中有一个已经连接好的delegate列表,被称为调用列表,它由一个或者更多个元素组成。当一个multicast delegate被启动调用时,所有在调用列表里的delegate都会按照它们出现的顺序被调用。如果在执行列表期间遇到一个错误,就会立即抛出异常并停止调用。

3、事件是什么

事件是对委托的再次封装,目的是限制用户直接操作委托实例中的变量。因此,事件不能通过等号(=)赋值,而是只能通过注册和注销方法来增减委托的数量。这种限制的好处是显而易见的:在多人合作开发时,公开的委托很容易被其他人无意覆盖,而事件能更好地维护项目的稳定性和可靠性。

二、装箱和拆箱

1、什么是装箱和拆箱

装箱和拆箱,装箱是指将值类型转换成引用类型,拆箱是指将引用类型转换为值类型。

装箱:

csharp 复制代码
int a = 5;

object obj = a;

拆箱:

csharp 复制代码
a = (int)obj;

装箱过程中,a赋值给obj,obj创建一个指针并指向a的数据空间。

拆箱过程中,obj复制一份数据给a。

值类型声明时即初始化自身,不能为null。而引用类分配内存后,不指向任何空间,默认为null。

2、堆、栈

栈是一种特殊的容器,用来存放对象,遵循先进后出的原则。它的存储空间是连续的,因此对栈数据的定位速度比较快。与之相反,堆是随机分配的空间,处理的数据比较多,定位速度较慢。堆内存的创建和删除节点的时间复杂度是O(lgn),而栈的时间复杂度则是O(1),因此栈的速度更快。

尽管栈速度快,但它的生命周期必须确定,销毁时必须按照特定次序进行,即从最后分配的部分开始销毁。因此,栈主要用于生命周期比较确定的场景,如函数调用和递归调用。相反,堆内存可以存放生命周期不确定的内存块,满足需要在需要删除时再删除的需求。因此,堆内存更适合用于存放全局类型的内存块,分配和销毁更加灵活。

但要注意,值类型和引用类型并不是对应栈内存和堆内存。栈内存主要为确定性生命周期的内存服务,堆内存则更多的是无序的随时可以释放的内存。值类型和引用类型能在堆也能在栈内,其中引用类型指针部分可以指向栈内或堆内。

3、应用

在项目中需要一个通用的接口时就需要装箱操作。

4、优化

装箱、拆箱时会不断分配和销毁内存,增减内存碎片。

我们需要尽量少用。怎么做呢。

1、使用泛型。

2、统一接口提前装箱、拆箱。

3、使用Struct时通过重载函数来避免装箱、拆箱。对于值类型(Struct)而言,如果没有重载 ToString() 和 GetType() 等方法,当调用它们时会发生装箱操作。装箱操作会将值类型转换为引用类型(Object 类型),这会导致内存块重新分配,从而带来性能损耗。

书中举例:

如果Struct A和Struct B都继承了接口I,我们调用的方法是void Test(I i)。当调用Test方法时,传进去的Struct A或Struct B的实例相当于提前执行了装箱操作,Test方法里拿到参数后就不用再担心内部再次出现装箱、拆箱的问题了。


总结

在项目中,需要注意委托、装箱与拆箱的使用,尽量避免性能损耗。使用泛型、重载函数、提前装箱等方式进行优化。期待你的精益求精,加油!

相关推荐
Death2001 小时前
Qt 中的 QListWidget、QTreeWidget 和 QTableWidget:简化的数据展示控件
c语言·开发语言·c++·qt·c#
Death2002 小时前
Qt 3D、QtQuick、QtQuick 3D 和 QML 的关系
c语言·c++·qt·3d·c#
yufei-coder2 小时前
C#基础语法
开发语言·c#·.net
yngsqq2 小时前
031集——文本文件按空格分行——C#学习笔记
笔记·学习·c#
feng_xiaoshi2 小时前
【云原生】云原生架构的反模式
云原生·架构
XiaoLiuLB3 小时前
ChatGPT Canvas:交互式对话编辑器
人工智能·自然语言处理·chatgpt·编辑器·aigc
架构师吕师傅4 小时前
性能优化实战(三):缓存为王-面向缓存的设计
后端·微服务·架构
团儿.6 小时前
解锁MySQL高可用新境界:深入探索MHA架构的无限魅力与实战部署
数据库·mysql·架构·mysql之mha架构
艾伦~耶格尔15 小时前
Spring Boot 三层架构开发模式入门
java·spring boot·后端·架构·三层架构
新手unity自用笔记17 小时前
项目-坦克大战学习-子弹的移动与销毁
笔记·学习·c#