浅谈C sharp中的值类型和引用类型
1. 值类型
- 常见的值类型:int/long/short/byte/float/double/bool/char/Struct(用户建立的结构体通常是值类型的)/Nullable Types(这是一个特殊的值类型,表示一个正常值或者空,比如
int?
) - 值类型的例子:
1
2
3
4
5
6
7
8
9
10int a=10;
int b=a;
Console.WriteLine($"a:{a}");//a:10
Console.WriteLine($"b:{b}");//b:10
b=20;
Console.WriteLine($"a:{a}");//a:10,原始值不受影响
Console.WriteLine($"b:{b}");//b:20,只有b的值改变了 - 值类型直接存储在内存(称之为栈(STACK),栈以LIFO访问,后进栈的数据先被访问,栈的大小是固定的,不是动态分配的,所以访问速度快)中,当把一个值赋值给另外一个变量时,其实是把变量的值复制给了新的变量,而不会改变原有值(
a=10;b=a;b=20;
这个例子中并不会因为b变成20了就反过来使a也变成20了,因为这个过程是复制,副本虽然变了,但是a=10这个原始值一直没有被改变。) - 当一个方法传递值类型的参数时(包括结构体),会将参数的值复制到函数的参数中,对参数的修改不会影响到原始变量;
2. 引用类型
- 类,接口,委托,数组,字符串(字符串比较特殊,他可以像值类型一样用,但是它又具有
不可变性
。) - 用new动态分配内存,由GC(垃圾回收器)释放
- 引用类型实际上操作的是地址。在C#中,你需要获取引用类型实例的地址只需要用
&
,如下1
2string str = "Hi";
IntPtr address = new IntPtr(&str) - 但是当想要在值类型实例上获得地址,就变得很困难,你可能需要先把该值类型封装在一个引用类型(比如元素是值类型的数组类型)中,然后再获取该引用类型的地址。
- 在传参时使用
ref
,如下,ref实际上传入的不是值类型的值(副本),而是值类型的引用(地址):1
2
3
4
5
6
7
8
9
10
11
12public static void ModifyValue(ref int num)
{
num = 42;
}
public static void Main()
{
int value = 10;
Console.WriteLine(Value);// output 10
ModifyValue(ref value);
Console.WriteLine(value);// output 42
} - 引用类型存储在内存的
堆(Heap)
,动态分配,当在堆上分配了实例之后(new之后),访问该实例实际上是通过访问该实例的内存地址来访问该实例的。
3. 由值类型和引用类型不同引发的问题案例
- 如下,有一个方法,方法尝试把一个字符串切分后的值准确的赋值给结构体:
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
33
34
35
36
37
38
39
40
41
42public static T ParseString2Struct<T>(string in_str) where T : struct
{
T result = default(T);
//Type type = result.GetType();
//object clone_result = Activator.CreateInstance(type);
FieldInfo[] fields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public);
object parsedValue;
var lines = in_str.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);
if (lines.Length != fields.Length)
{
throw new ArgumentException("richtextbox string length does not match the structer.");
}
for (int i = 0; i < fields.Length; i++)
{
var lineParts = lines[i].Split(':');
if (lineParts.Length != 2 || lineParts[0].Trim() != fields[i].Name)
{
throw new ArgumentException("richtextbox string format does not match the structer.");
}
var value = lineParts[1].Trim();
var fieldType = fields[i].FieldType;
try
{
parsedValue = Convert.ChangeType(value, fieldType);
}
catch (Exception)
{
throw new ArgumentException("richtextbox string does not match the structer.");
}
if (!fields[i].IsInitOnly)
{
fields[i].SetValue(result, parsedValue);
}
}
//result = (T)clone_result;
return result;
}
} - 在这个方法里,使用
T result = default(T);
初始化值类型,这里不管是使用default(T)
还是new T()
其实本质都是获取了一个全新的值类型副本,但是default和new之间有一些小区别:用default的时候不会去获取结构构造函数中的初始值,而是直接使用该字段的数据类型的默认初始值。用new的时候程序会去扫描并使用该结构体构造函数中的初始值 - 下面这句话用于调用该方法:
1
Ds64_65 ds6465 = DataSet_lib.ParseString2Struct<Ds64_65>(str);
- 实际调试中发现一个很奇怪的现象,不管parsedValue值是多少,result的字段无论怎样都不能被赋值:
FieldInfo[] fields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public);
用来设定结构体字段的public属性。fields[i].IsInitOnly
用来检查每个字段都没有设置只读属性。- 为了检查FieldInfo[]类中的方法是否适用,甚至通过修改结构体构造函数的初始值再用new T()创建一个新的初始结构体,然后使用GetValue()方法,可以正常获得初始化值。
- 通过询问AI才知道SetValue()方法对于值类型的注意点:
- 1.值传递是传递的字段的副本,副本的改变对字段本身的值没有影响。
- 2.
传递的值和字段类型是兼容的,不兼容会抛异常,这一条上面的方法可以确保。 - 3.对于结构体中的字段,SetValue()方法只会修改字段的副本,而不是原始结构体实例。意味着在使用SetValue()修改结构体之后,需要将修改后的副本重新赋值给原始结构体实例.
- 但SetValue()这个方法本身又没有包含
ref
类型的重载,所以不能靠ref。 - 对于修改建议,AI建议是使用
SetValueDirect
:__makeref()
:用于获取结构体字段的引用,底层特性,不建议直接使用1
typeof(T).GetFields()[i].SetValueDirect(__makeref(result),parsedValue)
- 通过GitHub上参考的代码, 找到一个更合适的解决方法,就是创建一个Clone,再把Clone赋值回去,如下:
- 注意此方法生成的
object clone_result
,它是一个object类型,引用类型。那就不存在上面值类型放进SetValue()里的那些问题。最后再强转回T,给到result输出。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
33
34
35
36
37
38
39
40
41public static T ParseString2Struct<T>(string in_str) where T : struct
{
T result = default(T);
Type type = result.GetType();
object clone_result = Activator.CreateInstance(type);
FieldInfo[] fields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public);
object parsedValue;
var lines = in_str.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries);
if (lines.Length != fields.Length)
{
throw new ArgumentException("richtextbox string length does not match the structer.");
}
for (int i = 0; i < fields.Length; i++)
{
var lineParts = lines[i].Split(':');
if (lineParts.Length != 2 || lineParts[0].Trim() != fields[i].Name)
{
throw new ArgumentException("richtextbox string format does not match the structer.");
}
var value = lineParts[1].Trim();
var fieldType = fields[i].FieldType;
try
{
parsedValue = Convert.ChangeType(value, fieldType);
}
catch (Exception)
{
throw new ArgumentException("richtextbox string does not match the structer.");
}
if (!fields[i].IsInitOnly)
{
fields[i].SetValue(clone_result, parsedValue);
}
}
result = (T)clone_result;
return result;
}
- 注意此方法生成的
- Clone可以成功解决该问题。
浅谈C sharp中的值类型和引用类型
http://example.com/2024/08/27/浅谈C sharp中的值类型和引用类型/