C#装箱和拆箱
装箱与拆箱(又叫取消装箱)就是值类型与引用类型的转换,是值类型和引用类型之间的桥梁。
之所以可以这样转换是因为 CTS 允许这样做。只有值类型才存在装箱和拆箱。
装箱是隐式的,拆箱是显式的,因为你需要告诉 CLR 你要给拆出来的值赋予什么类型。
通过深入了解装箱与拆箱的过程,我们可以知道其中包含了对堆上内存的操作,故会消耗性能,这是完全不必要的。
另外值得注意的是,装箱需要比原数据更多的空间,因为它需要两个引用类型的标准配置:类型对象指针和同步块索引。
我们可以从图中看到,装箱就是生成图中除了一开始 i=1 的变量之外另外两块变量的过程。
实际上,仅仅通过观察 C# 代码,是无法意识到装箱的,只有访问对应的 IL 代码才能真正观察到装箱。
IL 代码的装箱指令为 box。上面两行代码对应的 IL 代码为:
IL 代码的拆箱指令为 unbox。具体过程:
2) 创建一个新的对象 b,并将第一步获得的值复制到 b 中。
在 CLR via C# 中,拆箱被定义为第一步。下面的代码就是拆箱:
拆箱对应的 IL 代码为:
装箱过程需要创建一个新的引用类型对象实例。
由于 ArrayList 支持任何类型,所以其方法的参数全都是 object,这意味着即使我们的类型是结构体,也会被隐式地装箱,然后在使用时再拆箱。
C# 2 的泛型解决了这个问题。我们可以通过使用泛型集合避免不必要的装箱和拆箱。
很多地方会出现隐蔽的装箱,例如,对结构体的判等。当我们要实现自定义结构体的判等时:
之所以可以这样转换是因为 CTS 允许这样做。只有值类型才存在装箱和拆箱。
装箱是隐式的,拆箱是显式的,因为你需要告诉 CLR 你要给拆出来的值赋予什么类型。
通过深入了解装箱与拆箱的过程,我们可以知道其中包含了对堆上内存的操作,故会消耗性能,这是完全不必要的。
另外值得注意的是,装箱需要比原数据更多的空间,因为它需要两个引用类型的标准配置:类型对象指针和同步块索引。
装箱的过程
装箱就是把值类型转换为 object 类型或由此值类型实现的任何接口类型,如下图所示:int i = 1; object o = i;具体过程:
- 在堆中申请内存,内存大小为值类型的大小,再加上额外固定空间(类型对象指针和同步块索引)。
- 将值类型的字段值拷贝到新分配的内存中。
- 返回新引用对象的内存地址(给栈上的引用)。
我们可以从图中看到,装箱就是生成图中除了一开始 i=1 的变量之外另外两块变量的过程。
实际上,仅仅通过观察 C# 代码,是无法意识到装箱的,只有访问对应的 IL 代码才能真正观察到装箱。
IL 代码的装箱指令为 box。上面两行代码对应的 IL 代码为:
IL_0040: ldc.i4.1 IL_0041: stloc.2 IL_0042: Idloc.2 IL_0043 : box [mscorlib] System.Int32 IL_0048: stloc.3其中前两行对应
int i=1
这句代码,后三行对应object o=i
这句代码。拆箱的过程
简单地说,就是把装箱后的引用类型转换为值类型。由于并非一定成功,所以存在抛出异常的可能。IL 代码的拆箱指令为 unbox。具体过程:
int b = (int) o;
1) 检查是否为 Null,否则抛出 NullReferenceException 异常。检查实例是否为给定值类型的装箱值。否则抛出 InvalidCastException 异常,最后获得对象各个成员的地址2) 创建一个新的对象 b,并将第一步获得的值复制到 b 中。
在 CLR via C# 中,拆箱被定义为第一步。下面的代码就是拆箱:
(int) o;
但这句代码是无法通过编译的。通常来说,我们拆箱的目的都是为了将值拷贝到一个值类型中,所以拆箱之后,往往伴随着一次值的复制动作。上面的例子中,就将值复制到了变量 b。拆箱对应的 IL 代码为:
IL_000a: Idloc.1 IL_000b: unbox.any [mscorlib]System.Int32 IL_0010: stloc.2与拆箱比较,装箱的性能消耗更大,因为引用对象的分配更加复杂,成本也更高,值类型分配在栈上,分配和释放的效率都很高。
装箱过程需要创建一个新的引用类型对象实例。
如何避免拆箱和装箱
在 C#1 的时代,没有泛型,我们要定义一组自定义类型的数组只能使用 ArrayList。由于 ArrayList 支持任何类型,所以其方法的参数全都是 object,这意味着即使我们的类型是结构体,也会被隐式地装箱,然后在使用时再拆箱。
C# 2 的泛型解决了这个问题。我们可以通过使用泛型集合避免不必要的装箱和拆箱。
很多地方会出现隐蔽的装箱,例如,对结构体的判等。当我们要实现自定义结构体的判等时:
struct Rectangle { public override bool Equals(object obj) { } }我们发现,默认的签名为将两个比较对象转换为 object,这当然会引起装箱了。解决的办法是令结构体实现 IEquatable<T>接口 :
struct Rectangle : IEquatable<Rectangle> { public bool Equals(Rectangle r) { //… } }这样一来,即使类型存在两个 Equals,CLR 也会优先选择类型较小的那个,即参数为具体类型的 Equals,而不是参数为 Object 的 Equals。
所有教程
- C语言入门
- C语言编译器
- C语言项目案例
- 数据结构
- C++
- STL
- C++11
- socket
- GCC
- GDB
- Makefile
- OpenCV
- Qt教程
- Unity 3D
- UE4
- 游戏引擎
- Python
- Python并发编程
- TensorFlow
- Django
- NumPy
- Linux
- Shell
- Java教程
- 设计模式
- Java Swing
- Servlet
- JSP教程
- Struts2
- Maven
- Spring
- Spring MVC
- Spring Boot
- Spring Cloud
- Hibernate
- Mybatis
- MySQL教程
- MySQL函数
- NoSQL
- Redis
- MongoDB
- HBase
- Go语言
- C#
- MATLAB
- JavaScript
- Bootstrap
- HTML
- CSS教程
- PHP
- 汇编语言
- TCP/IP
- vi命令
- Android教程
- 区块链
- Docker
- 大数据
- 云计算