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、什么是装箱和拆箱

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

int a = 5;

object obj = a;

拆箱:

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方法里拿到参数后就不用再担心内部再次出现装箱、拆箱的问题了。


总结

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

相关推荐

  1. Unity 委托事件装箱

    2024-05-11 20:24:05       32 阅读
  2. 装箱(js的问题)

    2024-05-11 20:24:05       55 阅读
  3. C#面:简述装箱

    2024-05-11 20:24:05       53 阅读
  4. 自动装箱了解吗?原理是什么?

    2024-05-11 20:24:05       60 阅读
  5. 装箱, 包装类的缓存机制

    2024-05-11 20:24:05       29 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-05-11 20:24:05       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-05-11 20:24:05       100 阅读
  3. 在Django里面运行非项目文件

    2024-05-11 20:24:05       82 阅读
  4. Python语言-面向对象

    2024-05-11 20:24:05       91 阅读

热门阅读

  1. React 学习-5

    2024-05-11 20:24:05       34 阅读
  2. 6.5.Docker数据管理和端口映射应用

    2024-05-11 20:24:05       23 阅读
  3. 算法练习17——罗马数字转整数

    2024-05-11 20:24:05       28 阅读
  4. debian apt 更改阿里源

    2024-05-11 20:24:05       23 阅读
  5. android原生开发学习路线

    2024-05-11 20:24:05       29 阅读
  6. 算法详解——穷举法

    2024-05-11 20:24:05       25 阅读
  7. mysql5.x 的某些神奇问题

    2024-05-11 20:24:05       31 阅读
  8. 全志A133 android10 增加开机脚本

    2024-05-11 20:24:05       28 阅读