复制值类型的变量和类
C#大多数基元类型都是值类型.比如声明int类下的变量会导致编译器分配4字节内存卡。向int变量赋值(比如10),将导致值被复制到内存块中.
类类型则以不同方式处理。声明Circle变量时,编译器不生成代码来分配足以容纳一个Circle的内存块。相反,它唯一做的事情就是分配一小块内存,其中刚好可以容纳一个地址.以后,Circle实际占用内存块的地址会填充到这里.该地址称为对内存块的引用.
a++后的值不影响b,因为它们各自有一个内存块.
而引用类型就不一样了,引用类型容纳对内存块的引用
a和b将容纳同一个地址,所以修改了b a也随之改变了.
只复制引用称为“浅拷贝”,能够复制引用的对象,称为“深拷贝”.
null值和可空类型
将a赋给b后,b原来引用的Circle实例会发生什么事情?
一旦将a赋给b,b就会引用a所引用的实例,b原来引用的实例就“落单了”,现在不存在对它的任何引用。
这种情况下,"运行时"通过垃圾回收机制来回收内存.垃圾回收是一个可能比较耗时的操作;不要创建从来不用的对象,否则只会浪费时间和资源。
既然变量在程序运行到某个地方时都会被赋值为另一个对象的引用,提前初始化有什么意义?
问题在于不在声明时初始化,很有可能会造成代码出问题.
C#允许将null值赋给任意引用变量,null表明该变量不引用内存中的任何对象.
空条件操作符
可以使用空条件操作符更简洁地测试空值,使用它需要为变量名附加问号?前缀。
使用?操作符就更方便了
…
…
可空类型
null值在初始化引用类型时非常有用,但null本身就是引用,不能把它赋给值类型
比如
int i = null;//非法
但是我们可以将变量声明为可空值类型。
int? i = null;
同时也可以判断i是否包含null
if(i == null)
...
可将恰当值类型的表达式直接赋给可空变量.
int? i = null;
int j = 1;
i = 2;
i = j;
但是
j = i;//非法
因为i可能包含null,而j不是可空类型。
不要混淆可空类型和空条件操作符。前者的问号加在类型名称后,后者加在变量名称后.
可空类型的属性
可空类型公开了两个属性,用于判断类型是否实际包含非空的值,以及该值是什么。
实例 如下:
int? i = null;
if(!i.HasValue)
{
i = 10;
}
else
{
Console.WriteLine(i.Value);
}
可空类型的Value属性是只读的。不能修改.修改要用普通的赋值语句.
…
…
…
ref和out
向方法传递实参时,对应的参数(形参)通常会用实参的拷贝来初始化,不管参数是值类型,可空类型,还是引用类型.
下面是个例子:
可见Increment方法递增的只是实参的拷贝,原始实参并没有发生改变.
如果一个方法的参数是引用类型,那么使用那个参数来进行的任何修改都会改变传入的实参所引用的数据。
这里的关键在于,虽然引用的数据发生了改变,但传入的实参没有变。虽然可以通过参数来修改实参引用的对象,但是无法修改实参它自己本身。有时候,我们希望方法能实际地修改一个实参,为此C#专门提供了ref和out关键字
创建ref参数
ref使用方法如下:
这一次,由于向Increment方法传递的是对原始实参的引用而非拷贝,所以用这个引用进行的任何修改都会反映到原始实参中,因此输出为2.
注意上例中的a必须先初始化。
创建out参数
有时候我们希望调用方法来初始化参数,所以希望向其传递没有初始化的实参,这时就要用到out关键字.
out关键字用法如下:
out参数必须在方法中赋值,所以可以调用方法的时候不需要进行初始化。
内存的组织方式
通常内存划分为两个独立区域,称为栈和堆.
调用方法时,它的参数和局部变量所需的内存总是从栈中获取。方法结束后,分配的内存都自动归还给栈,并可在另一个方法调用时重新使用.
使用new关键字创建对象时,构造对象所需的内存总是从堆中获取.对象最后一个引用消失之后,对象占用的内存就可供宠用。
使用栈和堆
void Method(int param)
{
Circle c;
c = new Circle(param);
...
...
}
假设传给param的值为1.调用方法时,栈中将分配一小块内存,并用1初始化。在方法内部,还要从栈中分配出另一小块内存,它刚够存储一个引用,只是暂不初始化。这是为C准备的。接着分配一个足够大的内存区域来容纳一个Circle对象。这是new关键字所执行的操作,将原始堆内存转换成Circle对象,对该Circle对象的引用将存到变量C中
注意:
虽然对象本身存在堆中,但是对象的引用存储在栈中。
堆内存是有限的资源。堆内存耗尽了,new会抛出OutofMemoryException,对象创建失败.
方法结束时,为C和param分配的内存被自动回收到栈。
"运行时"法线不存在对Circle对象的引用,在某个时刻,安排垃圾回收器回收内存.
…
…
…
System.Object类
所有类都是System.Object的派生类,而且System.Object类型的变量能引用任何对象。
也就是说
class Employee
{
...
}
和
class Employee : System.Object
{
...
}
是完全一样的
由于所有类型都从System.Object派生,每个类型的每个对象都保证了一组最基本的方法
装箱
object类型的变量能够引用任何引用类型的任何对象。
object类型的变量也能引用值类型的实例。
int i = 1;
object o = i;
i是值类型,在栈中。如果o直接引用i,引用的是将是栈。然而,所有引用都必须引用堆上的对象;引用栈上的数据项,会严重损害"运行时"的健壮性,并造成潜在的安全漏洞。所以实际上,"运行时"在堆中分配一小块内存,然后将i的值复制到这块内存中,最后让o引用该拷贝。这种将数据从栈自动复制到堆的行为叫装箱
…
…
…
拆箱
int i = 1;
object o = i;//装箱
i = (int)o;
上面的过程叫做拆箱,编译器发现指定了类型int,会在运行时生成代码检查o实际引用什么。如果o真的引用一个已装箱的int,转型成功执行,编译器生成的代码会从装箱的int中提取出值.
然而,如果o引用的不是已装箱的int,就会出现类型不匹配的情况,造成转型失败。
比如:
注意装箱和拆箱都会产生较大的开销,因为涉及到不少的检查工作,而且需要分配额外的堆内存。
…
…
…
数据的安全转型
is操作符
可以用is操作符验证对象的类型是不是自己希望的
is操作符取两个操作数:左边是引用,右边是类型名称。如果左边对象is右边的类型,则is表达式的求值结果为true,反之为false.
…
as操作符
和is操作符一样,as操作符取对象和类型作为左右操作数。"运行时"尝试将对象转换成指定类型,如果转换成功,返回转换成功的结果,否则返回null.