【C#学习笔记】装箱和拆箱

文章目录


装箱和拆箱

在讲引用类型object的时候,我们说它是万能的,却没说它万能在哪里。

除了object为每一种变量类型提供了ToString,GetHashCode,Equals,GetType方法之外,object作为所有类型的父类,它可以实现任意变量类型到object的转换

一方面,使用object类型可以显式转换到任意类型,(其中没有发生装箱拆箱):

csharp 复制代码
object arr=new int[10];
int[] ar=(int[])arr;

object arr=new int[10];
int[] ar=arr as int[]; // as强转object类型为int数组

另一方面,我们将一个值类型 转化为object(引用类型 )的过程称为装箱 ,而将装箱的object转化回值类型的过程称为拆箱

下例将整型变量 i 进行了装箱并分配给对象 o。

csharp 复制代码
int i = 123;
// The following line boxes i.
object o = i; //隐式装箱

然后,可以将对象 o 取消装箱并分配给整型变量 i:

csharp 复制代码
o = 123;
i = (int)o;  // unboxing

下面是官方示例,展示了如何使用一个object的list来存储不同值类型的数据并对其分别进行操作:

csharp 复制代码
Console.WriteLine(String.Concat("Answer", 42, true));
List<object> mixedList = new List<object>();
mixedList.Add("First Group:");

for (int j = 1; j < 5; j++)
{
    mixedList.Add(j);
}

mixedList.Add("Second Group:");
for (int j = 5; j < 10; j++)
{
    mixedList.Add(j);
}

foreach (var item in mixedList)
{
    Console.WriteLine(item);
}

var sum = 0;
for (var j = 1; j < 5; j++)
{
    sum += (int)mixedList[j] * (int)mixedList[j];
     // 注意在进行数值运算的时候不能用object直接运算,应当拆箱为原类型再计算
}
Console.WriteLine("Sum: " + sum);

// Output:
// Answer42True
// First Group:
// 1
// 2
// 3
// 4
// Second Group:
// 5
// 6
// 7
// 8
// 9
// Sum: 30

性能消耗

相对于简单的赋值而言,装箱和取消装箱过程需要进行大量的计算。 对值类型进行装箱时,必须分配并构造一个新对象。 取消装箱所需的强制转换也需要进行大量的计算,只是程度较轻。

实际上,在编程中应当尽量避免装箱和拆箱,除非真的必须要使用。

如果一个变量被装箱引用后还需要被拆箱引用,倒不如直接赋值一个新的变量,因为对值类型进行装箱时,必须创建一个全新的对象, 这可能比简单的引用赋值用时最多长 20 倍。 取消装箱的过程所需时间可达赋值操作的四倍。

如果一个变量,一个方法需要频繁装箱拆箱,说明本身程序设计就存在问题。例如定义了一个接受任意输入类型的函数,但是所有的输入值类型在方法内都会被强制转化为object类型。就比如上面例子中定义的List<object>,我们每存入一个值就要被装箱一次。实际上定义一个泛型List<T>是更好的选择,当我们需要接收任意值类型变量时,应当使用泛型来代替object对其他类型的装箱。例如下面的例子,显然前者更好:

csharp 复制代码
    public class Stack<T>
    {
        List<T> a = new List<T>();
    }
    public class Stack
    {
        List<object> b = new List<object>();
    }

装箱

装箱用于在垃圾回收堆中存储值类型。 装箱是值类型到 object 类型或到此值类型所实现的任何接口类型的隐式转换。 对值类型装箱会在堆中分配一个对象实例,并将该值复制到新的对象中。

此语句的结果是在堆栈上创建对象引用 o,而在堆上则引用 int 类型的值。 该值是赋给变量 i 的值类型值的一个副本。 以下装箱转换图说明了 i 和 o 这两个变量之间的差异:

(从上图中可以看到,原本i作为值类型存储在栈上,而object作为引用类型存储在堆上。当我们执行装箱操作的时候,一方面将值类型的装箱类型记录在了堆上,同时将堆上的object的值赋值为了123,并且在栈上同时创建了一个引用o用于引用堆上的object。)

这意味着在装箱的时候,object只是复制了i的值,而非它的地址本身,这也容易理解,因为值类型赋值的时候会重新创建一个地址,引用值类型是不可靠的:

csharp 复制代码
class TestBoxing
{
    static void Main()
    {
        int i = 123;
        // Boxing copies the value of i into object o.
        object o = i;
        // Change the value of i.
        i = 456;
        // The change in i doesn't affect the value stored in o.
        System.Console.WriteLine("The value-type value = {0}", i);
        System.Console.WriteLine("The object-type value = {0}", o);
    }
}
/* Output:
    The value-type value = 456
    The object-type value = 123
*/

