谈谈C#中的值类型和引用类型
概述
在C#中,有且仅有两种数据类型:值类型和引用类型。换言之,一个变量要么是值类型,要么是引用类型。像我们常见的数据类型(int,float,double)、结构、枚举等等都属于值类型,而像类、接口、委托等都属于引用类型。所以,要想深入的了解.net framework的一些原理性的知识,值类型与引用类型是跨不过去的一道门槛。
正如Jeffrey Richter(CLR via C#作者)所说:
“不理解引用类型和值类型区别的程序员将会把代码引入诡异的陷进和诸多的新能问题”。
下面,我们将一步步摸透这俩者的真面目。
C#中变量的类型取决于什么?
在C#中,变量是值类型还是引用类型,取决于其基本数据类型。在C#中,其基本数据类型并没有内置于语言中,而是存在与.Net Framewok中。.Net使用CTS(通用语言系统)定义在IL(中间语言)中使用的预定义数据类型。C#中所有数据类型都是对象,它们有属性、方法等。
例如,在C#中声明一个int变量时,实际上是声明了CTS中System.Int32的一个实例。
谈谈System.Object和System.ValueType
.Net中,System.Object是一切的一切的"根源”。所以,值类型与引用类型也是继承自System.Object就不足为奇了。但是,它们俩之间还是有区别的。
引用类型是直接继承自System.Object的,而值类型是隐式直接派生于System.ValueType的,这个System.ValueType则是直接派生于System.Object的。其实,这也好理解,如果值类型和引用类型都是直接派生于System.Object,那该如何区别这两种数据类型呢?
那么为什么要有这个System.ValueType呢,亦或者说这个System.ValueType里面跟System.Object有什么不一样呢?
前面已经说过System.ValueType的父类就是System.Object,所以他们绝大部分都是相同的,而唯一不同的一块是System.ValueType类重写了Equals()方法,所以带来的后果就是必须得重写与Equals()方法相关联的几个方法,像FastEqualsCheck()、GetHashCode()等等。
下面来看看重写后的Equals()方法的庐山真面目:
1: public override bool Equals(object obj) 2: { 3: if (obj == null) 4: { 5: return false; 6: } 7: RuntimeType type = (RuntimeType) base.GetType(); 8: RuntimeType type2 = (RuntimeType) obj.GetType(); 9: if (type2 != type) 10: { 11: return false; 12: } 13: object a = this; 14: if (CanCompareBits(this)) 15: { 16: return FastEqualsCheck(a, obj); 17: } 18: FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); 19: for (int i = 0; i < fields.Length; i++) 20: { 21: object obj3 = ((RtFieldInfo) fields[i]).InternalGetValue(a, false); 22: object obj4 = ((RtFieldInfo) fields[i]).InternalGetValue(obj, false); 23: if (obj3 == null) 24: { 25: if (obj4 != null) 26: { 27: return false; 28: } 29: } 30: else if (!obj3.Equals(obj4)) 31: { 32: return false; 33: } 34: } 35: return true; 36: }对比System.Object中的Equals()版本:
1: public static bool Equals(object objA, object objB) 2: { 3: return ((objA == objB) || (((objA != null) && (objB != null)) && objA.Equals(objB))); 4: }可以看出,值类型中Equals()比较的不再是引用地址,而是比较的是实例的值,这便是值类型与引用类型在这个层面上的最大的不同了。
值类型
前面已经说过,值类型隐式派生于System.ValueType,那么在平时遇到的数据类型中,有哪些属于值类型呢?
整型。包括我们常见的short、int、long、byte、sbyte、bigint等这。
浮点型。包括float、double。
用于财务计算的高精度decimal类型。
结构体。struct,已经预定好的和用户自定义的。
枚举。
bool类型。
可空类型。
每种值类型均有一个隐式的默认构造函数来初始化该类型的默认值,这也是为什么像定义一个int型的变量时,有时候没有初始化,它也会有初始值为0的缘故。
还有一点需要注意的是,所有值类型都是seal(密封)类型,是不能再派生出新的数据类型的。
引用类型
在C#中,有以下这些引用类型:
数组。
类。
接口。
委托。
object。其实就是System.Object,老大哥。
字符串。string,System.String的别名,这也是一个极其重要的引用类型,后面会有专门一篇来描述字符串中的点点滴滴。
与值类型不同的是,引用类型可以派生出新的类型(不绝对话,比如说string就不可以)。需要注意的是,值类型中的结构体也是可以实现接口的。
内存分配
说起值类型和引用类型的不同,内存分配可谓是它们的本质区别了,这也可以让我们更加清晰的了解一些生活中遇到的问题。
值类型的实例一般都会存放在栈上,之所以说是一般,是因为它有时候也会去引用类型那里窜门,会有特殊情况,存放在堆上,接下来会讲。与之不同的是,引用类型的对象则总是存储在堆中,当然它的引用有时候也会存放在栈上,但是对象实例是一定存放在堆上的。
下面,考虑几个实际的情况:
1.如果某个类的实例中有个值类型的字段,那么这个值类型的字段是怎么个存储法呢?
答案是:这个值类型的字段会和类实例存放在同一个地方,即堆中。这便是上文的"不一般”的例子。
2.如果一个结构体的字段中有个引用类型,那么这个引用类型该怎么存储呢?
答案是:和结构体存放的位置一样,即栈上。好吧,我假设你是这个回答,那么恭喜你,答得不全对,也就是答错了。
其实,在栈上或堆上是不确定的,要根据具体情况而定。这里的引用类型在栈上存储了一个引用,而其实际存储的位置位于托管堆。
所以这里就可以看出值类型和引用类型存储方面的一个区别了:
值类型总是存储在它声明的地方,不在引用类型内部,那么就是存储在栈上,一旦在类等引用类型内部声明的时候,就存储在堆上了。这个可以总结为:当这个值类型,作为字段时,存储在托管堆上;作为局部变量时,存储在栈上。
引用类型,则不同,它无论在哪定义,它实际的值都是存放在堆上,而存放它的引用的位置则是根据他定义的位置确定。
总结
先看一个示例:
SomeType[] sample=new SomeType[100];上面的代码定义了一个位置类型的数组,SomeType可能是值类型(int等),也可能是引用类型(string等),那么它们定义的时候都发生了什么事情呢?
如果SomeType是值类型,则只需要一次内存分配,大小为这个值类型所需空间的100倍。
如果SomeType是引用类型,则需要分两步走,先是分配100次内存,分配后的值为null,然后再初始化100个元素,所以结果需要101次内存分配。
很显然,引用类型在执行时间上是不利于性能的,还会造成很多的内存碎片。所以如果类型的职责主要是存储数据,值类型比较适合。
一般来说,值类型(不支持多态)适合存储供C#应用程序操作的数据,而引用 类型(支持多态)应该用于定义应用程序的行为。当然,在现实中,我们用的引用类型是远远多于值类型。
还有一个问题,便是如何判断一个类型是值类型,还是引用类型?
其实,方法很简单,我们只需要判断这个类型是不是值类型就可以了,因为如果不是值类型那就必是引用类型。
可以用过Type.IsValueType这个属性来判断:
1: int value1 = 12; 2: string value2 = "Hello!"; 3: Console.WriteLine(value1.GetType().IsValueType);//output:True 4: Console.WriteLine(value2.GetType().IsValueType);//output:False