C#中的值类型和引用类型
工作之外暂没有可以上手写的东西,这周主要内容还是对C#一些关于类型的知识进行巩固,涉及到的书籍主要是深入理解C#(第3版)
值类型和引用类型
先从现实生活中的值和引用来讨论这点:
假设你正在读一份报纸,觉得里面的内容很棒,希望一个朋友也去读,影印了报纸的全部内容并交给他。届时,他将获得属于他自己的一份完整的报纸。在这种情况下,我们处理的是值类型的行为。所有信息都在你的手上,不需要从任何其他地方获得。制作了副本之后,你的这份信息和朋友的那份是各自独立的。可以在自己的报纸上添加一些注解,他的报纸根本不会改变。
再假设你正在读的是一个网页。与前一次相比,这一次,唯一需要给朋友的就是网页的URL。这是引用类型的行为,URL代替引用。为了真正读到文档,必须在浏览器中输入URL,并要求它加载网页来导航引用。另一方面,加入网页由于某种原因发生了变化(如一个维基页面,你在上面添加了自己的注释),你和你的朋友下次载入页面时,都会看到那个改变。
.NET中大多数类型都是引用类型
类(使用class来声明)是引用类型,而结构(使用struct)来声明是值类型。特殊情况包括: 1.数组类型是引用类型,即使元素类型是值类型(所以即便int是值类型,int[]仍是引用类型); 2.枚举(使用enum来声明)是值类型; 3.委托类型(使用delegate来声明)是引用类型; 4.接口类型(使用interface来声明)是引用类型,但可由值类型实现。
那么,为什么要分为值类型和引用类型呢:
引用类型总是从托管堆分配,C#的new操作符返回对象的内存地址,如果所有类型都是引用类型,则程序在运行的过程中,需要进行很多次内存分配,会显著影响程序性能 值类型的实例一般在线程栈上分配(虽然也可作为字段嵌入引用类型的对象中),代表值类型实例的对象中不包含指向实例的指针,其不受垃圾回收器的控制。因此,值类型的使用缓解了托管堆的压力,并减少了应用程序生存期内的垃圾回收次数。
可以这样说:对于值类型的表达式,它的值就是表达式的值,与此同时,对于引用类型的值,它的值是一个引用,这个引用指向它在堆中的位置。我们用下一段代码,通过对不同类型的变量内部进行值比较以及赋值等操作来理解前面提到的这些:
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
42
43
44
45
46
47
static void Main(string[] args)
{
//定义一个值类型变量,其值存放在线程栈上
int intVal = 0;
//定义一个引用类型变量,其实际数据位于堆中,值(其引用)存放在线程栈上
List<int> objListRef = new List<int>();
bool bolResult;
#region 变量值比较
//先定义两个新的变量,用来与前面的两个变量作比较
int intCompare = 0;
List<int> objListCompare = new List<int>();
bolResult = (intVal == intCompare);
//这里返回true,因为这两个值类型的值是相同的
Console.WriteLine($"{nameof(intVal)}和{nameof(intCompare)}{(bolResult == true ? "" : "不")}相等");
bolResult = (objListRef == objListCompare);
//这里返回false,因为这两个值(这两个变量的引用)位于堆中不同的位置
Console.WriteLine($"{nameof(objListRef)}和{nameof(objListCompare)}{(bolResult == true ? "" : "不")}相等");
#endregion
#region 变量赋值
intCompare = intVal;
intCompare++;
//这里会输出0,因为虽然intCompare的值加了1,但是其是值类型
//intCompare = intVal把intVal的值赋给了intCompare,相当于于复制了一个intVal,其值和intCompare相同
//所以对intCompare的更改不会作用到intVal
Console.WriteLine($"{nameof(intVal)}:{intVal}");
objListCompare = objListRef;
objListCompare.Add(0);
//这里会输出1,因为这里的变量是引用类型
//objListCompare = objListRef表示把objListRef的值(它的引用)赋给objListCompare
//因为它们指向堆中的同一个位置,所以从此时开始,对objListCompare做的任何修改操作都会作用到objListRef
Console.WriteLine($"{nameof(objListRef)}长度为:{objListRef.Count}");
objListCompare = new List<int>();
//这里仍然会输出1,因为当objListCompare重新初始化,它的值和objListRef的值已经不是指向堆中的同一个位置
//所以对它们的操作又重新变成对堆中不同数据的操作
Console.WriteLine($"{nameof(objListRef)}长度为:{objListRef.Count}");
#endregion
}
所以不难理解在日常使用方法的产生的一些疑惑:
Question1:为什么日常使用一些方法时,方法参数用默认的val传递,在方法内部对传递过来的参数进行修改,在方法执行完毕后,调用位置的那个变量的数据不会发生改变(当参数为值类型),数据发生改变(当参数为引用类型) 呢?
Answer:在进行参数值传递时,对于值类型来说,相当于传递了它的值的一个副本,对于引用类型来说,相当于传递了它的引用的一个副本。所以在方法内部对值的副本进行改变,外部值类型变量本身的数据不会发生变化,而引用类型变量变化时(前提是不改变其引用),因为它们都指向同一个地址,所以可以实现数据的改变;
Tips:值传递时,如果在方法内修改了参数的引用,则代表切断了与外部变量的联系,即在值传递时,是无法在方法内部修改调用处变量的引用的实际值的,只是把这个值赋值过来“用”。如果想改变调用处变量的引用,需要使用ref传递,如下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void Main(string[] args)
{
List<int> objList = null;
SomeByValMethod(objList);
//这里判断的结果是等于null
Console.WriteLine($"{nameof(objList)}{(objList == null ? "" : "不")}等于null");
SomeByRefMethod(ref objList);
//这里判断的结果是不等于null
Console.WriteLine($"{nameof(objList)}{(objList == null ? "" : "不")}等于null");
}
private static void SomeByValMethod(List<int> objList)
{
objList = new List<int>();
}
private static void SomeByRefMethod(ref List<int> objList)
{
objList = new List<int>();
}
Question2:为什么ref传递时,在传参时不能写形如ref null这样的形式,而是必须要传一个实际的参数,但当一个引用类型的变量等于null,却是可以这样写且不会报错的呢?
Answer:关于为何不能写成ref null的形式,因为不难看出这样做的不合理性,因为不可能“改变null在内存中指向的位置”;而当一个引用类型的变量等于null的时候,只是代表还没有为其在堆中重新分配内存,还未引用任何一个对象(内存中用全零来表示null),本质上它还是采用和其他引用一样的方式来存储的,所以在方法内部把这个变量的地址指向一个新的位置也就说得通了