[C#学习笔记02]理解值和引用

值类型和类类型

所有基本数据类型(如int,float,double和char)统称为值类型。将一个变量声明为一个值类型时,编译器将生成代码来分配足以容纳这种值的一个内存块。如,声明一个int型的变量会导致编译器分配4字节的内存(32位)。

类类型则不同。声明一个Circle类变量时,编译器不会生成代码来分配足以容纳一个Circle的内存块。相反,它唯一做的事情就是分配一片内存,其中刚好可以容纳一个地址。以后,Circle实际占据的内存块的地址会填充到这里。这个地址也称为对内存块的一个引用。Circle对象实际占有的内存是在使用new关键字来创建对象时分配的。类是引用类型的一个例子。在引用类型中,容纳的只是对内存块的引用。

使用null

为了初始化一个引用变量(例如一个类),可以创建这个类的一个新的实例,并将引用变量赋给这个新对象,例如:

Circle c = new Circle(42);

但是假如你并不想真的创建一个新对象,只想用一个变量来存储对一个现有对象的引用。例如:

Circle c = new Circle(42);
Circle copy = new Circle(99);

Copy = c;

在例子中,一旦将c赋给copy,copy就会引用c所引用的实例,copy原来引用的实例就“落单”了,不存在对它的任何引用。在这种情况下,“运行时”通过执行一个称为垃圾回收的操作来回收内存。但是这个代价比较高。

这时很多人或许会产生这样一个疑问:既然一个变量反正都要在程序运行到某个地方的时候被赋值对另一个对象的引用,提前初始化它有什么意义呢?这时我们就能用到null值来初始化变量了。如下所示:

Circle c = new Circle(42);
Circle copy = null;

If (copy == null)
copy = c;

允许将null值赋给任意引用变量。值为null的变量表明该变量不引用内存中的任何对象。

使用可空类型

null值在初始化引用类型时非常有用,但null本身就是一个引用,不能将它赋给一个值类型,例如以下语句是非法的:

int i = null;

但是,利用C#定义的一个修饰符,可以将一个变量声明为一个可空的值类型。可空的值类型在行为上与普通的值类型相似,但你可以将一个null值赋给它。我们用一个问号来指明一个值类型是可空的,如下所示:

int? i = null;

为了确定一个可空的变量是否包含null,可以采取和引用类型一样的测试方法:

if (i == null)

可以把具有恰当值类型的一个表达式直接赋给一个可空变量。如下所示:

int?  I = null;
int j = 99;
i = 100;
i = j;

反之则不然,不能将一个可空的值赋给一个普通的值类型的参数。

这还意味着假如一个方法希望接收的是一个普通的值类型的参数,就不能将一个可空的变量作为参数传给它。

使用ref和out参数

通常,向方法传递一个实参时,对应的参数(形参)会用实参的一个副本来初始化。不管参数是值类型(例如int),可空类型(例如int?),还是引用类型,这一点都是成立的。换言之,随便在方法内部进行什么修改,都不会影响作为参数来传递的一个变量的原始值。例如下面代码中,向控制台输出的值是42,而不是43。DoWork方法递增的只是实参(arg)的一个副本,而不是对原始实参进行递增。

static void DoWork(int param)
{
param++;
}
static void Main()
{
int arg = 42;
DoWork(arg);
Console.WriteLine(arg);
}

为此,C#语言专门提供了ref和out关键字。

如果为一个参数(形参)附加ref作为前缀,该参数就会成为实参的一个引用,而不再是实参的副本。如下所示:

static void DoWork(ref int param)
{
param++;
}
static void Main()
{
int arg = 42;
DoWork(ref arg);
Console.WriteLine(arg);
}

由于向DoWork方法传递的是对实参的一个引用,而非副本,所以方法对这个引用进行的任何修改都会反映到原始实参中去。因此,控制台输出的是43。

有时候,我们可能希望由方法本身来初始化参数,所以希望向其传递一个未初始化的实参。out关键字正好解决了我们的这一考虑。

在向方法传递一个out参数之后,必须在方法内部对其进行赋值。使用时如下所示:

static void DoWork(out int param)
{
param = 42;
}
static void Main()
{
int arg;
DoWork(out arg);
Console.WriteLine(arg);
}