CLR支持两种类型:引用类型与值类型。引用类型(reference type)总是从托管堆上分配,C#的new操作符返回的就是对象位于托管堆中的内容地址--该内存地址指向对象占用的数据位。在使用引用类型时,我们需要有一些性能考虑,因为内存必须从托管堆中分配;每个托管堆中分配的对象都有一些与之相关联的额外附加成员必须被初始化;从托管堆中分配对象可能会导致执行垃圾收集。这样,CLR提供了值类型。它分配在线程的堆栈上,没有指向实例的指针。.NET框架参考文档明确指出被称为“类”的类型都是引用类型,比如System.Object类,System.Exception类,System.IO.FileStream类,以及System.Random等;而结构或枚举称作为值类型,比如System.Boolean结构,System.Decimal结构,System.TimeSpan结构,System.DayOfWeek枚举等。所有的枚举类型都继承自System.Enum,而System.Enum和所有的结构类型又都继承自System.ValueType类型。举个例子如下:
using System;
using System.Collections.Generic;
using System.Text;
namespace TestValueType
{
class Program
{
static void Main(string[] args)
{
TypeRef r1 = new TypeRef(); //分配在托管堆上
TypeVal v1 = new TypeVal(); //分配在堆栈上
r1.x = 5; //解析指针
v1.x = 5; //在堆栈上修改
Console.WriteLine(r1.x); //显示为5
Console.WriteLine(v1.x); //也显示为5
TypeRef r2 = r1; //仅拷贝指针
TypeVal v2 = v1; //先在堆栈上分配,然后拷贝成员
r1.x = 8; //改变了r1.x和r2.x
v1.x = 9; //改变了v1.x,没有改变v2.x
Console.WriteLine();
Console.WriteLine(r1.x); //8
Console.WriteLine(r2.x); //8
Console.WriteLine(v1.x); //9
Console.WriteLine(v2.x); //5
}
}
class TypeRef
{
public Int32 x;
}
struct TypeVal
{
public Int32 x;
}
}
运行结果为:55 8895。
所以,在我们设计自己的类型时,需要仔细考虑是将它们定义为值类型,还是引用类型。当满足以下表述时,我们就应该考虑将类型声明为值类型:1.该类型的行为类似于基元类型;2.该类型不需要继承自任何其他类型;3.该类型不会被任何其他类型继承;4.该类型的实例不会频繁地用于方法的参数传递;5.该类型的实例不会作为方法的结果频繁地返回;6.该类型的实例不会被频繁地用于诸如ArrayList、Hashtable之类的集合,这里主要是考虑装箱拆箱导致的程序性能损伤。
另外,许多编译器(包括C#和Visual Base)都不允许在值类型中定义Finalize方法。虽然CLR允许一个值类型定义Finalize方法,但是在值类型的装箱实例被执行垃圾收集时,CLR并不会调用该方法。
这里,我们再来看一下java中是如何处理值类型与引用类型的。绝大多数情况下,Java程序的多个部分(方法、变量和对象)驻留在内存中以下两个位置之一:即栈和堆。它的实例变量和对象驻留在堆上,局部变量驻留在栈上。当我们把对象引用和基本值传入方法中时,方法能够声明为接受基本值和/或对象引用。只不过,传入方法中时,对象引用和基本变量之间的差别很大,并且非常重要。
把对象变量传递到方法时,必须记住是在传递对象引用,而不是实际的对象自身。引用变量保存的位(向底层VM)表示在内存中(堆上)获得具体对象的一种方法,它根本没有传递实际引用变量,而是该引用变量的一份副本。变量副本指获得该变量的内的位副本,因此,当传递引用变量时,是在传递表示怎样获得具体对象的位副本。换句话说,调用者和被调用者方法现在都具有该引用完全相同的副本,因此二者都将引用堆上完全相同的(不是副本)对象。但是对于运行在单个vm内的所有变量,Java实际上是传值。传值是指按变量的值传递。也就是传递该变量的一份副本。如果传递基本变量或引用变量则没有任何区别,它们总是在传递该变量内的位副本。因此,对于基本变量,是在传递表示该值的位副本。例如,如果传递一个值为3的int变量,则是在传递一个表示3的位副本。被调用方法之后得到其自己的值副本,并随意处理它。如果传递对象引用变量,则是在传递一份表示对一个对象引用的位副本。被调用方法之后得到其自己的引用变量副本,并随意处理它,但是,因为相同的引用变量引用完全相同的对象,所以,如果被调用方法修改该对象,则调用者将看到调用者原来变量引用的对象也会被修改。传值的要点是:被调用方法不能修改调用者的变量,尽管对于对象引用变量,被调用方法能够修改该变量引用的对象。
变量的影子世界---隐藏。直接声明一个相同名称的局部变量,或者像下面这样作为参数的一部分声明一个相同名称的局部变量,这两种方法都能隐藏一个实例变量:
class Foo{
static int size = 7;
static void changeIt(int size){
size = size + 200;
System.out.println("size in changeIt is " + size);
}
public static void main(String[] args){
Foo f = new Foo();
System.out.println("size = " + size);
changeIt(size);
System.out.println("size after changeIt is " + size);
}
}
上面程序局部size变量被修改,而实例变量size则不受影响。运行结果如下:size = 7 size in changeIt is 207 size after changeIt is 7。
当被隐藏的变量是一个对象引用而不是基本变量时:
class Bar{
int barNum = 28;
}
class Foo{
Bar myBar = new Bar();
void changeIt(Bar myBar){
myBar.barNum = 99;
System.out.println("myBar.barNum in changeIt is " + myBar.barNum);
myBar = new Bar();
myBar.barNum = 420;
System.out.println("myBar.barNum in changeIt is now " + myBar.barNum);
}
public static void main(String[] args){
Foo f = new Foo();
System.out.println("f.myBar.barNum is " + f.myBar.barNum);
f.changeIt(f.myBar);
System.out.println("f.myBar.barNum after changeIt is " + f.myBar.barNum);
}
}
此时的运行结果为f.myBar.barNum is 28;myBar.barNum in changeIt is 99;myBar.barNum in changeIt is now 420;f.myBar.barNum after changeIt is 99。
可以看到隐藏变量能够影响myBar实例变量,因为myBar参数接收对同一个Bar对象的引用。但是,当把局部myBar重新赋予新的Bar对象时,Foo原来的myBar实例变量不受影响。
参考:《.NET框架程序设计》