image-20220827164448734

值类型和引用类型的内存存放位置

值类型

  • 包含类型:
    • 简单类型:int,long,short,sbyte,uint,ulong,ushort,byte,char,float,double,decimal,bool
    • 枚举类型:enum
    • 结构体类型:struct
  • 值类型在相互赋值的时候,把内容拷贝给了对方
  • 值类型储存在栈空间,栈空间是系统自动分配、自动回收,小而快

image-20220827164610420

引用类型

  • 包含类型:
    • 类类型(Class):string,Console类,list,dictionary,自定义类
    • 数组类型:一维数组,多维数组,交错数组
    • 接口类型:interface
    • 委托类型:delegate
  • 引用类型的相互赋值是把两者都指向了一个位于堆空间的地址
  • 引用类型存储在堆空间,堆空间的内存需要手动的申请和释放,大而慢

image-20220827164647338

  • 特殊的引用类型string:
    • string虽然是引用类型,但是他具备值类型的特性:
      • 当我们将str1赋值给str2时,str2指向的同为str1在堆中的地址
      • 但是当我们为str2重新赋值时,会在堆中重新分配一个内存空间,str2存储的地址会变成新的地址
    • 注意:string在发生拼接或赋值时会在堆中分配新的内存空间,频繁改变string变量会产生内存垃圾,所以可以使用stringbuilder替代

image-20220827170702832

装箱和拆箱

.NET中,数据类型划分为值类型和引用(不等同于C++的指针)类型,与此对应,内存分配被分成了两种方式,一为栈,二为堆(托管堆)

装箱:

  • 装箱是将值类型转换为object类型或由此值类型实现的任何接口类型的过程。
  • 对值类型在堆中分配一个对象实例,并将该值复制到新的对象中。
  • 按三步进行:
    • 首先从托管堆中为新生成的引用对象分配内存(大小为值类型实例大小加上一个方法表指针和一个SyncBlockIndex)。
    • 然后将值类型的数据拷贝到刚刚分配的内存中。
    • 返回托管堆中新分配对象的地址,这个地址就是一个指向对象的引用了。
  • 可以看出,进行一次装箱要进行分配内存和拷贝数据这两项比较影响性能的操作。

拆箱:

  • 拆箱是将object类型到值类型或从接口类型到实现该接口的值类型的显式转换
  • 按两步进行:
    • 首先获取托管堆中属于值类型那部分字段的地址,这一步是严格意义上的拆箱。
    • 将引用对象中的值拷贝到位于线程堆栈上的值类型实例中
  • 经过这2步,可以认为是同装箱是互反操作。
  • 严格意义上的拆箱,并不影响性能,但伴随这之后的拷贝数据的操作就会同装箱操作中一样影响性能。

总结:

  • 装箱和拆箱都会对数据进行复制,所以会影响性能,最好使用泛型来减少装箱拆箱。
  • 在方法的形参操作中,值类型调用方法后不会更改原数据的值,引用类型则会更改数据的值。
  • 但是引用类型的string是具有不可变性的,例如在形参传递string类型的数据,该方式通过重新分配一个新的空间存储新数据实现的,所以string是特殊的引用类型。

问题: C#中int与string的类型转换涉及装箱拆箱吗?

  • int和string没有继承关系(它们都继承自object,但是互相没有继承关系),因此无法发生装箱与拆箱。
  • int转换成string时,会在堆上创建一个空间,把int在栈上的内容的字符复制一份放入堆上相对应的空间,而不是把int在栈上的内容复制一份放入堆上相对应的空间,注意内容的字符和内容的区别!

参考资料

C#值类型和引用类型

C#——值类型和引用类型

装箱和取消装箱(C# 编程指南)

【C#】Struct和Class区别