1.对象的生存期
首先我们来看一下创建对象的过程。对象用new操作符创建。下例创建Square(正方形)类的新实例
class Square
{
…
void Draw()
{
…
}
}
Square mySquare = new Square();//Square是引用类型
new 表面上是单步操作,但实际分两步走:
1.new操作符从堆中分配原始内存。这个阶段无法进行任何干预
2.new操作符将原始内存转换成对象;它必须初始化对象。可用构造器控制这一阶段
创建好对象后,可用点操作符(.)访问其成员。例如,Square 类提供了Draw方法:
mySquare.Draw();
mySquare变量离开作用域时,它引用的Square对象就没人引用了,所以对象可被销毁,占用的内存可被回收(稍后会讲到,这并不是马上就发生的)。和对象的创建相似,对象的销毁也分两步走,过程刚好与创建相反。
1. CLR执行清理工作,可以写一个析构器来加以控制。
2. CLR将对象占用的内存归还给堆,解除对象内存的分配。对这个阶段你没有控制权。销毁对象并将内存归还给堆的过程称为垃圾回收。
1.1编写析构器
使用析构器,可以在对象被垃圾回收时执行必要的清理。CLR能自动清理对象使用的任何托管资源,所以许多时候都不需要自己写析构器。但如果托管资源很大(比如一个多维数组),就可考虑将对该资源的所有引用都设为null, 使资源能被立即清理。另外,如果对象引用了非托管资源(无论直接还是间接),析构器就更有用了。
注意:间接的非托管资源其实很常见。文件流、网络连接、数据库连接和Windows操作系统管理的其他资源都是例子。所以,如果方法要打开一个文件,就应考虑添加析构器在对象被销毁时关闭文件。但取决于类中的代码的结构,或许有更好、更及时的办法关闭文件,详情参见稍后对using语句的讨论。
和构造器相似,析构器也是一一个特殊方法,只是CLR会在对象的所有引用都消失之后调用它。析构器的语法是先写一个~符号,再添加类名。例如,下面的类在构造器中打开文件进行读取,在析构器中关闭文件(注意这只是例子,不建议总是像这样打开和关闭文件):
class FileProcessor
{
Filestream file = null;
public FlleProcessor(string fileName)
{
this.file = File.OpenRead(1leName); //打开文件来读取
}
~FileProcessor()
{
this.file.Close(); //关闭文件
}
}
析构器存在以下重要限制
(1)析构器只适合引用类型。值类型(例如struct)不能声明析构器。
(2)不能为析构器指定访问修饰符(例如public).这是由于永远不在自己的代码中调用析构器一总 是由垃圾回收器(CLR的一部分)帮你调用。
(3)析构器不能获取任何参数。这同样是由于永远不由你自己调用析构器。
编译器内部自动将析构器转换成对object.Finalize方法的一个重写版本的调用。例如,编译器将以下析构器:
class FileProcessor
{
~FileProcessor() { //你的代码放到这里}
}
转换成以下形式:
class FileProcessor
{
protected override void Finalize()
{
try { //你的代码放在这里}
finally { base.Finalize(); }
}
}
注意:只有编译器才能进行这个转换。你不能自己重写Finalize,也不能自己调用Finalize.
1.2为什么要使用垃圾回收器
在C#中,你永远不能亲自销毁对象。没有任何语法支持该操作。相反,CLR在它认为合适的时间帮你做这件事情。注意,可能存在对一一个对象的多个引用。在下例中,变量myFp
和referenceToMyFp引用同一个FileProcessor对象。
FlleProcessor myFp = new FileProcessor();
FileProcessor referenceToMyFp = myFp;
能创建对一个对象的多少个引用?答案是没有限制。这对对象的生存期产生了影响。CLR必须跟踪所有引用。如果变量myFp 不存在了(离开作用域),其他变量(比如referenceToMyFp)可能仍然存在,FileProcessor对象使用的资源还不能被回收(文件还不能被关闭)。因此,对象的生存期不能和特定的引用变量绑定。只有在对一个对象的所有引用都消失之后,才可以销毁该对象,回收其内存以进行重用。
可以看出,对象生存期管理是相当复杂的-件事情, 这正是C#的设计者决定禁止由你销毁对象的原因。如果由程序员负责销毁对象,迟早会遇到以下情况之一。
(1)忘记销毁对象。这意味着对象的析构器(如果有的话)不会运行,清理工作不会进行,内存不会回收到堆。最终的结果是,内存很快被消耗完。
(2)试图销毁活动对象,造成一个或多个变量容纳对已销毁的对象的引用,即所谓的虚悬引用。虚悬引用要么引用未使用的内存,要么引用同一个内存位置的-一个完全不相干的对象。无论如何,使用虚悬引用的结果都是不确定的,甚至可能带来安全风险。什么都可能发生。
(3)试图多次销毁同一个对象。这可能是、也可能不是灾难性的,具体取决于析构器中的代码怎么写。
对于C#这种将健壮性和安全性摆在首要位置的语言,这些问题显然是不能接受的。取而代之的是,必须由垃圾回收器负责销毁对象。垃圾回收器能做出以下几点担保。
(1)每个对象都会 被销毁,它的析构器会运行。程序终止时,所有未销毁的对象都会被销毁。
(2)每个对象只被销毁一次。
(3)每个对象只有在它不可达时(不存在对该对象的任何引用)才会被销毁。
但要注意,垃圾回收不一定在对象不再需要之后立即进行。垃圾回收可能是一个代价较高的过程,所以“运行时”只有在觉得必要时才进行垃圾回收(例如,在它认为可用内存不够的时候,或者堆的大小超过系统定义阀值的时候)。
注意:可通过静态方法System.GC.Collect 在程序中调用垃圾回收器。但除非万不得已,否则不建议这样做。System. GC.Collect方法将启动垃圾回收器,但回收过程是异步发生的方法结束时,程序员仍然不知道对象是否已被销毁。让CLR决定垃圾回收的最佳时机!
1.3垃圾回收器的工作原理
垃圾回收器是非常复杂的软件,能自行调整,并进行了大量优化以便在内存需求与应用程序性能之间取得良好平衡。内部算法和结构比较复杂(Microsoft自己也在不断改进垃圾回收器的性能),但它采取的大体步骤如下。
(1)构造所有可达对象的 一个映射(map).为此,它会反复跟随对象中的引用字段。垃圾回收器会非常小心地构造映射,确保循环引用(你引用我,我引用你)不会造成无限递归任何不在映射中的对象肯定不可达。
(2)检查是否有任何不可达对象包含一个需要运行的析构器(运行析构器的过程称为“终结”)。需终结的任何不可达对象都放到一个称为freachable (发音是F-reachable)的特殊队列中。
(3)回收剩下的不可达对 象(即不需要终结的对象)。为此,它会在堆中向下面移动可达
的对象,对堆进行“碎片整理",释放位于堆项部的内存。一个可达对象被移动之后,会更新对该对象的所有引用。
(5)然后, 允许其他线程恢复执行。
(6)在一个独立的线程中,对需要终结的不可达对象(现在,这些对象在freachable队列中了)执行终结操作。
1.4慎用析构器
写包含析构器的类,会使代码和垃圾回收过程变复杂。此外,还会影响程序的运行速度。如果程序不包含任何析构器,垃圾回收器就不需要将不可达对象放到freachable 队列并对它们进行“终结”(也就是不需要运行析构器)。显然,一件事情做和不做相比,不做会快一些。所以,除非确有必要,否则请尽量避免使用析构器。例如,可以改为使用using语句,待会讨论
写析构器时要小心。尤其注意,如果在析构器中调用其他对象,那些对象的析构器可
能已被垃圾回收器调用。记住,“终结” (调用析构器的过程)的顺序是得不到任何保障的。
所以,要确定析构器不相互依赖,或相互重叠(例如,不要让两个析构器释放同一个资源)。
2.资源管理
有时在析构器中释放资源并不明智。有的资源过于宝贵,用完后应马上释放,而不是等待垃圾回收器在将来某个不确定的时间释放。内存、数据库连接和文件句柄等稀缺资源应尽快释放。这时唯一-的选择就是亲自释放资源。这是通过自己写的资源清理(disposal)方法来实现的。可显式调用类的资源清理方法,从而控制释放资源的时机。
2.1资源清理方法
实现了资源清理方法的一个例子是来自System. IO命名空间的TextReader类。该类提供了从顺序输入流中读取字符的机制。TextReader 包含虚方法Close,它负责关闭流,这就是一个资源清理方法。StreamReader 类从流(例如- 个打开的文件)中读取字符,StringReader类则从字符串中读取字符。这两个类均从TextReader类派生,都重写了Close方法。下例使用StreamReader类从文件中读取文本行并在屏幕上显示:
TextReader reader = new StreanReader(filename);
string line;
while (line = reader ,ReadLine()) != null)
{
Console .WriteLine(1ine);
}
reader.Close();
但这个例子存在一个问题,即它不是异常安全的。如果对ReadLine(或WriteLine)的调用抛出异常,对Close的调用就不会发生。如果经常发生这种情况,最终会耗尽文件句柄资源,无法打开任何更多的文件。
2.2异常安全的资源清理
对上面的例子进行改进:
TextReader reader = new StreanReader(filename);
try
{
string line;
while ((line = reader. ReadLine()) != null)
{
Console.WriteLine(1ine);
}
}
finally
{
reader .Close();
}
像这样使用finally块是可行的,但由于它存在几个缺点,所以并不是特别理想。
(1)要释放多个资源, 局面很快就会变得难以控制(将获得嵌套的try和finally块)。
(2)有时可能需要修改代码来适应这一惯用法(例如,可能需要修改资源引用的声明顺序,记住将引用初始化为null,并记住查验finally块中的引用不为null)。
(3)它不能创建解决方案的 -一个抽象。这意味着解决方案难以理解,必须在需要这个
功能的每个地方重复代码。
(4)对资源的引用保留在 finally块之后的作用域中。这意味着可能不小心使用一个已释放的资源。
using语句就是为了解决所有这些问题而设计的。
2.3using语句和IDisposable接口
using语句提供了一个脉络清晰的机制来控制资源的生存期。可以创建一个对象,这个对象会在using语句块结束时销毁。
using语句的语法如下:
using ( type variable = initialization )
{
statementBlock
}
下面是确保代码总是在TextReader上调用Close的最佳方式:
using (TextReader reader = new StreanReader(filename))
{
string line;
while ((line=reader .ReadLine()) != null)
{
Console.Writeline(line);
}
}
这个using语句完全等价于以下形式:
TextReader reader =new StreanReader(filename);
try
{
string line;
while ((line =reader.ReadLine()) !=null)
{
Console .WriteLine(line);
}
finally
{
if (reader != null)
{
((IDisposable)reader).Dispose();
}
}
注意:using语句引入了它自己的代码块,这个块定义了一个作用城。也就是说,在语句块的末尾,using 语句所声明的变量会自动离开作用城,所以不可能因为不小心而访问已被清理的资源。
using语句声明的变量的类型必须实现IDisposable 接口。IDisposable 接口在System命名空间中,只包含-一个 名为Dispose的方法:
namespace System
{
interface Idisposable
{
void Dispose();
}
}
Dispose方法的作用是清理对象使用的任何资源。StreamReader 类正好实现了IDisposable接口,它的Dispose方法会调用Close来关闭流。可将using语句作为一种清晰、异常安全以及可靠的方式来保证一个资源总是被释放。 这解决了手动try/finally方案存在的所有问题。新方案具有以下特点。
(1)需要清理多个资源时, 具有良好的扩展性。
(2)不影响程序 代码的逻辑。
(3)对问题进行良好抽象, 避免重复性编码。
(4)非常健壮: using语句结束后,就不能使用using语句中声明的变量(前一个例子是reader),因为它已离开作用域。非要使用会产生编译时错误。
参考书籍:《Visual C#从入门到精通》