C# 中的装箱和拆箱

2024 年 08 月 29 日 | 阅读 9 分钟

在本文中,您将了解 C# 中的装箱(Boxing)拆箱(Unboxing)及其工作原理和示例。

什么是装箱?

C# 中的装箱是将值类型(例如,int、float、struct)转换为引用类型(例如,object)的过程。此操作实质上是将值类型封装在对象实例中,允许您将其存储在需要引用类型的集合中,或将其作为参数传递给期望引用类型的方法。装箱是 C# 编程中的一个基本概念,但重要的是要了解它的工作原理以及如何谨慎使用它。

为什么需要装箱?

C# 区分两种基本数据类型:值类型引用类型

值类型:这些包括像整数 (int)、浮点数 (float)用户定义的结构这样的原始数据类型。值类型存储在栈上,直接保存它们所代表的数据。

引用类型:当您创建引用类型的变量时,例如类实例委托,变量存储的是实际对象数据存储的内存位置的引用。此引用类似于指向对象的指针。

装箱如何工作

当您装箱值类型时,会在堆上创建一个新对象,并将值类型的值复制到该对象中。此过程涉及以下步骤:

  • 在堆上分配内存以存储值类型的实例。
  • 值类型的值被复制到此堆分配的内存中。
  • 返回对此新创建对象的引用,该引用可以分配给对象类型或任何其他引用类型的变量。

这是一个装箱的例子

在此代码中,int 值42被装箱成一个名为boxedNum的对象引用。

性能影响

装箱和拆箱可能会带来性能影响,例如

内存开销:装箱会在堆上创建对象,这会消耗内存并可能导致垃圾回收活动增加。频繁的装箱操作可能会对应用程序的性能产生负面影响,尤其是在内存受限的情况下。

类型安全:拆箱(将对象转换回其原始值类型)涉及运行时类型检查以确保拆箱有效。它会引入性能成本。

最佳实践是使用泛型集合(例如 List)来最大限度地减少装箱的性能开销,并使用直接处理值类型的方法,从而完全避免装箱的需要。

避免装箱

如前所述,在可能的情况下,最好避免装箱。以下是一些避免装箱的策略:

  • 使用泛型集合(例如,List)而不是非泛型集合(例如,ArrayList)。
  • 在设计数据结构和类时,在适当的情况下优先使用值类型。
  • 如果可用,使用直接接受值类型的方法重载。
  • 例如,使用 List 而不是ArrayList将消除在集合中存储整数时对装箱的需求。

程序

输出

Boxed Integer: 42

说明

命名空间和类声明

  • 代码以using System;语句开头,它允许您使用 System 命名空间中的类型和成员。
  • 类 Program 声明定义了一个名为 Program 的类。在这个类中,有一个名为Main的静态方法。这个Main 方法作为程序的入口点。

主方法

  • static void Main():它是程序的入口点。它是一个不返回值 (void) 的静态方法。

装箱

  • int num = 42;:这里,我们声明一个名为num的整数变量,并将其初始化为 42。num是一个值类型变量,因为 int 在 C# 中是一个值类型。它通常存储在栈上。
  • object boxedNum = num:在此行中,我们执行装箱功能。我们将num 变量的值分配给一个名为boxedNum的对象变量。此操作有效地将int 值转换为对象。结果,boxedNum现在包含对装箱整数对象的引用。

输出

  • Console.WriteLine("Boxed Integer: " + boxedNum);:在此行中,我们使用Console.WriteLineboxedNum的值打印到控制台。由于 boxedNum 现在是一个对象,我们将其与字符串"Boxed Integer: "连接以创建输出。

复杂度分析

时间复杂度:此代码的时间复杂度非常低,可以认为是常量或O(1)。这是因为代码主要由变量声明、赋值和单个Console.WriteLine语句组成。这些操作需要恒定时间,并且执行时间不取决于任何输入数据的大小或任何循环迭代。

空间复杂度:此代码的空间复杂度也非常低,可以认为是常量或O(1)

我们声明并初始化一个整数变量num,它占用固定量的内存来存储整数值,而与值本身无关。此内存使用是常量,不取决于输入大小或程序逻辑。

我们创建一个对象变量boxedNum来存储装箱的整数。它也消耗恒定量的内存,因为它只是堆上对象的引用,并且对象引用的大小是固定的。

Console.WriteLine 语句将字符串输出到控制台。但是,此操作使用的内存量不取决于输入的大小或任何其他动态因素。

什么是拆箱?

C# 中的拆箱是将引用类型(通常是对象)转换回其原始值类型的过程。此操作与装箱相反,装箱涉及将值类型转换为对象。当您之前已经装箱了一个值类型并且现在需要检索原始值时,拆箱是必需的。

拆箱的必要性

当您将值类型存储在对象或其他引用类型中并希望将其作为其原始值类型提取时,需要拆箱。这很重要,因为值类型和引用类型具有不同的存储位置和行为。

拆箱语法

拆箱使用显式转换执行。您在括号中指定目标值类型,后跟包含装箱值的引用变量。

在此示例中,(int)是显式转换,它告诉编译器拆箱boxedReference并将其解释为整数。

运行时类型检查

拆箱涉及运行时类型检查,以确保要拆箱的对象确实与指定的值类型兼容。如果类型不匹配,则会在运行时抛出InvalidCastException。此检查对于类型安全至关重要。