拆箱

取消装箱是从 object 类型到值类型或从接口类型到实现该接口的值类型的显式转换。 取消装箱操作包括:

  • 检查对象实例,以确保它是给定值类型的装箱值。

  • 将该值从实例复制到值类型变量中。

从拆箱过程可以看出,实际上我们拆箱的时候,首先判断拆箱项o是否是对object的引用。然后判断拆箱类型是否是object的装箱类型,最后拆箱的值将会在栈中重新创建一个。

此外,拆箱也存在着隐式转换,但拆箱项类型必须相同,例如下面的例子:

csharp 复制代码
float t =(int) o; //拆箱项正确,拆出的int可直接隐式转换为folat
int t =(float) o; //拆箱项错误

根据上述原理,如果想要改变一个已经装箱的object的值,唯一的方法只有先拆箱,再重新装箱。


比较var,object,dynamic,<T>

这四种方法看起来比较类似,都可以接收任意类型,但实际区别很大。

var

var的原理是基于编译器,当我们用var来定义变量类型时,只能用于局部变量,并且是让编译器从初始化表达式推断出变量的类型。

csharp 复制代码
var a = 12;
a.IndexOf("1", 0, 2); //报错,编译器已经推测出a是int类型了,不能使用string的方法

var 的常见用途是用于构造函数调用表达式。当函数中的一些局部变量的赋值类型不明的时候,使用var来接收是安全的。使用var类型应当是最方便最随意的。

object

object虽然可以转化为任意类型,也可以通过装箱接收值类型的转换。优点是我们可以将object类型作为函数的返回类型,也可以在函数的入参中定义object类。但是它的优点也是它的缺点,假设函数里定义了object入参,鬼知道当别人使用函数的时候会传入什么东西进去,情况会变得越来越复杂。要么使用泛型,要么指定参数类型,在函数内部对其装箱。

因此,我们最好只在类型转换的时候使用object。如果想在函数定义一个可变类型,并且也不希望出现装箱拆箱,请使用<T>

<T> 泛型

泛型除了不能定义变量,其他都能定义。可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。还可以对泛型类进行约束以访问特定数据类型的方法。

一个简单的泛型例子就是集合类提供的List<T>。这个东西有多好用就不用讲了。

使用泛型在各种类或方法中获取任意类型。泛型不用装箱拆箱。你可以将泛型理解成替换,在使用的时候将泛型参数替换成具体的类型,这个过程是在编译的时候进行的,使用泛型,编译器依然能够检测出类型错误。

dynamic

dynamic本身也是一个Object。在大多数情况下,dynamic 类型与 object 类型的行为类似。 具体而言,任何非 Null 表达式都可以转换为 dynamic 类型。 dynamic 类型与 object 的不同之处在于,编译器不会对包含类型 dynamic 的表达式的操作进行解析或类型检查。 编译器将有关该操作信息打包在一起,之后这些信息会用于在运行时评估操作。 在此过程中,dynamic 类型的变量会编译为 object 类型的变量。 因此,dynamic 类型只在编译时存在,在运行时则不存在。

优点就是dynamic像大部分脚本语言一样是动态变量,一方面方便某些动态使用,另一方面也可以和脚本语言对接。

但是缺点也很明显,可能有时使用它写的程序想要debug就没那么简单了。同时也别把dynamic作为函数的参数或者返回值,它只会比object带来更麻烦的后果。

相关推荐
Nu11PointerException1 小时前
JAVA笔记 | ResponseBodyEmitter等异步流式接口快速学习
笔记·学习
亦枫Leonlew3 小时前
三维测量与建模笔记 - 3.3 张正友标定法
笔记·相机标定·三维重建·张正友标定法
考试宝3 小时前
国家宠物美容师职业技能等级评价(高级)理论考试题
经验分享·笔记·职场和发展·学习方法·业界资讯·宠物
黑叶白树5 小时前
简单的签到程序 python笔记
笔记·python
@小博的博客5 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
幸运超级加倍~5 小时前
软件设计师-上午题-15 计算机网络(5分)
笔记·计算机网络
南宫生5 小时前
贪心算法习题其四【力扣】【算法学习day.21】
学习·算法·leetcode·链表·贪心算法
懒惰才能让科技进步6 小时前
从零学习大模型(十二)-----基于梯度的重要性剪枝(Gradient-based Pruning)
人工智能·深度学习·学习·算法·chatgpt·transformer·剪枝
love_and_hope6 小时前
Pytorch学习--神经网络--搭建小实战(手撕CIFAR 10 model structure)和 Sequential 的使用
人工智能·pytorch·python·深度学习·学习