引用类型:
以下是引用类型,继承自System.Object:
1.通用类型系统
- 确保IL上的强制类型安全;
- 实现了不同.NET语言的互操作性;
- 所有的数据类型都是对象。它们可以有方法,属性,等。例如:
int i;
i = 1;
string s;
s = i.ToString();
2.值类型
int i = newint();
等价于:
Int32 i = newInt32();
等价于:
int i = 0;
等价于:
Int32 i =0;
值得注意的是,System.ValueType直接派生于System.Object。即System.ValueType本身是一个类类型,而不是值类型。其关键在于ValueType重写了Equals()方法,从而对值类型按照实例的值来比较,而不是引用地址来比较。
TestType testType =new TestType ();
if (testTypetype.GetType().IsValueType)
{
}
3.引用类型
C#有以下一些引用类型:
- 数组(派生于System.Array)
- 用户用定义的以下类型:
-
- 类:class(派生于System.Object);
- 接口:interface(接口不是一个“东西”,所以不存在派生于何处的问题。Anders在《C#Programming Language》中说,接口只是表示一种约定[contract]);
- 委托:delegate(派生于System.Delegate)。
- object(System.Object的别名);
- 字符串:string(System.String的别名)。
可以看出:
- 引用类型与值类型相同的是,结构体也可以实现接口;
- 引用类型可以派生出新的类型,而值类型不能;
- 引用类型可以包含null值,值类型不能(可空类型功能允许将null 赋给值类型);
- 引用类型变量的赋值只复制对对象的引用,而不复制对象本身。而将一个值类型变量赋给另一个值类型变量时,将复制包含的值。
对于最后一条,经常混淆的是string。我曾经在一本书的一个早期版本上看到String变量比string变量效率高;我还经常听说String是引用类型,string是值类型,等等。例如:
string s1 = "Hello,";
string s2 = "world!";
string s3 = s1 + s2;//s3 is "Hello, world!"
这确实看起来像一个值类型的赋值。再如:
string s1 = "a";
string s2 = s1;
s1 = "b";//s2 is still "a"
改变s1的值对s2没有影响。这更使string看起来像值类型。实际上,这是运算符重载的结果,当s1被改变时,.NET在托管堆上为s1重新分配了内存。这样的目的,是为了将做为引用类型的string实现为通常语义下的字符串。
4.值类型和引用类型在内存中的部署
经常听说,并且经常在书上看到:值类型部署在栈上,引用类型部署在托管堆上。实际上并没有这么简单。
MSDN上说:托管堆上部署了所有引用类型。这很容易理解。当创建一个应用类型变量时:
object reference = newobject();
关键字new将在托管堆上分配内存空间,并返回一个该内存空间的地址。左边的reference位于栈上,是一个引用,存储着一个内存地址;而这个地址指向的内存(位于托管堆)里存储着其内容(一个System.Object的实例)。下面为了方便,简称引用类型部署在托管推上。
再来看值类型。《C#语言规范》上的措辞是“结构体不要求在堆上分配内存(However,unlike classes, structs are value types and do not require heapallocation)”而不是“结构体在栈上分配内存”。这不免容易让人感到困惑:值类型究竟部署在什么地方?
4.1数组
考虑数组:
int[] reference = newint[100];
根据定义,数组都是引用类型,所以int数组当然是引用类型(即reference.GetType().IsValueType为false)。
而int数组的元素都是int,根据定义,int是值类型(即reference[i].GetType().IsValueType为true)。那么引用类型数组中的值类型元素究竟位于栈还是堆?
如果用WinDbg去看reference[i]在内存中的具体位置,就会发现它们并不在栈上,而是在托管堆上。
实际上,对于数组:
TestType[] testTypes =new TestType[100];
如果TestType是引用类型,则会先在托管堆为testTypes分配一次空间,并且这时不会自动初始化任何元素(即testTypes[i]均为null)。等到以后有代码初始化某个元素的时候,这个引用类型元素的存储空间才会被分配在托管堆上。
4.2类型嵌套
规律是:
- 引用类型部署在托管堆上;
- 值类型总是分配在它声明的地方:作为字段时,跟随其所属的变量(实例)存储;作为局部变量时,存储在栈上。
我们来分析一下上面的代码。对于引用类型实例,即referenceTypeClassInstan
- 从上下文看,referenceTypeClassInstan
ce是一个局部变量,所以部署在托管堆上,并被栈上的一个引用所持有; - 值类型字段_valueTypeField属于引用类型实例referenceTypeClassInstan
ce的一部分,所以跟随引用类型实例referenceTypeClassInstan ce部署在托管堆上(有点类似于数组的情形); - valueTypeLocalVariable是值类型局部变量,所以部署在栈上。
而对于值类型实例,即valueTypeStruct:
- 根据上下文,值类型实例valueTypeStructInstance本身是一个局部变量而不是字段,所以位于栈上;
- 其引用类型字段_referenceTypeField不存在跟随的问题,必然部署在托管堆上,并被一个引用所持有(该引用是valueTypeStruct的一部分,位于栈);
- 其引用类型局部变量referenceTypeLocalVariab
le显然部署在托管堆上,并被一个位于栈的引用所持有。
所以,简单地说“值类型存储在栈上,引用类型存储在托管堆上”是不对的。必须具体情况具体分析。
5.正确使用值类型和引用类型
这一部分主要参考《EffectiveC#》,并非本人原创,希望能让你加深对值类型和引用类型的理解。
5.1辨明值类型和引用类型的使用场合
C#中,我们用struct/class来声明一个类型为值类型/引用类型。
考虑下面的例子:
TestType[] testTypes =new TestType[100];
通常我们创建的引用类型总是多于值类型。如果以下问题的回答都为yes,那么我们就应该创建为值类型:
- 该类型的主要职责是否用于数据存储?
- 该类型的共有借口是否完全由一些数据成员存取属性定义?
- 是否确信该类型永远不可能有子类?
- 是否确信该类型永远不可能具有多态行为?