C#---类与结构体-引用与拷贝

499 阅读3分钟

本文已参与「新人创作礼亅活动,一起开启掘金创作之路。

之前遇到一个结构体赋值拷贝的问题,是一个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

类与结构体的选择

  1. 类型实例较大时,成员变量较多,并且经常作为实参传递,优先选择类,因为在值类型赋值给另一个值类型时,需要将字段逐个复制,有性能损耗。
  2. 类型实例较小时,如果使用时机较短,可选择结构体。因为当该实例不在活动时,分配的存储会立刻释放,而不用等垃圾回收。
  3. 大多数情况下,值类型数组具有更好的引用局部性。数组元素如果是引用类型,元素其实是指向那些位于堆中的引用类型的实例,如果是值类型数组,元素的分配是内联的,这意味着数组元素是值类型的真正实例。
  4. 函数如果有值参数,并且在函数体内对参数修改,不会影响调用方传参所使用的变量的值。如果一定要用值类型做参数,又想修改参数原来的值,可以在该参数类型前面加上ref/out 关键字,它指示参数按引用传递,而非按值传递。但是引用传递不代表参数是引用类型,并且通过引用传递即使是值参数,也不会发生装箱