C# Struct的值拷贝的问题

概述

C#中的Struct是值类型数据结构,在使用时常发生值拷贝。

公用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public struct ChildStruct
{
public int Value;
}

public class ChildClass
{
public int Value;
}

public struct ParentStruct
{
public int Value;
public ChildStruct ChildStruct;
public ChildClass ChildClass;
}

作为参数进行值拷贝

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void Main(string[] args)
{
ParentStruct parentStruct1 = new ParentStruct();
parentStruct1.Value = 1;
Console.WriteLine(parentStruct1.Value);

CopyValue1(parentStruct1);

Console.WriteLine(parentStruct1.Value);
}

public static void CopyValue1(ParentStruct parentStruct)
{
parentStruct.Value = 2;
}

输出结果:

1
2
1
1

分析

函数CopyValue1中对传入参数parentStruct的修改,并不能影响函数外parentStruct1的值。所以认为Struct作为传入参数发生了值拷贝,函数内修改的与函数修改的Struct并非同一个引用。

改良

使用ref进行传参,这样不再传递Struct的值,而是传递Struct的引用来避免值拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void Main(string[] args)
{
ParentStruct parentStruct1 = new ParentStruct();
parentStruct1.Value = 1;
Console.WriteLine(parentStruct1.Value);

CopyValue1(ref parentStruct1);

Console.WriteLine(parentStruct1.Value);
}

public static void CopyValue1(ref ParentStruct parentStruct)
{
parentStruct.Value = 2;
}
1
2
1
2

return进行值拷贝

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void Main(string[] args)
{
ParentStruct parentStruct1 = new ParentStruct();
parentStruct1.Value = 1;

ParentStruct parentStruct2 = CopyValue2(ref parentStruct1);
parentStruct2.Value = 3;

Console.WriteLine(parentStruct1.Value);

Console.WriteLine(parentStruct2.Value);
}

public static ParentStruct CopyValue2(ref ParentStruct parentStruct)
{
parentStruct.Value = 2;
return parentStruct;
}

输出结果

1
2
2
3

分析

从之前的示例可以得知ref传参传递的是引用,因此认为函数内的parentStruct变量的与函数外的parentStruct1变量是同一个引用。

变量parentStruct2接收CopyValue2的返回值,并对parentStruct2中的值进行修改,但输出的结果发现parentStruct1中的值并未发生修改。

可以得出结论:函数的返回值也是值传递,并非引用传递。

改良

使用ref将返回的类型为引用类型。接收的变量也需要使用ref修饰。来避免值拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void Main(string[] args)
{
ParentStruct parentStruct1 = new ParentStruct();
parentStruct1.Value = 1;

ref ParentStruct parentStruct2 = ref CopyValue1(ref parentStruct1);
parentStruct2.Value = 3;

Console.WriteLine(parentStruct1.Value);

Console.WriteLine(parentStruct2.Value);
}

public static ref ParentStruct CopyValue1(ref ParentStruct parentStruct)
{
parentStruct.Value = 2;
return ref parentStruct;
}

1
2
3
3

Struct类型数组元素存为变量时进行值拷贝

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(string[] args)
{
ParentStruct[] structs = new ParentStruct[1];


structs[0] = new ParentStruct();
structs[0].Value = 1;

Console.WriteLine(structs[0].Value);

ParentStruct parentStruct0 = structs[0];

parentStruct0.Value = 2;

Console.WriteLine(structs[0].Value);
Console.WriteLine(parentStruct0.Value);
}

输出结果

1
2
3
1
1
2

分析

由输出的结果可以发现对于将Struct数组的元素存为变量parentStruct0后,对于parentStruct0的修改并不会改变数组中元素内的值,所以拷贝的是值而不是引用。

改良

使用ref修饰变量,这样变量存储的是数组元素的引用来避免值拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void Main(string[] args)
{
ParentStruct[] structs = new ParentStruct[1];


structs[0] = new ParentStruct();
structs[0].Value = 1;

Console.WriteLine(structs[0].Value);

ref ParentStruct parentStruct0 = ref structs[0];

parentStruct0.Value = 2;

Console.WriteLine(structs[0].Value);
Console.WriteLine(parentStruct0.Value);
}

函数内创建并返回的Struct避免值拷贝

示例代码

1
2
3
4
5
6
7
8
9
10
static void Main(string[] args)
{
ParentStruct parentStruct1 = CreateParentStruct();
}

public static ParentStruct CreateParentStruct()
{
ParentStruct parentStruct = new ParentStruct();
return parentStruct;
}

分析

从之前的结论可以得知函数返回值是值拷贝,所以使用out关键词来避免值拷贝

改良

1
2
3
4
5
6
7
8
9
10
static void Main(string[] args)
{
ParentStruct parentStruct1;
CreateParentStruct(out parentStruct1);
}

public static void CreateParentStruct(out ParentStruct parentStruct)
{
parentStruct = new ParentStruct();
}

Struct内的Struct类型成员与Class类型成员

ParentStruct是个Struct,内部有Struct类型成员变量ChildStructClass类型成员变量ChildClass。分别创建这两种类型的变量,初始化Value的值,并赋值给ParentStruct变量对应的成员。两种类型的变量的Value值,观察ParentStruct变量内部对应成员值的变化。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void Main(string[] args)
{
ParentStruct parentStruct1 = new ParentStruct();

ChildStruct childStruct1 = new ChildStruct();
childStruct1.Value = 1;

parentStruct1.ChildStruct = childStruct1;

Console.WriteLine($"childStruct1.Value:{childStruct1.Value}");

childStruct1.Value = 2;
Console.WriteLine("-------childStruct1.Value的值修改后-------");

Console.WriteLine($"childStruct1.Value:{childStruct1.Value}");
Console.WriteLine($"parentStruct1.ChildStruct.Value:{parentStruct1.ChildStruct.Value}");

Console.WriteLine("\n");

ChildClass childClass1 = new ChildClass();
childClass1.Value = 3;

parentStruct1.ChildClass = childClass1;

Console.WriteLine($"childClass1.Value:{childClass1.Value}");

childClass1.Value = 4;
Console.WriteLine("-------childClass1.Value的值修改后-------");

Console.WriteLine($"childClass1.Value:{childClass1.Value}");
Console.WriteLine($"parentStruct1.ChildClass.Value:{parentStruct1.ChildClass.Value}");
}

输出结果

1
2
3
4
5
6
7
8
9
10
childStruct1.Value:1
-------childStruct1.Value的值修改后-------
childStruct1.Value:2
parentStruct1.ChildStruct.Value:1


childClass1.Value:3
-------childClass1.Value的值修改后-------
childClass1.Value:4
parentStruct1.ChildClass.Value:4

分析

Struct的Struct成员存储的是成员内部的值,而Struct的Class成员存储的是成员的引用。并且在C#11之前的版本没法使用ref成员。因此Struct类型的成员没法使用享元模式。因为每次都会拷贝。

疑问

对于C#的ref关键词,个人认为ref在修饰参数时和作用与类型上时是不一样的。 修饰参数时表示参数是通过引用的方式来传递的,但没法传递一个数组的元素的引用。只能传递数组元素的值拷贝,在内部对引用的变更并不会影响到外部。对此感到疑惑,因此需要慎重在方法内对于ref或out修饰的参数的引用进行修改。