本文已参与「新人创作礼亅活动,一起开启掘金创作之路。
之前遇到一个结构体赋值拷贝的问题,是一个List<Struct> 的数据结构,想通过 list[index].fun() (fun())是结构体的一个方法,改变结构体的一个变量的值,最后发现list[index]的值并没有改变,还是原来的旧值
于是做了个实验,验证值类型和引用类型之间在修改值时的区别
public class RefType
{
public int x;
}
public struct ValType
{
public int x;
}
private void RVTest()
{
RefType r1 = new RefType();
ValType v1 = new ValType();
r1.x = 5;
v1.x = 5;
Console.WriteLine($"r1.x={r1.x} v1.x={v1.x}") // r1.x=5 v1.x=5
}
- 此时的内存分配情况,可以见到r1只是变量在栈上,而实际引用的值在堆上。而v1值直接存储在栈上
private void RVTest()
{
RefType r1 = new RefType();
ValType v1 = new ValType();
r1.x = 5;
v1.x = 5;
RefType r2 = r1;
ValType v2 = v1;
r2.x = 6;
v2.x = 6;
Console.WriteLine($"r1.x={r1.x} v1.x={v1.x}")// r1.x=6 v1.x=5
Console.WriteLine($"r2.x={r2.x} v2.x={v2.x}")// r2.x=6 v2.x=6
}
- 此时的内存分配情况
由图中可知,由于r1和r2是引用类型,同时引用托管堆上同一块内存,所以其中一个变量改变x的值,另一个也会受影响。而v1和v2是值类型,在v2=v1时,其实是进行一次数据拷贝,两个变量分别对应两个对象,所以其中一个修改也不会影响另一个
回到一开始遇到的问题上,由于我是直接在List里直接修改指定下标的数据,这时会copy出那份数据再进行修改,而不是修改list里的
public struct Point
{
public int x;
public Point(int x)
{
this.x = x;
}
public void Set(int v)
{
x = v;
}
}
List<Point> pointList = new List<Point>() { new Point(1) };
pointList[0].Set(2);
Console.WriteLine($"下标0 = {pointList[0].x}"); // 1
可以看出值x并没有改变,因为在pointList[0]在调用Set的时候也是拷贝出了一份数据副本
如果想改变list里的数据,可以换一种写法
List<Point> pointList = new List<Point>() { new Point(1) };
var data = pointList[0]; //把数据取出
data.Set(2);
pointList[0] = data; //把修改后数据再赋值回去
Console.WriteLine($"下标0 = {pointList[0].x}"); // 2
引用类型
创建对象并从托管堆分配内存,栈上的变量保存对象的引用,因此两个变量可以引用同一对象,其中一个变量执行操作会影响另一个拥有相同引用的变量。 引用类型包含
- class
- interface
- object
- array
值类型
值类型在线程栈上分配,值类型实例的变量不包含指向实例的指针,相反,变量包含实例本身的字段,所以操作实例中的字段不需要提领(“*”返回左值与指针地址处的值等效的值)指针 值类型包含
- int
- bool
- double
- char
//下面两种定义都能编译通过
ValType v1; //v1字段未初始化
ValType v2 = new ValType();//v2字段已初始化
int a = v1.x; //编译错误,因为x未被赋值
int b = v2.x; //编译正常,x=0
类与结构体的选择
- 类型实例较大时,成员变量较多,并且经常作为实参传递,优先选择类,因为在值类型赋值给另一个值类型时,需要将字段逐个复制,有性能损耗。
- 类型实例较小时,如果使用时机较短,可选择结构体。因为当该实例不在活动时,分配的存储会立刻释放,而不用等垃圾回收。
- 大多数情况下,值类型数组具有更好的引用局部性。数组元素如果是引用类型,元素其实是指向那些位于堆中的引用类型的实例,如果是值类型数组,元素的分配是内联的,这意味着数组元素是值类型的真正实例。
- 函数如果有值参数,并且在函数体内对参数修改,不会影响调用方传参所使用的变量的值。如果一定要用值类型做参数,又想修改参数原来的值,可以在该参数类型前面加上ref/out 关键字,它指示参数按引用传递,而非按值传递。但是引用传递不代表参数是引用类型,并且通过引用传递即使是值参数,也不会发生装箱