3.1.3 值类型和引用类型的区别 37
· 值类型和引用类型的基本概念
· 值类型和引用类型的内存分配
· 值类型的基类
分析问题
所有.NET的类型都可以分为两类:值类型和引用类型。最简单也最明确的一个区分标准是:所有的值类型都继承自 System.ValueType(System.ValueType继承自System.Object),也就是说,所有继承自System.ValueType的类型都是值类型,而其他类型都是引用类型。常用的值类型包括结构、枚举、整数型、浮点型、布尔型等,而在C#中所有以class关键字定义的类型都是引用类型。
严格来说,System.Object作为所有内建类型的基类,本身并没有值类型和引用类型之分。但是System.Object的对象,具有引用类型的特点。这也是值类型在有些场合需要装箱拆箱操作的原因。
1.赋值时的区别
引用类型和值类型最显著的一个区别在于变量的赋值问题。值类型的变量将直接获得一个真实的数据副本,而对引用类型的赋值仅仅是把对象的引用赋给变量,这样就可能导致多个变量引用到一个实际对象实例上。
来看一段简单的示例:代码3-2。首先为了测试建立一个简单的引用类型和一个简单的值类型。
代码3-2 值类型引用类型赋值:ValueRef.cs
/// <summary>
/// 一个简单的引用类型
/// </summary>
public class Ref
{
private int _int;
public Ref(int i)
{
_int = i;
}
public int I
{
get
{
return _int;
}
set
{
_int = value;
}
}
public override string ToString()
{
return "I 的值为:"+ _int.ToString();
}
}
/// <summary>
/// 一个简单的值类型
/// </summary>
public struct Val
{
private int _int;
public Val(int i)
{
_int = i;
}
public int I
{
get
{
return _int;
}
set
{
_int = value;
}
}
public override string ToString()
{
return "I 的值为:"+ _int.ToString();
}
}
随后编写main方法,测试对值类型和引用类型对象进行赋值的不同结果,如代码3-3所示。
代码3-3 值类型引用类型赋值:ValueRef.cs
public class ValueRef
{
static void Main(string[] args)
{
//测试引用类型的赋值
Ref ref1 = new Ref(1);
Ref ref2 = ref1;
ref2.I = 2;
//测死值类型的赋值
Val val1 = new Val(1);
Val val2 = val1;
val2.I = (2);
//输出
Console.WriteLine("ref1 " + ref1);
Console.WriteLine("ref2 " + ref2);
Console.WriteLine("val1 " + val1);
Console.WriteLine("val2 " + val2);
Console.Read();
}
}
简单分析一下代码3-2和代码3-3,程序定义了一个引用类型Ref和一个值类型Val,两者的内容几乎完全相同。在main方法中,分别测试了引用类型和值类型的赋值。当代码把一个引用类型变量赋值给另一个引用变量:Ref ref2=ref1时,实际上把ref1的对象引用赋给了ref2,这样,两个引用变量实际指向了同一个对象。而值类型的赋值则不同,val1和val2 都保留了属于自己的数据副本,所以当val2改变时,val1不受到影响。以下是代码3-2和代码3-3的输出结果:
ref1 I 的值为:2
ref2 I 的值为:2
val1 I 的值为:1
val2 I 的值为:2
2.内存分配的区别
除了赋值的区别,引用类型和值类型在内存的分配位置上也有区别。引用类型的对象将会在堆上分配内存,而值类型的对象则会在堆栈上分配内存。堆栈的空间相对有限,但运行效率却比堆高得多,关于堆和堆栈的特点,将在本章的后续章节中有详细的说明。
3.来自继承结构的区别
最后,由于所有的值类型都有一个共同的基类:System.ValueType,所以值类型拥有一些引用类型不具有的共同性质,较重要的一点是值类型的比较方法:Equals方法的实现有了改变。在前文中笔者已经谈到,所有的值类型已经实现了内容的比较,而引用类型在没有重写Equals方法的情况下,仍然采用引用比较。代码3-4展示了这个特性,代码3-4仍然使用了代码3-2中建立的值类型和引用类型,这里不再重复列出这两个类型的定义。
代码3-4 值类型引用类型比较:ValueRefEquals.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace NET.MST.Third.ValueRefEquals
{
public class ValueRefEquals
{
static void Main(string[] args)
{
//引用类型
Ref ref1 = new Ref(1);
Ref ref2 = new Ref(1);
//值类型 struct类型
Val val1 = new Val(1);
Val val2 = new Val(1);
//输出
Console.WriteLine(ref1.Equals(ref2));
Console.WriteLine(val1.Equals(val2));
Console.Read();
}
}
}
在main方法中,分别申明了一对内容完全相同的值类型对象和引用类型对象,调用Equals方法来比较,发现值类型对象比较返回true,而引用类型对象比较返回false。以下是代码3-4的执行结果:
False
True
答案
所有继承自System.ValueType的类型都是值类型,而其他类型都是引用类型。值类型的赋值会产生一个新的数据副本,所以每个值类型都拥有一个数据副本,而引用类型的赋值则是赋值引用。值类型的对象分配在堆栈上,而引用类型的对象分配在堆上。当比较两个值类型时,进行的是内容比较,而比较两个引用类型时,进行的是引用比较。
笔者这里列举的,仅仅是值类型和引用类型的一些主要区别,通过这些本质区别,可以产生更多的细节区别,有兴趣的读者可以自己总结整理。