例如

避免拆箱异常

您应该确保只拆箱到正确的D目标值类型,以避免拆箱时出现 InvalidCastException。您可以通过以下方法实现:

  • 在拆箱之前使用 is 运算符或 as 运算符结合null 检查来检查装箱对象的类型。
  • 使用模式匹配安全地拆箱。

性能考虑

拆箱,与装箱一样,涉及运行时类型检查并可能带来性能成本。了解这些成本并谨慎使用拆箱至关重要,尤其是在性能关键的代码中。

程序

输出

Boxed Integer: 42
Unboxed Integer: 42

说明

  • 代码以`using System;`语句开头,它允许您使用 System 命名空间中的类型和成员。
  • 类 Program 声明定义了一个名为Program的类。在这个类中,有一个名为Main的静态方法。Main 方法作为程序的入口点。
  • static void Main(): 它是程序的入口点。它是一个不返回值 (void) 的静态方法。
  • int num = 42;: 我们声明一个名为num的整数变量,并将其初始化为值 42。num 是一个值类型变量,因为 int 在 C# 中是一个值类型。它通常存储在栈上。
  • object boxedNum = num: 在此行中,我们执行装箱。我们将 num 变量的值分配给一个名为boxedNum的对象变量。此操作有效地将 int 值转换为对象。boxedNum现在包含对装箱整数对象的引用。
  • int unboxedNum = (int)boxedNum;: 此行演示了拆箱。我们获取boxedNum 对象,并使用(int)boxedNum将其显式转换回 int。此拆箱操作从装箱对象中检索原始 int 值,并将其存储在 unboxedNum 变量中。
  • Console.WriteLine("Boxed Integer: " + boxedNum);: 我们使用Console.WriteLineboxedNum的值打印到控制台。由于 boxedNum 现在是一个对象,我们将其与字符串"Boxed Integer: "连接以创建输出。
  • Console.WriteLine("Unboxed Integer: " + unboxedNum);: 同样,我们将unboxedNum的值打印到控制台,这次将其与字符串 "Unboxed Integer: " 连接。

复杂度分析

时间复杂度

此代码的时间复杂度相对简单,可以认为是常量或O(1)。这意味着程序的执行时间不取决于任何输入数据的大小或任何循环。

代码主要由变量声明、赋值和打印语句组成,所有这些都是基本操作,需要恒定时间。这些操作不涉及迭代或递归,并且不受任何数据结构大小的影响。

装箱和拆箱操作本身也具有恒定时间复杂度,因为它们涉及复制或转换值,并且不取决于输入大小。

空间复杂度

此代码的空间复杂度也相当低,可以认为是常量或O(1)。这意味着程序的内存使用量不会随着任何输入数据的大小或任何循环而增长。

为一些变量分配内存,包括num、boxedNumunboxedNum。但是,这些变量的内存使用量是恒定的,不取决于任何数据结构或输入的大小。

此外,基本操作(如变量赋值和打印语句)的内存使用量也是恒定的。

装箱和拆箱之间的主要区别

C# 中的装箱拆箱之间存在一些主要区别。装箱和拆箱之间的一些主要区别如下:

装箱

转换

目的:装箱是将值类型转换为引用类型(通常是对象)的过程。

隐式:装箱通常是隐式的,这意味着它可以自动发生,而无需显式转换。

类型兼容性

类型安全:装箱通常是类型安全的,因为编译器确保值类型可以装箱为引用类型。

类型信息:装箱对象使用运行时类型标记保留有关原始值类型的信息。

性能

开销:装箱涉及在上分配内存并将值复制到新创建的对象中。它可能会引入性能开销,尤其是在频繁执行时。

堆分配:装箱对象在托管堆上分配并受垃圾回收。

存储位置

堆存储:装箱涉及将值类型存储在托管堆上,创建一个新对象。之后,此对象的引用被分配给引用变量

间接访问:装箱后,您通过引用变量访问值,该变量指向堆上的装箱对象。

值复制

值复制:您对原始值类型变量所做的任何更改都不会影响装箱对象,反之亦然。这就像复制某个东西并将其放入单独的盒子中,改变一个不会改变另一个。

不可变:装箱对象是不可变的。因此,您无法修改其值。

拆箱

转换

目的:拆箱是将引用类型(通常是对象)转换回其原始值类型的过程。

显式:拆箱需要显式转换,指定目标值类型。

类型兼容性

类型安全:拆箱可能涉及运行时类型检查以确保拆箱有效。类型不匹配可能导致InvalidCastException

转换:您必须将引用类型转换为正确的值类型才能成功执行拆箱。

性能

类型检查:拆箱涉及运行时类型检查,这可能会带来性能成本。

类型转换:如果类型匹配,拆箱需要将值从装箱对象复制到值类型变量。

存储位置

栈存储:拆箱就像将一个放在盒子中(在堆上)的值取出来,然后将其移回一个常规位置(值类型变量),以便您可以直接使用它。原始盒子(在堆上)和您移动的东西(到栈上)是分开的,所以改变一个不会影响另一个。

直接访问:拆箱后,您通过值类型变量直接访问值。

值复制

值复制:对值类型变量所做的任何更改都不会影响原始装箱对象,反之亦然。这就像从盒子中取出东西,对其进行更改,但盒子本身保持不变。

可变性:未装箱的值类型变量可以修改。