您的位置:首页 > 编程语言 > C#

C# 析构函数解析(译)

2012-04-24 23:07 796 查看
C# 析构函数解析(译)

Chandra Hundigam

2002-06-18

原文地址:

http://www.c-sharpcorner.com/UploadFile/chandrahundigam/UnderstandingDestructors11192005021208AM/UnderstandingDestructors.aspx

这篇文章谈了如何理解C#的析构函数的工作原理。我理解你们的疑问:为什么在这么简单的析构函数上花费一篇专门的文章来讲述。

介绍:

这篇文章谈了如何理解C#的析构函数的工作原理。我理解你们的疑问:为什么在这么简单的析构函数上花费一篇专门的文章来讲述。等你读完这篇文章,你将会理解跟C++的析构器相比,C#的析构函数是多么的不同。

简单来说,析构函数是一个成员,它实现那些自毁一个类实例所需要的行为。析构器能保证运行时系统,恢复堆空间,关闭绑定在要删除的类实例上的文件I/O,或者两者都有。为了更好的理解,我将把C++的析构函数和C#的析构函数做个比较。

一般来说,在C++中,析构函数在对象销毁是被调用。在C++中我们可以显式的调用析构函数。而且,对象的销毁顺序跟它们的创建顺序是相反的。所以在C++中,你是可以控制析构函数的。

你可能会想C#是怎么对待析构函数的。在C#中,你永远不能调用析构函数,原因是你没法销毁一个对象。那么谁能控制析构函数呢(在C#中)?它是.NET framework的垃圾收集器(GC).

现在就有问题了:为什么GC控制析构函数呢?而不是我们(程序员)自己呢?

答案是很简单的, 对于释放对象,GC能比我们做的更好。如果让我们来做手动的内存管理,我们将不得不同时注意内存的分配和释放。因此总是有可能你会忘记释放内存。而且,手动内存管理会消耗你的时间,并且是一个复杂的过程。那么就让我们来了解一下为什么C#禁止你写下显示的调用析构函数的代码。

1. 如果我们访问非托管代码,我们经常会忘记去销毁对象。这就会漏掉调用析构函数,被对象占有的内存因而永远得不到释放。

让我们通过这个例子来检查这种情况。下图展示了应用程序XYZ加载一段30字节非托管代码的内存栈的情形。

当程序XYZ结束时,想象一下它忘了销毁一个对象,这个对象有非托管代码,那么将要发生的情况是,程序XYZ的内存释放回到堆中,但是非托管代码仍然在内存中。这就造成内存浪费(内存泄漏)。

2. 如果我们试图释放对象仍在做一些处理,我的意思是对象还是活跃的。

3. 如果我们试图释放的对象已经被释放。

那么,让我们来看看GC将会怎么处理上面的情形:

1. 如果程序结束,GC自动回收程序中对象占有的内存。

2. GC跟踪所有的对象,保证每个对象只被销毁一次。

3. GC保证正被引用的对象不会被销毁。

4. GC只在需要的时候销毁对象。一些必要的时机是内存耗尽或者用户显式调用了System.GC.Collect()方法。

理解垃圾收集器(GC)的全部工作是一个比较大的话题。我会涵盖跟本文主体相关的细节。GC是一个.NET Framework的线程,它会在需要的时候,或者是其他的线程处在挂起状态的时候才运行。首先,GC通过遍历对象内部维护的引用字段,创建一个程序使用的所有对象的列表。然后,它会保证这个列表内部没有循环引用。在这个列表中,GC接着查看所有的对象,把那些有析构函数的对象放到另一个被称为FinalizationList(终结表)的列表

那么现在,GC生成了2个线程,一个是可控表,一个是不可控表(或终结表)。可控对象被一个接一个的从列表中清除掉,同时由这些对象占有的内存也被回收。第二个线程,读终结表,并调用每个对象的终结方法。

让我们看看C#编译器是如何理解析构函数代码的。下面是在VS .NET中创建的一个小的类。我创建了一个叫做class1的类,它有一个构造函数和一个析构函数。

using System;

namespace ConsoleApplication3

{

///<summary>

/// Summary description for Class1.

///</summary>

class Class1

{

public Class1()

{}

~Class1()

{}

static voidMain(string[] args)

{

//

// TODO: Add code to start application here

//

Class1 c= new Class1();

}

}

}

编译完代码后,在ILDASM.EXE(微软的反汇编工具)中打开汇编文件,查看IL代码。你将会看到一些不寻常的代码。你会看到编译器自动把析构函数翻译成了一个从Object.Finalize()重写的方法。换句话说,编译器把下面的析构函数:

class Class1

{

~Class1(){}

}

转换为:

In Source Code Format:

In IL Code Format:

class Class1

{

Protected override void Finalize()

{

try{..}

finally { base.Finalize();}

}

}

.method family hidebysig virtual instance
void

Finalize() cil managed

{

// Code size 10 (0xa)

.maxstack 1

.try

{

IL_0000: leave.s IL_0009

} // end .try

finally

{

IL_0002: ldarg.0

IL_0003: call instance void [mscorlib]System.Object::Finalize()

IL_0008: endfinally

} // end handler

IL_0009: ret

} // end of method Class1::Finalize

编译器生成的Finalize方法包含了在try块中的析构函数,并紧接着一个调用基类的Finalize方法的finally块。这就保证了析构函数总是调用了其基类的析构函数。从这里我们的结论是,在C#中Finallize是析构函数的别名。

需要记住的知识点:

1. 析构函数自动被调用,而且不能被显式调用。

2. 析构函数不能被重载。也就是说,一个类最多只能有一个析构函数。

3. 析构函数不会继承。因此,一个类要么没有析构函数,要么有一个在内部生命的析构函数。

4. 不能在结构体(struct)中定义析构函数。只能在类中定义析构函数。

5. 一个实例,如果它不再可能被任何代码使用,那么它就可能被析构。

6. 析构动作可能在任何时候被执行,只要它满足条件5。

7. 当一个实例被析构了,那么它的继承链上的地析构函数也会被顺序调用,顺序是从最深层到最近层。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: