首页 > 其他分享 >装箱拆箱(boxing and unboxing)

装箱拆箱(boxing and unboxing)

时间:2024-09-02 21:14:30浏览次数:10  
标签:拆箱 Int32 boxing Point unboxing IL 类型 装箱

1.引用类型和值类型

为了理解装箱和拆箱,首先需要了解值类型和引用类型的特点。

  • 引用类型:
    • 必须从托管堆分配
    • 每个对象有一些额外成员,这些成员必须初始化
    • 对象中其它字节总是为0
    • 从托管堆分配对象,可能强制执行一次垃圾回收

从引用类型的特点我们可以知道,如果所有类型都是引用类型,由于内存分配和垃圾回收等原因的存在,那么内存管理和性能开销将会非常大。因此,C#语言提供了值类型来优化。

  • 值类型
    • 一般在线程栈上分配
    • 变量中不包含指向实例的指针

由于值类型对象是在栈上分配,因此值类型对象的分配和回收都是比引用类型对象更高效,因为栈上内存分配和回收只需要移动栈指针即可。C#中提供了许多内置的值类型,如int、float、double等。

C#是通过struct和class关键字来区分值类型和引用类型的,struct定义值类型,class定义引用类型。注意这点与C++不同,C++中struct和class只表示类中成员的默认访问权限是public还是private。C++中指定在栈上还是堆中分配内存是通过初始化变量的方式来确定的,使用new运算符则表示在堆上分配内存。也就是说C#中是定义类型的开发者决定在什么地方分配内存,而C++中是使用类型的开发者来决定。

2.装箱和拆箱

2.1 装箱(boxing)

装箱就是把值类型转换成引用类型的机制。 装箱发生的时候,会在托管堆上重新分配内存新建一个对象,值类型的字段会复制到新分配的内存上。因此操作装箱后的对象对原始的值类型对象不会产生影响。

那么什么情况下会用到装箱机制呢,或者什么情况下需要把一个值类型转换成引用类型呢?一种常见的情况是将值类型传递给需要引用类型参数的方法时,例如当一个方法需要一个object类型的参数,而你传递的是一个值类型时,就会发生装箱操作。

class Program
{
    static void Main(string[] args)
    {
        ArrayList arrayList = new ArrayList();
        Point p = new Point { X = 10, Y = 20 };
        arrayList.Add(p); // 发生装箱,把引用添加到ArrayList中
    }
}

public struct Point
{
    public int X, Y;
}

例如上面的代码,ArrayList的Add函数接口如下,它接收的参数类型是Object,是一个引用。因此调用ArrayList的Add方法添加一个值类型对象到ArrayList中时,会发生装箱操作。

public virtual int Add(object? value);

2.2 拆箱(unboxing)

与装箱对应就是拆箱,拆箱就是把装箱后的值类型从引用类型转换回原始的值类型。 注意拆箱操作不要求在内存中复制任何字节,而是获取对象中原始值类型指针的过程。但是往往在拆箱之后会有一次复制的操作把拆箱后的对象赋值给一个值类型对象。

Point p1 = (Point)arrayList[0];

例如上面代码中的(Point)arrayList[0]就是一个拆箱操作,把arrayList[0]中的引用类型转换回值类型Point。

3.注意事项

3.1 减少装箱拆箱

从上述描述我们可以知道,装箱拆箱往往伴随着内存分配和数据拷贝操作,因此编写代码过程中应该注意尽量减少装箱拆箱。

看下面这个例子:

static void Main(string[] args)
{
   Int32 a = 5;
   Console.WriteLine("{0}, {1}, {2}", a, a, a); // 发生三次装箱
}

使用ildasm.exe可以看到上述代码生成的IL代码:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // 代码大小       33 (0x21)
  .maxstack  4
  .locals init (int32 V_0)
  IL_0000:  nop
  IL_0001:  ldc.i4.5
  IL_0002:  stloc.0
  IL_0003:  ldstr      "{0}, {1}, {2}"
  IL_0008:  ldloc.0
  IL_0009:  box        [mscorlib]System.Int32
  IL_000e:  ldloc.0
  IL_000f:  box        [mscorlib]System.Int32
  IL_0014:  ldloc.0
  IL_0015:  box        [mscorlib]System.Int32
  IL_001a:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                object,
                                                                object,
                                                                object)
  IL_001f:  nop
  IL_0020:  ret
} // end of method Program::Main

可以看到IL代码中有3次box即装箱操作,而我们又不需要修改这三个不同的装箱对象,因此这里可以提前手动装箱,这样可以减少两次装箱操作。
代码如下:

static void Main(string[] args)
{
   Int32 a = 5;
   Object o = a;
   Console.WriteLine("{0}, {1}, {2}", o, o, o);
}

3.2 其它装箱情况

  • 值类型对象没有类型对象指针,但是调用重写的虚方法时不需要装箱,因为值类型是sealed,因此调用的虚方法一定就是重写的虚方法。而如果调用继承的方法(比如GetTypeMemberwiseClone)和没有重写的虚方法时,就需要装箱。因为这些方法在System.Object中定义,需要接收一个this实参,即指向堆对象的指针。
  • 值类型转型为类型的某个接口时需要装箱,因为接口变量必须包含对堆对象的引用

3.3 使用接口更改已装箱对象中的字段

自定义的值类型无法继承其它类,但是可以实现接口,因此如果接口提供了修改内部字段的方法,那么就可以通过把装箱对象转成该接口然后通过该方法来修改内部字段,但是非常不推荐这种,值类型应该是不可变的

代码如下:

namespace HelloWorld
{
    sealed class Program
    {
        static void Main(string[] args)
        {
            Point p = new Point(1, 1);
            Console.WriteLine(p); //(1,1)

            p.Change(2, 2);
            Console.WriteLine(p); //(2,2)

            object o = p;
            Console.WriteLine(o); //(2,2)

            ((Point) o).Change(3, 3);
            Console.WriteLine(o); //(2,2)

            ((IChangeable) o).Change(3, 3);
            Console.WriteLine(o); //(3,3)
        }
    }
}

internal interface IChangeable
{
    public void Change(Int32 x, Int32 y);
}

internal struct Point : IChangeable
{
    private Int32 m_x, m_y;
    public Point(Int32 x, Int32 y)
    {
        m_x = x;
        m_y = y;
    }
    public void Change(Int32 x, Int32 y)
    {
        m_x = x;
        m_y = y;
    }
    public override String ToString()
    {
        return String.Format("({0},{1})", m_x.ToString(), m_y.ToString());
    }
}

值得注意的是, ((Point) o).Change(3, 3);这行代码实际上并不会修改o中的字段,因为这里会先拆箱再装箱,产生一个新的Point对象,修改的是这个新的Point对象的字段,而不是o中的字段。但是通过把o转换成IChangeable接口,然后调用Change方法,就可以修改其内部字段。转化接口的过程没有拆箱,因此修改的就是对象o中的字段。

3.4 值类型应该是不可变的

通过3.3中的例子我们可以看到,如果一个值类型中的字段是可变的,我们需要高度关注每个装箱和拆箱过程,避免发生预期之外的错误。如果值类型是不可变的,那么我们就不用过多关心什么时候发生了装箱和拆箱(当然仍然需要关注太多装箱拆箱产生的性能问题)

目前FCL(Framework Class Library)的核心值类型 (Int32、Int64、Int64、UInt64、Single、Double、Decimal、Boolean等) 都是不可变的,例如我们修改一个Int32的变量的值并不是修改这个变量的内部值,而是新建了一个Int32对象并赋值给这个变量。

我们可以用如下方式创建一个不可变的类型,如果需要修改内部值,我们通过创建一个新的实例来代替修改:

public struct ImmutablePoint
{
    public readonly int X;
    public readonly int Y;

    public ImmutablePoint(int x, int y)
    {
        X = x;
        Y = y;
    }

