I
import
Unregistered / Unconfirmed
GUEST, unregistred user!
Effective Delphi
条款1:不管怎么样,请让你的Project至少user一次SysUtils.pas单元
很多使用Delphi的人都对Delphi有着这样一个抱怨:Delphi虽然开发效率高,但是其编译出来的程序却是太大。使用Delphi5新建一个Project然后直接编译,程序的Size就已经达到了286KB,而如果把同样的程序放到Delphi7下面编译的话,那么其Size更是达到了360KB。正是由于这点,所以为Delphi编译生成的应用程序“减肥”便成为了几乎所有Delphi社区的一个保留性话题。
其间,大多数人都是使用可执行文件压缩工具(比如Aspack或者Upx等)来压缩Delphi所生成的可执行文件以达到“减肥”的目的,但是也有一些人,他们使用一种更为极端,但是更有效的方式来减少Delphi编译生成的可执行文件的Size,那就是抛弃VCL所提供的编程框架,而直接使用WIN32 SDK加上Object Pascal所提供的面向对象来能来进行程序的撰写。比如以下一段程序,使用Delphi5的编译器进行编译其大小只有16KB,而写一个基本的带窗口的Window程序其大小也不会超过25KB(以下这段程序使用Delphi3编译后会更小,其原因请见下述):
program SmallPro;
uses
Windows;
{$R *.RES}
begin
MessageBox(0, 'Hello World!', 'Information', MB_OK);
end.
请大家注意,以上程序是在project文件内直接编译,所以没有引用到其它的自定义单元。而所包含的Windows单元则只是为了调用MessageBox API函数而必须包含的。
这个编译出来的程序实在是太小了,小到它足以对那些熟悉Windows SDK方式编程,而又使用Delphi作为开发工具的人产生一定的诱惑力(我自己就算一个:->)。不知道这种方式是否同样也对你产生过诱惑力或者已经对你产生了诱惑力,如果是的话,那么先请听我一句忠告,“请为你的Project Uese上SysUtils.pas单元吧,否则你的程序将失去使用异常机制的能力,如果你不接这条忠告的话,你早晚会为你的行为任出代价。”
关于异常处理,各人的看法不同,有的人认为它是一种极美妙的错误处理方式:因为它能够使程序代码中处理错误部分的代码与实现逻辑部分的代码分离,使程序的源代码变得更优雅且撰写起来更方便和易读。而有人则认为使用异常机制来处理程序中的错误是不好的行为:因为一旦异常被触发,并且你未对其加控制的话,那么这个异常将导致应用程序终止,这种错误处理方式太过直接和粗鲁。但是,不管怎样,无庸置疑一点的是,你的程序代码可以不使用异常机制来处理错误,但是你却无法预计在你的代码当中所调用的各种库函数或者类是否使用或者支持异常机制,所以为了保证你程序的鲁棒性,即使你的代码不使用异常机制,那么你也应该在你代码的关键位置,加入异常处理的代码,以免你的代码所调用的其它代码或者操作系统抛出异常,导致程序意外的终止。下面便是一个简单的小例了:
program SmallPro;
uses
Windows,
SysUtils;
{$R *.RES}
var
p: PChar;
begin
try
p := nil;
p^ := 'l';
except
MessageBox(0, 'Exception', 'Information', MB_OK);
end;
end.
以上程序向地址空间0x00000000写一个字节的数据,在现在所有版本的Windows操作系统下面,这都将被系统视为非法操作,所以操作系统会抛出一个SHE异常,而Delphi的RTL系统会使你的程序能够栏截住这个异常并加以处理,如果你的程序没有处理这个异常的话,那么Delphi的RTL会弹出一个显示异常信息的对话框,并在你按下对话框的“确定”按钮后终止整个程序。我们上面的程序处理了这个异常,程序将在弹出MessageBox函数所显示的对话框后继续执行try…except.块后面的代码。
下面我们将上面的这个例子做一个很小的改动,将uses的SysUtils.pas单元去掉,然后再运行看看会出现什么样结果。
程序执行的结果和uses了SysUtils.pas单元的版本有着相当大的差异,程序只会显示一个如下图所示的:Runtime Error的对话框,然后便终止运行了,我们的异常处理块try..except则根本就没有起到作用。
(图1:运行时错误)
经过以上的测试,我想你已经能够明白,如果想让你的使用Delphi编译器所编译出来的程序能够支持异常机制的话,那么你就必须去在你的项目当中至少的包含的一次SysUtils.pas单元。写到此处,此条款应该可以说是功德圆满,但是我想我还是有必要带你简单的了解一下Delphi的整个异常处理机制,以便你能够对SysUtils.pas单元在整个Delphi异常机制中所占的地位有一个进一步的认识,并能够做到更好的使用它。
追根溯源,Delphi的编译器其实会向C/C++编译器一样为你的程序在链接时插入一段启动代码来使操作系统能够调用它,并启动整个应用程序(这个不是C/C++的main函数,如果你对这方面感兴趣的话,我建议你去读Jeffry Richter所著的《Programming Applications for Microsoft Windows Fourth Edition》,这本书的第4章对Processes的讲述中有相关的描述)。
对于以EXE形式存在的和以DLL形式存在的程序来说,Delphi为它们插入的启动代码的名称是不一样的,对于EXE型程序来说,Delphi会为你的程序插入其一个名称为_InitExe的过程,而对于DLL型程序来说,Delphi编译器会为你的程序插入一个名称为_InitLib的过程,你可以从SysInit.pas单元的源代码当中找到这两个过程的定义和实现。说到这里顺便提一句,System.pas和SysInit.pas两个Pascal单元是Delphi编译器在编译程序时默认包含的两个单元(你从来没有见到过哪一个程序uses过这两个单元吧)。而Delphi的每一个版本几乎都会对这两个单元进行扩展和修改,也正因为这个原因,所以在前面你看到的使用Delphi7编译的那个小程序的Size要比Delphi5编译出来的同样程序大的多。
在_InitExe过程的内部会调用一个名称为_StartExe的过程,而在这个_StartExe过程的内部中则会去调用在System.pas单元中定义的SetExceptionHandler函数来初始化整个Delphi的异常处理机制,在这个过程中设置的_ExceptionHandler过程则正是Delphi整个异常处理机制的核心处理过程。
在_ExceptionHandler会使用到System.pas单元中定义的一系列过程指针变量(比如ExceptProc,ExceptClsProc,ExceptObjProc等),这些过程指针变量都是Delphi整个异常机制当中必须的使用到的,而这些变量的初始化工作便是在SysUtils.pas单元中定义的InitExceptions单元中,InitExceptions变量会在SysUtils单元的initialization部分被调用,于是整个Delphi的异常处理机制便初始化完成。对于DLL型的程序,其异常处理过程的初始化部分与EXE型的程序一样,所以在这里就不再复述了。
好了在介绍了Delphi最核心的异常处理过程之后,我们再来介绍一下这些异常处理过程是如何被触发的。
当是一个异常被触发后,操作系统会最先拦截到这个异常。在操作系统拦截到这个异常后,它会马上调用.KiUserExceptionDispatcher函数(注1),这个函数是的Windows操作系统自身使用的异常处理函数,而在KiUserExceptionDispathcher函数调用的过程中,它会通过某种回调机制,最终去调用我们上面提到过的_ExceptionHandler过程,展开异常并处理之,如果没有找到如果被抛出异常所匹配的异常,那么则调用在SysUtils.pas中被赋值的ExceptHandler过程指针变量,抛出一个出现异常信息的话框,并在用户按下确定按钮之后终止程序。
这里面值得一的是,如果ExceptHandler过程指针变量的值为Nil,那么Delphi的RTL会去调用System.pas单元中定义的MapToRunError过程来做一个异常类型到运行时错误码的映射,并最终调用RunErrorAt过程在显示运行时错误对话框(如图1所示)终止整个程序的运行。
注1:我的操作系统是WIN2K所以KiUserExceptionDispatcher在ntdll当中,由于条件有限我没有在WIN98下做过类似的调试,不知道在WIN98下面是否也使用类似的方式来处理异常。另由于KiUserExceptionDispatcher函数微软未文档化的一个函数,所以我在这里不太方便对此函数的运作机制进行剖析(因为不同的操作系统中此函数的实现机制可能会不同),所以在这里还请您见谅。
条款1:不管怎么样,请让你的Project至少user一次SysUtils.pas单元
很多使用Delphi的人都对Delphi有着这样一个抱怨:Delphi虽然开发效率高,但是其编译出来的程序却是太大。使用Delphi5新建一个Project然后直接编译,程序的Size就已经达到了286KB,而如果把同样的程序放到Delphi7下面编译的话,那么其Size更是达到了360KB。正是由于这点,所以为Delphi编译生成的应用程序“减肥”便成为了几乎所有Delphi社区的一个保留性话题。
其间,大多数人都是使用可执行文件压缩工具(比如Aspack或者Upx等)来压缩Delphi所生成的可执行文件以达到“减肥”的目的,但是也有一些人,他们使用一种更为极端,但是更有效的方式来减少Delphi编译生成的可执行文件的Size,那就是抛弃VCL所提供的编程框架,而直接使用WIN32 SDK加上Object Pascal所提供的面向对象来能来进行程序的撰写。比如以下一段程序,使用Delphi5的编译器进行编译其大小只有16KB,而写一个基本的带窗口的Window程序其大小也不会超过25KB(以下这段程序使用Delphi3编译后会更小,其原因请见下述):
program SmallPro;
uses
Windows;
{$R *.RES}
begin
MessageBox(0, 'Hello World!', 'Information', MB_OK);
end.
请大家注意,以上程序是在project文件内直接编译,所以没有引用到其它的自定义单元。而所包含的Windows单元则只是为了调用MessageBox API函数而必须包含的。
这个编译出来的程序实在是太小了,小到它足以对那些熟悉Windows SDK方式编程,而又使用Delphi作为开发工具的人产生一定的诱惑力(我自己就算一个:->)。不知道这种方式是否同样也对你产生过诱惑力或者已经对你产生了诱惑力,如果是的话,那么先请听我一句忠告,“请为你的Project Uese上SysUtils.pas单元吧,否则你的程序将失去使用异常机制的能力,如果你不接这条忠告的话,你早晚会为你的行为任出代价。”
关于异常处理,各人的看法不同,有的人认为它是一种极美妙的错误处理方式:因为它能够使程序代码中处理错误部分的代码与实现逻辑部分的代码分离,使程序的源代码变得更优雅且撰写起来更方便和易读。而有人则认为使用异常机制来处理程序中的错误是不好的行为:因为一旦异常被触发,并且你未对其加控制的话,那么这个异常将导致应用程序终止,这种错误处理方式太过直接和粗鲁。但是,不管怎样,无庸置疑一点的是,你的程序代码可以不使用异常机制来处理错误,但是你却无法预计在你的代码当中所调用的各种库函数或者类是否使用或者支持异常机制,所以为了保证你程序的鲁棒性,即使你的代码不使用异常机制,那么你也应该在你代码的关键位置,加入异常处理的代码,以免你的代码所调用的其它代码或者操作系统抛出异常,导致程序意外的终止。下面便是一个简单的小例了:
program SmallPro;
uses
Windows,
SysUtils;
{$R *.RES}
var
p: PChar;
begin
try
p := nil;
p^ := 'l';
except
MessageBox(0, 'Exception', 'Information', MB_OK);
end;
end.
以上程序向地址空间0x00000000写一个字节的数据,在现在所有版本的Windows操作系统下面,这都将被系统视为非法操作,所以操作系统会抛出一个SHE异常,而Delphi的RTL系统会使你的程序能够栏截住这个异常并加以处理,如果你的程序没有处理这个异常的话,那么Delphi的RTL会弹出一个显示异常信息的对话框,并在你按下对话框的“确定”按钮后终止整个程序。我们上面的程序处理了这个异常,程序将在弹出MessageBox函数所显示的对话框后继续执行try…except.块后面的代码。
下面我们将上面的这个例子做一个很小的改动,将uses的SysUtils.pas单元去掉,然后再运行看看会出现什么样结果。
程序执行的结果和uses了SysUtils.pas单元的版本有着相当大的差异,程序只会显示一个如下图所示的:Runtime Error的对话框,然后便终止运行了,我们的异常处理块try..except则根本就没有起到作用。
(图1:运行时错误)
经过以上的测试,我想你已经能够明白,如果想让你的使用Delphi编译器所编译出来的程序能够支持异常机制的话,那么你就必须去在你的项目当中至少的包含的一次SysUtils.pas单元。写到此处,此条款应该可以说是功德圆满,但是我想我还是有必要带你简单的了解一下Delphi的整个异常处理机制,以便你能够对SysUtils.pas单元在整个Delphi异常机制中所占的地位有一个进一步的认识,并能够做到更好的使用它。
追根溯源,Delphi的编译器其实会向C/C++编译器一样为你的程序在链接时插入一段启动代码来使操作系统能够调用它,并启动整个应用程序(这个不是C/C++的main函数,如果你对这方面感兴趣的话,我建议你去读Jeffry Richter所著的《Programming Applications for Microsoft Windows Fourth Edition》,这本书的第4章对Processes的讲述中有相关的描述)。
对于以EXE形式存在的和以DLL形式存在的程序来说,Delphi为它们插入的启动代码的名称是不一样的,对于EXE型程序来说,Delphi会为你的程序插入其一个名称为_InitExe的过程,而对于DLL型程序来说,Delphi编译器会为你的程序插入一个名称为_InitLib的过程,你可以从SysInit.pas单元的源代码当中找到这两个过程的定义和实现。说到这里顺便提一句,System.pas和SysInit.pas两个Pascal单元是Delphi编译器在编译程序时默认包含的两个单元(你从来没有见到过哪一个程序uses过这两个单元吧)。而Delphi的每一个版本几乎都会对这两个单元进行扩展和修改,也正因为这个原因,所以在前面你看到的使用Delphi7编译的那个小程序的Size要比Delphi5编译出来的同样程序大的多。
在_InitExe过程的内部会调用一个名称为_StartExe的过程,而在这个_StartExe过程的内部中则会去调用在System.pas单元中定义的SetExceptionHandler函数来初始化整个Delphi的异常处理机制,在这个过程中设置的_ExceptionHandler过程则正是Delphi整个异常处理机制的核心处理过程。
在_ExceptionHandler会使用到System.pas单元中定义的一系列过程指针变量(比如ExceptProc,ExceptClsProc,ExceptObjProc等),这些过程指针变量都是Delphi整个异常机制当中必须的使用到的,而这些变量的初始化工作便是在SysUtils.pas单元中定义的InitExceptions单元中,InitExceptions变量会在SysUtils单元的initialization部分被调用,于是整个Delphi的异常处理机制便初始化完成。对于DLL型的程序,其异常处理过程的初始化部分与EXE型的程序一样,所以在这里就不再复述了。
好了在介绍了Delphi最核心的异常处理过程之后,我们再来介绍一下这些异常处理过程是如何被触发的。
当是一个异常被触发后,操作系统会最先拦截到这个异常。在操作系统拦截到这个异常后,它会马上调用.KiUserExceptionDispatcher函数(注1),这个函数是的Windows操作系统自身使用的异常处理函数,而在KiUserExceptionDispathcher函数调用的过程中,它会通过某种回调机制,最终去调用我们上面提到过的_ExceptionHandler过程,展开异常并处理之,如果没有找到如果被抛出异常所匹配的异常,那么则调用在SysUtils.pas中被赋值的ExceptHandler过程指针变量,抛出一个出现异常信息的话框,并在用户按下确定按钮之后终止程序。
这里面值得一的是,如果ExceptHandler过程指针变量的值为Nil,那么Delphi的RTL会去调用System.pas单元中定义的MapToRunError过程来做一个异常类型到运行时错误码的映射,并最终调用RunErrorAt过程在显示运行时错误对话框(如图1所示)终止整个程序的运行。
注1:我的操作系统是WIN2K所以KiUserExceptionDispatcher在ntdll当中,由于条件有限我没有在WIN98下做过类似的调试,不知道在WIN98下面是否也使用类似的方式来处理异常。另由于KiUserExceptionDispatcher函数微软未文档化的一个函数,所以我在这里不太方便对此函数的运作机制进行剖析(因为不同的操作系统中此函数的实现机制可能会不同),所以在这里还请您见谅。