C# 析构函数解析(译)
Chandra Hundigam
2002-06-18
原文地址:
这篇文章谈了如何理解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 | .method family hidebysig virtual instance void |
编译器生成的Finalize方法包含了在try块中的析构函数,并紧接着一个调用基类的Finalize方法的finally块。这就保证了析构函数总是调用了其基类的析构函数。从这里我们的结论是,在C#中Finallize是析构函数的别名。
需要记住的知识点:
1. 析构函数自动被调用,而且不能被显式调用。
2. 析构函数不能被重载。也就是说,一个类最多只能有一个析构函数。
3. 析构函数不会继承。因此,一个类要么没有析构函数,要么有一个在内部生命的析构函数。
4. 不能在结构体(struct)中定义析构函数。只能在类中定义析构函数。
5. 一个实例,如果它不再可能被任何代码使用,那么它就可能被析构。
6. 析构动作可能在任何时候被执行,只要它满足条件5。
7. 当一个实例被析构了,那么它的继承链上的地析构函数也会被顺序调用,顺序是从最深层到最近层。
2429

被折叠的 条评论
为什么被折叠?