    public ImmutablePoint Move(int dx, int dy)
    {
        return new ImmutablePoint(X + dx, Y + dy);
    }
}

标签:拆箱,Int32,boxing,Point,unboxing,IL,类型,装箱
From: https://www.cnblogs.com/wenxuanh/p/18393552

相关文章

  • 值类型和引用类型、装箱和拆箱、静态类和普通类、方法的重载、继承和多态、访问修饰符
    目录一、值类型和引用类型的区别?值类型(ValueTypes)定义:特点:示例:引用类型(ReferenceTypes)定义:特点:示例:举例说明:总结:二、装箱和拆箱装箱(Boxing)特点:示例:拆箱(Unboxing)特点:示例:示例代码:装箱和拆箱的影响最佳实践:三、静态类和普通类的区别?静态类(Static......
  • c#优化装箱拆箱
    1、通过泛型//obj是一个int类型的值类型,在newTest的时候传进去的obj是就会装箱成引用类型,以为Test类是引用类型intobj=2;Testtest=newTest(obj);//通过泛型这里obj传进去的就是值类型,就不需要装箱了Test<int>test=newTest<int>(obj);第一段代码中会发生装箱,因......
  • 值类型和引用类型、装箱和拆箱、静态类和普通类、方法的重载、继承和多态
    目录值类型和引用类型的区别?值类型(ValueTypes)定义:特点:示例:引用类型(ReferenceTypes)定义:特点:示例:举例说明:总结:装箱和拆箱装箱(Boxing)特点:示例:拆箱(Unboxing)特点:示例:示例代码:装箱和拆箱的影响最佳实践:静态类和普通类的区别?静态类(StaticClass)普通......
  • Java中的装箱与拆箱详解
    Java中的装箱与拆箱详解大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!装箱与拆箱的基本概念在Java中,装箱(Boxing)和拆箱(Unboxing)是指将基本数据类型与它们对应的包装类之间进行相互转换的过程。Java为每种基本数据类型提供了对应的包装类,如Integer对应in......
  • 拆箱和装箱
    //手动装箱Integerinteger=newInteger(10);//手动拆箱inti=integer.intValue();//自动装箱,通过Integer.valueOf()完成Integerinteger=10;//自动拆箱,通过Integer.intValue()完成inti=integer;IntegerCachepublicstaticIntegervalueOf(inti)......
  • Java基础进阶——128陷阱(剖析Integer类的自动拆箱和装箱)
    一、什么是128陷阱?下面用一段代码展示了什么是128陷阱:publicstaticvoidmain(String[]args){Integera=10;Integerb=10;Integeraa=127;Integerbb=127;Integeraaa=128;Integerbbb=128;......
  • Java 自动装箱跟拆箱
    ava的自动装箱和自动拆箱是Java5引入的特性,它们简化了基本数据类型和其对应的包装类之间的转换。下面是关于这两个特性的详细解释:自动装箱(Autoboxing)自动装箱指的是Java编译器自动将基本数据类型转换为其对应的包装类类型。例如,当你将一个int类型的值赋给一个Integer类型的......
  • 内存优化:Boxing
    dotMemory如今,许多开发人员都熟悉性能分析的工作流程:在分析器下运行应用程序,测量方法的执行时间,识别占用时间较多的方法,并致力于优化它们。然而,这种情况并没有涵盖到一个重要的性能指标:应用程序多次GC所分配的时间。当然,你可以评估GC所需的总时间,但是它从哪里来,如何减少呢?“普通......
  • Integer 自动拆箱封箱
    Integer自动拆箱封箱验证。先写一份Integer Double代码  思考一:这几个值true?false?结果是:truefalsefalsefalse思考二:为什么第一个为true,其他都是false? 理由在这里:Integer 自动拆箱和装箱判断 if (i >= IntegerCache.low && i <= IntegerCache......
  • 引用类型,值类型,装箱拆箱
    usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;namespace引用类型{classProgram{classDataTypeTest{publicintVal;}staticvoidMain(string[]args){......