C#的值类型和引用类型解析
一、引用类型与值类型
值类型:直接存储其值,派生自System.ValueType,部署在栈上。值类型不包含null,值类型在声明后,不管是否已经赋值,编译器会为其分配内存。值类型通常在线程栈上分配(静态分配)。
引用类型:存储其值的引用,派生自Object类,部署在堆上。引用类型可以使用null,当声明引用类型时,只会在栈上分配一小片内存,用于存放一个地址。当进行实例化的时候(new)会在堆上分配空间,并会把堆上空间的地址存储到声明时开辟的那个栈上的空间中(动态分配)。
二、常见的值类型引用类型
值类型:byte,sbyte,short,int,long,float,double,decimal,char,uint,ushort,ulong,bool ,枚举类型,用户定义的结构体struct
引用类型:class、delegate、dynamic、interface、object(Object)、string(String)、内插字符串(这个样子的:$"Name = {name}, hours = {hours:hh}")。
特殊情况:
(1)数组。数组是引用类型,数组的对象在栈上。其每一个元素均分配在托管堆上。如果是值类型数组(int,float....)那么会在堆上对数组自动进行初始化。如果是引用类型数组,那么不会初始化任何元素。此时数组为null。
(2)嵌套类型:如果声明一个类,该类中包含值类型的字段,同时包含值类型的局部变量,那么它同样是引用类型。但是值类型字段和值类型局部变量位置是不同的。
例如:
class A
{
int a; //值类型字段
public A()
{
int b; //值类型局部变量
}
}
字段跟随实例存储,所以值类型字段a存储在托管堆上。但是这里的b是局部变量,它是值类型,它在栈上。
三、注意事项
1.值类型测试
using System;
namespace ValueAndReference
{
class Program
{
static void Main(string[] args)
{
int VT1, VT2; //值类型变量VT1,VT2
VT1 = 5;
VT2 = VT1;
VT1 = 6;
Console.WriteLine("VT1 is " + VT1 +" VT2 is " + VT2); //传递的只是值
}
}
}
这个没啥可注意的,很正常。
2.引用类型测试(class)
using System;
namespace ValueAndReference
{
//引用类型定义
class ReferenceType
{
public int field; //定义字段
public ReferenceType(int v) //构造函数,初始化field
{
field = v;
}
}
class Program
{
static void Main(string[] args)
{
ReferenceType RT1 = new ReferenceType(5); //定义并实例化引用类型对象RT1,初始字段值为5
ReferenceType RT2 = RT1;
Console.WriteLine("RT1 is " + RT1.field +",RT2 is " + RT2.field); //输出RT1、RT2字段值,均为5
RT2 = new ReferenceType(6); //实例化RT2,并初始字段值为6
Console.WriteLine("RT1 is " + RT1.field +",RT2 is " + RT2.field); //输出RT1字段值为5、RT2字段值为6
ReferenceType RT3 = new ReferenceType(5); //定义并实例化引用类型对象RT3,初始字段值为5
ReferenceType RT4 = RT3;
Console.WriteLine("RT3 is "+RT3.field+",RT4 is "+RT4.field); //输出RT3、RT4字段值,均为5
RT4.field = 6; //更改RT3的字段值为6
Console.WriteLine("RT3 is " + RT3.field +",RT4 is " + RT4.field); //输出RT3、RT4字段值,均为6
}
}
}
这个比较需要注意,RT1的field字段初始为5,然后RT1赋值给RT2,由于RT2是引用变量,那么修改RT2就是修改RT1,可是为什么RT1在后边没变化呢?这是因为这种情况下RT1和RT2指向的不是一个堆的地址,具体可以看下边的图。RT3和RT4是正常的情况,修改了RT4的值,RT3同时跟着变化。关于这两种情况其实很简单,而且这个很好的反应了引用类型的性质。为什么第一种情况不行呢?是因为第一种情况改变了引用对象本身,重新进行了实例化,那么就是重新声明了一个新的实例。而第二种情况改变的是对象的属性,两个对象还是对应的同一个实例。看下面两幅图:
第一种情况:
第一次实例化RT2时拷贝的RT1,此时RT1、RT2是一个对象。接下来的实例化是对RT2本身的改变,并且在这里是会重新分配RT2地址的。所以这个RT2是一个全新的实例,与RT1没有任何联系。所以它再怎么改字段的值也不会在影响RT1的字段。
第二种情况:
这里修改的只是字段,所以修改后并没有产生新的实例,那么RT1、RT2对应的还是一个实例。修改任何一个的字段值,另外一个会同时变化。
以上这些情况同样适用于方法中的参数,如果在方法中重新实例了对象,那么原始的对象在方法结束时不会做出任何改变,如果仅是改变对象实例的字段,那么没有任何问题,可以修改。如果想要在方法中对对象的实例本身做修改,使用ref传递参数,这个可以做到。
3.引用类型测试(string)
using System;
using System.Runtime.InteropServices;
namespace ValueAndReference
{
class Program
{
static void Main(string[] args)
{
string STR1 ="aaaaa";
string STR2 = STR1;
Console.WriteLine("STR1 is " + STR1+",STR2 is "+STR2); //输出STT1、STR2
STR2 = "bbbbb";
Console.WriteLine("STR1 is " + STR1 +",STR2 is " + STR2); //输出STT1、STR2
}
}
}
最后在说下这个string(String),在这里STR1并没有随着STR2的变化而变化,这么看起来它确实像值类型。但实际上它确实是引用类型。为什么出现这种情况那,这是因为string做了运算符重载这样看起来是string更像是字符串。可以把它这个赋值理解为new,这样就和上边的class一样了,所以这种情况是正常的。