原文地址:http://www.cnblogs.com/zhangkai2237/archive/2013/03/17/2964528.html
类型一直是C#中最基本的问题,关于值类型和引用类型,我想每个C#程序员都知道“值类型保存在栈上,引用类型保存在堆上”。但是仅仅知道到这里是完全不够的,我们需要理解C#中的类型,了解为什么要有值类型和引用类型以及他们的特征。
一、值类型和引用类型的概念
值类型的实例是在线程栈上分配的(不能免俗的提起这句话),值类型的变量并没有一个指向实例的指针,而是变量中已经包含了实例本身的字段。
相应的引用类型的实例时在托管堆中分配的,返回的是一个指向实例对象的内存地址。
比如我们一个值类型的变量 valType, 他包含一个int的字段a,其值为5,他在栈和堆中的示意图为:
现有一个引用类型的变量refType,他指向RefType类的一个实例,下图为示意图:
另外我们都知道基元类型中除了string类型,其他的都是值类型,但是我们大部分人都没有发现他们之间的区别。只要我们进入各种基元类型的定义中就可以发现:string类型是一个class,而其他的值类型都是struct。翻阅资料发现了微软在定义值类型和引用类型的区别:
引用类型包括类和接口,所有的以class和interface修饰的类型都是引用类型;而值类型包括结构和枚举,所有的结构和枚举都是值类型。继续查找资料发现所有的结构都是抽象类型System.ValueType,所有的枚举都是派生自System.Enum类型的,而System.Enum类型也继承自System.ValueType类。所以我们可以得出
值类型都是继承自System.ValueType的结论。
值类型还有一个重要的特征是因为结构是隐式密封的,所以我们没办法由自值类型来派生一个我们想要的类型来。例如我们无法从System.Int32(int)类派生出另外一个类型来。
二、为什么要有值类型
FCL中的绝大多数类型都是引用类型,那么为什么要有值类型呢?首先我们回顾下实例化一个引用类型的步骤:
1. 计算好在托管堆上分配的内存大小,包括该类型实例的所有字段的大小,以及在托管堆中的两个“额外成员”(《CLR via C#》中的翻译):类型对象指针和同步块索引;
2. 在托管堆中分配指定大小的内存块;
3. 初始化对象的“类型对象指针”和“同步块索引”;
4、调用类型的构造函数,如果调用的是有参构造函数,则还要向其传入参数。注意,如果要实例化子类对象,则必须先调用父类的构造函数,一直会追溯到System.Object。这部分具体的可以参看《C#入门经典》相关章节。
如果所有的类型都是引用类型,那么我们程序的性能将显著下降。
引用类型在性能方面还有一个重大的问题是垃圾回收。当没有变量指向某个对象时,那么该对象就会成为垃圾,就会成为GC回收的对象,而GC是相当耗费资源的。而使用
值类型是不需要垃圾回收的,对象超过了其作用域就会
自动销毁。
上面这段解释了为什么我们需要值类型,讲解了值类型在性能上的又是,那么我们为何还需要引用类型呢?
我们都知道值类型的传值是要复制整个对象的,而引用类型仅仅是复制指向实例对象的指针。而且值类型不能被继承,所以
值类型适合一些比较轻量和简单的类型,否则同样会出现性能问题。
那么我们应该什么时候使用值类型呢:
必须满足以下所有条件,否则不要定义成值类型第一,类型具有基元类型的行为。类型简单,其中没有成员会修改类型的任何实例字段。第二,类型不需要从其他任何类型继承。第三,类型不会派生出其他任何类型。除了满足以上全部条件,还必须满足以下条件中的一个。第一,类型的实例较小(约是16字节或者更小)。第二,类型实例较大,但不作为方法的实参传递,也不通过方法返回。(这样即使很大但是不需实参传递,不会进行复制的操作)
到这里我们基本上把值类型和引用类型的一些基本知识了解完了,那么这部分面试官喜欢问什么问题呢?
值类型和引用类型的区别
1.
值类型分配在内存栈上,引用类型分配在托管堆上。当一个值类型的变量赋给另一个值类型的变量时,会执行一次
逐字段的复制,而一个引用类型的变量赋给另一个引用类型的变量时,仅仅会
复制对象的内存地址。
2. 基于上一条,
多个引用类型的变量可以同时指向同一个对象,对其中的任何一个变量执行操作都
会影响到另一个变量引用的对象。而每个值类型的变量都已经包含了自己的对象,所以对
值类型对象的操作不会影响到另一个值类型变量。
3. 值类型包括
结构和
枚举,他们均间接或直接派生自System.ValueType类;引用类型包括类和接口,他们都派生自System.Object类(这一句是废话,所有的类型都派生自System.Object类,可说可不说)。
4. 值类型都是
隐式密封的,不能将一个值类型作为基类来定义一个新的值类型或者引用类型,也因此值类型中不能包括虚方法(不能被继承,虚方法给谁重写呢)。
5、默认情况下,创建一个引用类型的变量时,他会被
初始化为null;而创建一个值类型时,他的所有成员都会被
初始化为0.
6. 值类型的变量一旦超过了其作用域,为他分配的内存就会被
立即释放;而引用类型则会在托管堆里
待一段时间,直到垃圾回收器将其回收。
7. 由于System.ValueType类重写了Equals方法,所以两个值类型的Equals方法会在
两个对象的字段完全匹配的情况下返回true;而引用类型的
Equals则会在两个变量引用同一个对象的情况下才返回true。(这一条不重要,不说也无所谓,但是如果被问到自己要有所了解)。
数组是值类型还是引用类型?
我第一次遇到的这个问题的时候并没有特别注意,但是心想他既然问这样的问题,应该是引用类型,所以坚定的回答”引用类型“,让面试官看不出来我是猜的。所以童鞋们以后如果遇到类似的问题,即使是猜的也要理直气壮,否则即使你答对了,面试官也知道你是猜的,在这个问题上还是会被扣分的。
数组类型的确是引用类型,可能有部分童鞋不承认,那我们简单的写一个验证方法:
#region 数组的类型 int[] intArray = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int[] copyArray = intArray; intArray[0] = 9; Console.WriteLine(copyArray[0]); #endregion
输出为:9.
结构和类的区别
实际上就是值类型和引用类型的区别,对照着第一题回答就行了。
到这里,值类型和引用类型还有一个特别重要的点没有提到,那就是装箱和拆箱。实际上是一个很简答的问题,但是如果你不了解,在面试时会很难回答。并且在日常的工作中很有可能经常掉入装箱的陷阱,对程序的性能有较大的影响。