摘 要 树型控件是一种有效的表示方法,在数据库应用程序的开发中经常用它可视化表示对象之间的层次关系。本文介绍了Developer Express inc. 的ExpressQuantumGrid Suite中树型控件 TdxDBTreeList 的使用方法和技巧,特别是由多关系表构成树型结构的情况。
关键词 树型控件,TdxDBTreeList,多关系表
简介
树型控件是一种有效的表示方法,在数据库应用程序的开发中经常用它可视化表示对象之间的层次关系,便于用户的操作和使用。比如:表示一个公司部门的层次结构;磁盘文件系统等。Borland 公司的Delphi 开发系统提供了一般的树型控件TTreeView,它可以在设计时或运行时生成必要的树型结构图,方法是通过生成控件对象的 Items 属性。设计时方式通过手工方式增加items,主要适用于静态、少量的 items 的情况;运行时方式通过程序生成树型结构的 items,它既适合静态又可用于动态的情况。在实际工作中,树状结构中的 items 经常来自数据库的一个或若干个关系表中,若想利用 TTreeView 控件生成相关的树状结构,设计时方式很难实现,运行时方式虽然可以实现,但需要程序员设计大量的代码,而且容易出错。因此,为了提高开发效率,减少重复开发,我们必须使用 data-aware 树型控件。Developer Express inc. 的ExpressQuantumGrid Suite 中树型控件TdxDBTreeList 属于 data-aware 型,items 不仅可以来自数据库的关系表,而且可以为数组等其它数据源。本文介绍了TdxDBTreeList 的使用方法和技巧,特别是由多关系表构成树型结构的情况。
TdxDBTreeList 概述
Developer Express inc. 开发的The ExpressQuantumGrid Suite 包含三个功能强大的控件,它们分别是:TdxTreeList 、 TdxDBTreeList 和 TdxDBGrid。TdxDBTreeList 直接继承于 TCustomdxDBTreeListControl ,它是 data-aware 型的,不仅结合标准的树型视图和列表控件的特性,而且具有独特的特性:运行时列定制、一个树结点内编辑任意列的能力和由dataset 生成树结构。
使用该树型控件关键是设定属性页中几个重要的属性,分别为:DataSource、KeyField、ParentField 和 Bands。其中 DataSource 为提供树型结构的数据源,可以为来自一个或若干个关系表的元组的集合,因此它是必须的且不为空;KeyField 为 DataSource 的元组中唯一标识自身的关键码;树型控件表示的是对象之间的层次关系,除树根结点之外(最上层项目),每一个结点都有一个唯一的双亲结点(直接上层结点)。ParentField 即是元组中标识其双亲结点的 KeyField 。根结点的 ParentField 值必须不是 DataSource 代表的元组中 KeyField 值之一;Bands 控制树型结构显示的列数,通过它提供的 Add 方法增加合适的 Columns ,Columns 有两个重要的属性:Caption 和 FieldName 。Caption 可以设定树型结构的标题,FieldName 代表 DataSource 的元组表示树型结构项目的字段。例如用如下的部门关系表作为 DataSource 形成树型结构:
表1 unti 关系表
UnitNo
unitName
superiorUnit
0000
东南大学
-1
0001
计算机系
0000
000101
硬件教研室
0001
000102
软件教研室
0001
0002
外语系
0000
000201
大外教研室
0002
000202
专外教研室
0002
其中,unitNo 表示部门号;unitName 部门名称;superiorUnit 上级部门号。KeyField 设为unitNo;ParentField 设为 superiorUnit ;Bands 的一列 Columns 的 FieldName 设为 unitName 。这样形成如图1所示的树型结构。

图1 树型结构示例图
多关系表生成的树结构
实际应用中经常需要由两个或若干个关系表构成树型结构,从表面上看,TdxDBTreeList 仅支持一个关系表。我们可以巧妙地利用 SQL 中功能强大的 SELECT 语句生成必要的数据源 DataSource 。SELECT 语句不仅可以由一个关系表生成元组集合,还可以由多表生成,语法形式为:
select ………. Union select …….. union select ………。
如存在如下两个关系表 unit 部门表和 teacher 教师表:
create table unit (
unitNo varchar2(6) not null, ………..部门号
unitName varchar2(30), ……….部门名称
superiorUnitNo varchar2(6), …..上级部门号
primary key (unitNo ))
create teacher (
teacherNo varchar2(8) not null , ……..教师号
teacherName varchar2(10) , ……..教师名
unitNo varchar(6), ……..教师所在教研室号
primary key (teacherNo),
foreign key unitNo(unitNo) references unit )
其中,teacher 表的字段 unitNo 为引用unit表的外键。假设unit 的关系表内容同表1;teacher的内容如下:
表2 teacher 关系表
teacherNo
teacherName
unitNo
00010001
Computer1
000101
00010002
Computer2
000102
00020001
Foreign1
000201
00020001
Foreign2
000202
如要生成如图2所示的来自unit 和 teacher 两个关系表数据的树型结构:

图2 待生成的unit 和 teacher 两个关系表数据的树型结构
可以用下列语句:
select unitNo||’ ‘ as unitNo,unitName, superiorUnit
from depart
union
select teacherNo as unitNo,teacherName as unitName,unitNo||’ ‘ as superiorUnit from teacher
生成的数据集作为 TdxDBTreeList 的数据源;unitNo 作为 KeyField;superiorUnit 作为 ParentField 等。需要注意的是,其中 uintNo||’ ‘是一种字符串连接运算,之所以要这么做是因为 unit 关系模式中 unitNo 的字段宽度为 6 位,而 teacher 中的 teacherNo 为 8 位,作为 TdxDBTreeList 的 KeyField 必须统一二者的宽度,否则可能出现问题,读者可以亲自试验。
结论
树型控件是一种强有力的交互工具,利用第三方提供的 data-aware 树型控件可以效、可靠地开发应用程序。SQL 中的 SELECT 是一个功能强大的语句,灵活的使用它可以实现过程性语言所无法代替的效果。欲了解 TdxDBTreeList 的详细情况可以查看该公司的网站 www.devexpress.com 。
|
delphi运行错误信息 |
|||
|
|||
|
|
delphi编译错误信息 |
|||
|
|
|||
|
|||
|
初级优化篇 说到优化,很多人又不屑一顾了,“现在计算机速度都那么快了,再快那么百分之几有什么意义啊”。这么说确实有些道理,现在的编译器编译后的结果已经是充分优化过了,除了图形图像多媒体等特定软件的开发外、多数情况下刻意的优化确实没必要,但是如果开发人员在编写代码的时候已经具有了优化意识,在完成优化的同时,又能保证了甚至提升开发效率,何乐而不为呢? 当然,算法的设计都是优化的核心,绝大多数情况下,程序的执行效率高低主要由开发人员对程序整体把握,算法的设计等来决定!但有时候针对细节的优化也是有一定意义的! 而且这种优化在很多情况下也并不需要直接通过汇编来写代码实现,但这种情况下却也能体现出掌握汇编知识的优越性! 如下面两个函数: function GetBit(i: Cardinal; n: Cardinal): Boolean; function GetBit(i: Cardinal; n: Cardinal): Boolean; 对应的汇编代码: MOV ECX, EDX MOV ECX, EDX 它们的作用一样,都是取i某位的值,为1返回True,0返回False! 表面上看可能都会认为两个函数的执行效率一样,实际上还是有区别的,第一段程序是的移位操作是对i进行的,按照Delphi中默认的调用约定register,此时的i的值是存在寄存器EAX中,移位操作可直接完成;而第二段程序则不同,要对立即数1完成移位操作,必须先将其传送到寄存器,由此也就必然多出一条指令!当然也不是所有情况下,指令少就一定比指令多要快,具体执行时还要考虑指令执行的时钟周期和指令的配对等问题(后面再介绍些),独立出来也说明不了问题,只有在具体代码环境中才好作比较。 一般情况下这种效率上的执行差异实在是太微不足道了,但在编程期间时刻保持着优化的意识绝不是件坏事!如果此类代码位于循环的最里层,N个时钟周期经过大量循环的累积,产生的执行效率差异也可能变的很大! 上面只是个很小的例子,由此可以看出在开发中如果能站在汇编的角度思考一些问题,能在保证开发效率的同时用高级语言编写出更有效率的细节代码!但还有很多时候,细节优化还要用使用嵌入汇编代码来完成,而且有些时候由于嵌入汇编代码应用,还能使代码编写变得更有效率。 如需要将一个32位数的字节顺序颠倒,在Delphi中,完全用高级语言实现怎么做?用移位可以,多次调用内建函数Swap也可以,但是如果想到一条BSWAP指令,这一切变得很简单。 function SwapLong(Value: Cardinal): Cardinal; 注:同上,Value的值是存在寄存器EAX中,而32位数的值也通过EAX返回,所以只需要一句即可。 当然多数的嵌入汇编优化没有这么简单,不过通过大学里所学的那一点点汇编知识也很难做到更深入的优化,也只能通过不断的积累,对比编译后的汇编代码获取经验!好在多数情况下,细节优化并不是程序设计的主体。 但如果所开发程序涉及到图形图像多媒体等方面,还是有必要进行更深入的优化的!好在不管是浮点指令的优化还是应用MMX、SSE、3DNow等完成优化,Delphi6都能提供良好的支持。即使是想早期版本的Delphi支持这些CPU扩展指令集或者想要支持以后新的CPU指令集,利用Delphi在嵌入汇编中所支持的DB、DW、DD、DQ等四条汇编指令(在Borland的Delphi6官方语言手册里只说支持DB、DW、DD)插入相关指令的数值表示也能灵活的实现。 如: DW $A20F //CPUID DW $770F //EMMS 了解指令只是基础,在围绕FPU,MMX,SSE设计完算法后,想更深一步的进行优化,还必须了解一些CPU本身的技术特性。 先看看下面两段代码: asm asm 第二个效率高?错了,如上面说的,指令少不意味着执行效率高,查查相关资料可知,第一段代码的两条指令执行的时钟周期为3(每条指令都需要完成读、改、写三步),第二段代码中的6条指令执行的时钟周期都为1。那么说两段代码效率一样?又错了,实际上第二段代码执行效率比第一段代码要高!为什么?因为奔腾级以后的CPU都有两条流水线来执行指令,所以当相邻的两条指令能够完成配对,那么它们就能够同时执行!具体到上面的两段代码来说具体原因又是什么呢? 第一段代码中的两条指令虽然可以完成配对,但需要的总执行时钟周期为5而不是3,而第二段代码的六条指令可以两两之间并行执行,所以也就导致了这个结果。 说到这里,都是些很浅显的例子,本身给不了大家太多的帮助。如果真的想优化特定程序,还是找些FPU,MMX优化的专题文章看看,或者找来技术手册好好专研专研“乱序执行”和“分枝预测”等技术。只希望各位在上大学的朋友们不要只专注于那些“能赚钱”的开发工具和时髦的新技术,能把更多的时间花在打基础上,有了扎实的基础才能快速掌握新知识、才能用更快的时间掌握新的开发工具、才能…(省略一千字)。 不过话又说回来,知识还是要用来解决实际问题的,如果每天就只在技术细节上做文章,也许能成为一个出色的黑客,但绝对开发不出一流的软件。所以还是要以创造价值为根本目的。所以…不说了,再说下去就真不像技术文章了。^_^ 附:程序优化除了考虑执行效率以外,当然也要考虑体积的问题(体积小才能更快的载入内存,更快的完成指令译码等工作),比如清空EAX寄存器都是用SUB EAX, EAX或XOR EAX, EAX而不会用MOV EAX, $0,虽然它们的执行时钟周期都是1,但前者的指令长度(2字节)明显比后者(5字节)短。但因为上面说的都是些细节,所以没提到体积的问题。更多的缩小体积的问题还是交给编译器去解决吧,在编写嵌入ASM代码的同时稍微注意一下就可以了。
begin
Result := Boolean((i shr n) and 1);
end;
begin
Result := Boolean((1 shl n) and i);
end;
SHR EAX, CL
AND EAX, $01
MOV EDX, $01
SHL EDX, CL
AND EAX, EDX
asm
BSWAP EAX
end;
DB $0F, $6F, $C1 //MOVQ MM0, MM1
ADD [a], ECX
ADD [b], EDX
end
MOV EAX, [a]
MOV EBX, [b]
ADD EAX, ECX
ADD EBX, EDX
MOV [a], EAX
MOV [b], EBX
end
前言 很多人脑子里都有这么一些概念: “汇编啊?那是‘高手’们的专利,我用不上”、“我又不和系统打交道,学汇编干嘛啊”、“用汇编写程序的人是白痴(CSDN论坛里看到的原话),太没效率了” 同意的朋友一定不少!诚然现在的软件已经越来越庞大越,来越复杂,程序开发人员已经远离了那个只和二进制0、1代码,汇编助记符打交道的年代!就算是写系统软件也是如此,PC上的操作系统、编译工具绝大多数代码也是用高级语言完成的!如果现在有人说要完全用汇编在PC上开发软件(不是写程序),那确实不值得大家推崇!但任何问题都要辩证的来看,但如果能在应用高级语言开发软件的过程中合理的应用所学的汇编知识绝对是有百利而无一害(因为这里是说Delphi和汇编,移植等问题不考虑在内)。知识是用来解决实际问题的,所以没有必要排斥汇编,应该让其充分融入到我们应用高级语言开发的过程中去!虽然一味的强调技术至上不对,但也不能因此完全忽略代码细节。最好还是能在开发效率和执行效率中找到一个平衡。 如果掌握了汇编,如果使用其相关知识呢? 首当其冲的当然是利用嵌入汇编编写程序。在Delphi中的嵌入汇编程序很多情况可以参照Win32ASM。但由于嵌入汇编的特殊性,这里并不支持EQU、PROC、STRUC、SEGMENT等伪指令,它们实现的大部分功能已由Object Pascal实现。另外在寄存器的修改方面,除了ESI、EDI、EBP等外,EBX也不能被随便修改,因为在Delphi中用EBX保存Self指针。如果一定要用,必须用栈来保存/恢复寄存器中的原始数据。当然嵌入汇编只是解决问题的手段,不是目的,汇编代码会降低程序的可读性、可维护性能,在同等情况下理应优先考虑如何用高级语言实现。 利用嵌入汇编编写程序自然也不是学习汇编的唯一目的,更重要的掌握了一种从深层次理解程序的方法。即使不直接用汇编写程序,将这种用汇编思考问题的思想融入到用高级语言编写程序的过程中去,汇编一样成为我们分析解决问题的利器。另外汇编知识对于程序调试一样也有着不可替代的作用,在很多情况汇编层的调试可以更快的解决问题。由此可见,有些情况下汇编知识的应用不光是为了提高程序执行效率,有时候也是为了提高开发效率。 许多朋友并没有认真学习过汇编,就开始大喊汇编无用,大学课本无用就显得太不负责任了。有些朋友在大学里只求掌握几种时髦的开发工具和最新的开发技术,而忽视汇编这类基础知识的学习也不见得是明智的做法。相信只要冯.诺伊曼的计算机体系不发生改变,现有的汇编知识必然会有其用武之地。
2.调用动态链接库(DLL)方式
第二种方法比第一种方法实现起来麻烦一些。在这种方法中,FORTRAN程序首先被编译成Windows标准的动态链接库文件(DLL, Dynamic-Link Library),然后在Delphi中调用。在FORTRAN语言程序设计中,本文采用Compaq Visual Fortran6.6编译器,可以容易地生成动态链接库。
在这种方式混合编程中,由于需要在两种不同的语言之间进行内存中的数据交换,因此,其数据类型必须一一对应。由于不同语言的数据类型所对应的存储方式、数据传递方式不尽相同,而且程序调试需要在两个不同的编译器中进行,因此这种方法编译调试较为麻烦,不易解决编译中出现的一些问题。
在生成动态链接库的FORTRAN子程序中,必须采用以下方法进行说明:
!DEC$ATTRIBUTES DLLEXPORT::SUB_NAME 上面第一句话中,关键字DLLEXPORT表明这个子程序在动态链接库中可被外部调用,SUB_NAME为此子程序在动态链接库中的程序名;第二句话中的ALIAS给该程序名另赋一个别名,因为FORTRAN默认情况下编译出的程序名为大写字母,别名中可以改变。
函数和程序的调用中,参数传递的方式有两种。一种是传递参数地址的方式,即Call by Refence,另一种是传递值的方式,即Call by Value。在CVF生成动态链接库时,默认的通信协议为’_StdCall’,其参数传递方式是第一种。而在Delphi中,参数的传递方式跟参数本身的类型相关。如有一子过程定义:
Procedure Sub_Name(Const x1,x2:Double; Var x3,x4:Double);
这里,x1,x2被定义为常数类型的双精度实数,其参数传递方式为第二种,即Call by value,其值在传递中保持不变;x3, x4为变量类型的双精度整数,其传递方式为第一种,即Call by Reference,其值在计算中可以被改变*。在默认情况下,即参数前面没有说明属于那种类型的参数,则默认为常数类型。
也可以采用Delphi中的指针变量来获得与FORTRAN默认条件下完全相同的调用方式:
Procedure Sub_Name(x1,x2:Pointer;Var x3,x4:Double);
Pointer表明x1,x2是无类型指针(也可以采用有类型指针,这里从略),这时如果在另一程序中定义了四个双精度数a1,a2,a3,a4,按如下方式调用:
Sub_Name(@a1,@a2,a3,a4)
则a1,a2,a3,a4中的值在计算前后都是可以改变的。
以指针变量作为虚参对于Delphi和FORTRAN的混合编程中数组的传递很有意义。因为Delphi的数组和FORTRAN中不同,Delphi中固定的数组和动态数组的传递方式是不相同的。采用指针,就没有那么费事。如上例中,如果a1,a2是数组,那么其调用方式为:
Sub_Name(@a1[0],@a2[0],a3,a4)
就是将第一个数组元素的地址传递过去。如果不是第一个元素的地址,是后面某个元素的地址,则虚实数组的结合从这个元素开始。
如果虚参中出现了字符串,则有两种传递方式。一种是根据FORTRAN中的标准,同时传递字符串内容和长度,这时,FORTRAN中的子程序定义为:
Subroutine Sub_Name(x1,x2,Str,x3, x4)
!DEC$ATTRIBUTES DLLEXPORT::SUB_NAME Real(8),Dimension(:)::x1,x2
Character(Len=*) Str
Real(8),Intent(Out)::x3,x4
在Delphi中相应的接口过程定义为:
Procedure Sub_Name(Const x1,x2:Pointer;
Str:String;
Len:Integer;
Var x3, x4:Double);
则按如下方式调用:
Sub_Name(@a1[0],@a2[0],Str,Length(Str),a3,a4);
另一种方式更为简便,因为在Delphi中,字符串被视为动态数组,以Call by Refence方式传递,因此,可先在FORTRAN中使用编译字将字符串的传递方式强制为地址传递方式,即:
Subroutine Sub_Name(x1,x2,Str,x3, x4)
!DEC$ATTRIBUTES DLLEXPORT::SUB_NAME Real(8),Dimension(:)::x1,x2
Character(Len=*) Str
Real(8),Intent(Out)::x3,x4
!DEC$ Attributes Refence::Str
这时,Delphi中相应的接口过程定义为:
Procedure Sub_Name(Const x1,x2:Pointer; Str:String; Var x3, x4:Double);
则按如下方式调用:
Sub_Name(@a1[0],@a2[0],Str, a3,a4);
第二种混合编程方式的实现过程是:首先在FORTRAN子程序按上述方法定义好,并编译成动态链接库ForSub.Dll;然后在Delphi中按如下方法定义动态链接库子过程接口。在接口区(Interface)中定义过程首部:
Procedure Sub_Name(Const x1,x2 : Pointer;
Str : String;
Var x3, x4 : Double); Stdcall;
在实现区(Implementation)中加上以下语句:
Procedure Sub_Name; external ‘ ForSub.dll’ name ‘ Sub_Name ‘;
这样,在Delphi程序设计中就可以像调用自己的子程序一样调用Sub_Name了。
由于FOR90引入了模块(Module)单元,可以将一些相关的数据和方法封装在模块里,因此,对一个模块中不同的子程序进行上述的定义,则在一个动态链接库中获得多个可被外部程序调用的子程序。在Delphi中要调用这些子程序,对每一个都需要编写Delphi中的子过程接口。
* 在FOR90中借鉴了这种区分输入输出参数的定义方式,引入了关键字Intent,在参数定义中,Intent(in)表示该参数在传递过程中是不改变的,Intent(Out)表示是可以改变的,并且FOR90中规定得更为严格。
!DEC$ATTRIBUTES ALIAS:’Sub_AliasName’::SUB_NAME
!DEC$ATTRIBUTES ALIAS:’Sub_Name’::SUB_NAME
!DEC$ATTRIBUTES ALIAS:’Sub_Name’::SUB_NAME
众所周知,FORTRAN强于数值计算,尤其是如果计算主要针对复数进行,则FORTRAN更有无可比拟的优势。FORTRAN是所有语言中唯一将复数定义为一种标准数据类型的语言。但是FORTRAN语言在可视化程序设计方面是非常欠缺的,至少目前还没有一家厂商推出具有RAD特性的FORTRAN编译集成开发环境。因此,当用FORTRAN实现了一种大型的科学计算以后,却难以将这种计算转变为数据输入简易、结果显示方便的WINDOWS可视化应用程序。这一点,采用Delphi很容易实现。因此,在许多情况下,使用FORTRAN和Delphi的混合编程可同时具有二者的优点。 本文采用两种不同的方法来实现混合编程。一种是直接执行可执行文件的方式,一种是调用动态链接库中子程序的方式。在第一种方式下,在Delphi程序设计中直接执行FORTRAN程序的执行文件,通过文件来进行数据交换;在第二种情况下,首先将FORTRAN程序编译成动态链接库(DLL),在Delphi程序设计中,调用此动态链接库中某个子程序来完成某项计算。这两种方式各有优缺点。第一种方式的调试较为简单,不存在不同语言之间的数据类型的不匹配问题。但是,这种方式下,在Delphi中无法实现对程序运行的有效监督,同时,以文件进行数据交换在操作中也不太方便,效率也不高。第二种方式则整合了两种程序之间的差别,如果编制成功,程序运行时看不出混合语言编程的痕迹,但是这种方式调试起来特别麻烦。一般说来,对于已有的输入输出较为复杂的FORTRAN程序,可以考虑第一种方式,而对于相对简单的,或者自己着手编制的新的程序,可选用第二种。
1.执行可执行文件(exe)方式
Windows中提供了API函数WinExec来执行存在的执行文件。该函数定义为:
UINT WinExec(LPCSTR lpCmdLine, UINT uCmdShow );
参数说明如下: 系统将在以下范围查找应用程序: 如果Str为一记录可执行文件的路径及文件名变量,则WinExec ( Pchar ( Str ), SW_SHOWNORMAL )表示在正常状况下执行该可执行文件。
LPCSTR lpCmdLine: 包含要执行的命令行。
①应用程序启动位置
②当前目录位置
③Windows system目录
④Windows 目录
⑤path中设置的路径列表
UINT uCmdShow: 定义了以怎样的形式启动程序的常数值。具体说明如下:
SW_HIDE 隐藏窗口,活动状态给令一个窗口
SW_MINIMIZE 最小化窗口,活动状态给令一个窗口
SW_RESTORE 用原来的大小和位置显示一个窗口,同时令其进入活动状态
SW_SHOW 用当前的大小和位置显示一个窗口,同时令其进入活动状态
SW_SHOWMAXIMIZED 最大化窗口,并将其激活
SW_SHOWMINIMIZED 最小化窗口,并将其激活
SW_SHOWMINNOACTIVE 最小化一个窗口,同时不改变活动窗口
SW_SHOWNA 用当前的大小和位置显示一个窗口,不改变活动窗口
SW_SHOWNOACTIVATE 用最近的大小和位置显示一个窗口,同时不改变活动窗口
SW_SHOWNORMAL 与SW_RESTORE相同
Delphi.NET 内部实现分析(5)
2.5 其它
Delphi.NET 内部实现分析(6)
不好意思,实在想不到有什么值得说的了,只好草草结束了
在了解了Borland.Delphi.System中的几个重要部分之后,剩下的就是一些零零碎碎的扫尾工作。
2.5.1 类型别名
为兼容Delphi中的特有类型,Borland.Delphi.System单元中定义了很多类型别名。
如我们前面分析过的TObject就是System.Object的别名。
//—————————————–Borland.Delphi.System.pas–
type
TDateTime = type Double;
Extended = type Double; // 80 bit reals are unique to the Intel x86 architecture
Comp = Int64 deprecated;
TGUID = packed record
D1: LongWord;
D2: Word;
D3: Word;
D4: array[0..7] of Byte;
end;
//—————————————–Borland.Delphi.System.pas–
对于Delphi的TDateTime类型来说,它在实现上是以一个Double即8字节浮点数存储的,
兼容OLE自动化中的时间格式。在Delphi.NET中继承了这一存储方式,而没有直接使用BCL
提供的System.DateTime结构,不过仍然可以使用DateTime.FromOADate和
DateTime.ToOADate方法在System.DateTime和TDateTime之间双向转换。
格式存储说明如下(from MSDN)
OLE 自动化日期以浮点数形式实现,其值为距 1899 年 12 月 30 日午夜的天数。
例如,1899 年 12 月 31 日午夜表示为 1.0;1900 年 1 月 1 日早晨 6 点表示为 2.25;
1899 年 12 月 29 日午夜表示为 -1.0;1899 年 12 月 29 日早晨 6 点表示为 -1.25。
只有刻度值大于或等于正的或负的 31241376000000000 的 DateTime 对象才可以表示为
OLE 自动化日期。未初始化的 DateTime(即刻度值为 0 的实例)将转换为等效的未初始化
OLE 自动化日期(即值为 0.0 的日期,它表示 1899 年 12 月 30 日午夜)。
而Extended和Comp则只是一个简单的别名。TGUID也只是一个简单的重定义。
值得注意的是这里的packed关键字。在CLR中,类的成员的物理位置对程序本身是没有意义的,
CLR可以任意安排字段的位置以进行字节对齐等等优化操作。而为了与现有代码进行交互,CLR提供了
StructLayoutAttribute属性允许限定类型的内部物理结构。在Delphi.NET中可以通过packed
关键字定义此结构的成员必须按定义的次序在内存中排列,即LayoutKind.Sequential的形式。
而在Delphi.NET中,所有的record在实现上都是ValueType的子类,即值类型,直接在堆栈上操作。
2.5.2 异常
同TObject一样,Delphi中异常类继承链的根Exception在Delphi.NET中,也只是BCL的异常类
System.Exception的一个别名,而只是通过class helper提供源代码级兼容性。
//—————————————–Borland.Delphi.System.pas–
Exception = System.Exception;
ExceptionHelper = class helper for Exception
private
class function CreateMsg(const Msg: string): Exception;
function GetHelpContext: Integer;
procedure SetHelpContext(AHelpContext: Integer);
public
// Doc: The help context return zero(0) if exception’s helplink property
// cannot be parsed into an integer.
property HelpContext: Integer read GetHelpContext write SetHelpContext;
// constructor Create(const Msg: string) is provided by the CLR class
class function CreateFmt(const Msg: string; const Args: array of const): Exception;
class function CreateHelp(const Msg: string; AHelpContext: Integer): Exception;
class function CreateFmtHelp(const Msg: string; const Args: array of const;
AHelpContext: Integer): Exception;
end;
ExceptionClass = class of Exception;
EConvertError = class(Exception);
threadvar
_ExceptObject: TObject;
function ExceptObject: TObject;
//—————————————–Borland.Delphi.System.pas–
几个类函数Create*(…)都只是对System.Exception构造函数的包装而已,
用于保存异常相关帮助文件路径的System.Exception.HelpLink属性,则被Delphi.NET
用于保存HelpContext,因为在VCL中,所有的异常都是共用一个帮助文件TApplication.HelpFile。
而ExceptObject函数则由编译器支持,提供访问当前被抛出的异常的手段。此函数在Delphi中
通过VCL维护的SEH异常链获取,而在Delphi.NET中只好由编译器在异常被截获后手动赋值给线程局部存储变量
_ExceptObject,然后再由ExceptObject函数读出,这只是语法一级兼容Delphi而已。
不过这个预览版好像没有提供threadvar的支持,只是把它简单的放到Borland.Delphi.System单元的
全局变量中,作为自动生成Unit类的一个静态成员变量而已,并非线程安全!在Borland.Delphi.Classes
中甚至直接把TThread的定义注释掉,实现不提供。估计还在开发中……sigh
2.5.3 断言(Assert)
断言负责在调试模式下检测一个条件是否成立,失败则引发异常。
//—————————————–Borland.Delphi.System.pas–
interface
{ debugging functions }
procedure _Assert(const Message, Filename: AnsiString; LineNumber: Integer);
type
EAssertionFailed = class(Exception)
public
ShortMessage: string;
Filename: string;
LineNumber: Integer;
end;
resourcestring
SAssertionFailed = ’%s (%s at %d)’;
implementation
procedure _Assert(const Message, Filename: AnsiString; LineNumber: Integer);
var
LException: EAssertionFailed;
begin
{ TODO : Should we be using System.Diagnostics.Debug.Assert/Fail? }
{$MESSAGE WARN ’Assert doesn”t use CreateFmt because it returns the wrong type’}
LException := EAssertionFailed.Create(Format(SAssertionFailed, [Message, Filename, LineNumber]));
LException.ShortMessage := Message;
LException.Filename := Filename;
LException.LineNumber := LineNumber;
raise LException;
end;
//—————————————–Borland.Delphi.System.pas–
_Assert函数的定义基本上是EAssertionFailed异常的一个简单包装。因为Delphi没有提供类似
C++中__FILE__、__LINE__之类的预定义宏,故而只能由编译器在用户使用到Assert函数时,
将当前文件名、行号等调试信息编译进代码中,即在编译器一级提供断言实现。
//—————————————–Borland.Delphi.System.pas–
function Assigned(const AGCHandle: GCHandle): boolean;
begin
Result := AGCHandle.IsAllocated;
end;
//—————————————–Borland.Delphi.System.pas–
//—————————————–GCHandle.cs–
namespace System.Runtime.InteropServices {
public struct GCHandle {
private IntPtr m_handle;
public bool IsAllocated { get { return m_handle != IntPtr.Zero; } }
}
}
//—————————————–GCHandle.cs–
Assigned则是对一个引用类型变量进行检测,与Delphi类似,Delphi.NET中直接通过检测引用类型值
是否为空(null)判断是否有效,但对于值类型则将之与0进行比较。
2.5.4 随机数
Delphi.NET中的随机数基本上是对BCL相关类的一个简单包装,而BCL的随机数算法与VCL一样弱智,
简单的功能还凑合,BCL的System.Security.Cryptography.RandomNumberGenerator相比之下
随机性就好得多,不过要付出速度上的代价。
//—————————————–Borland.Delphi.System.pas–
interface
{ random functions }
var
RandSeed: LongInt = 0;
procedure Randomize;
function Random(const ARange: Integer): Integer; overload;
function Random: Extended; overload;
implementation
var
LastRandSeed: Integer = -1;
RandomEngine: System.Random;
procedure InitRandom;
begin
if LastRandSeed <> RandSeed then
begin
if RandSeed = 0 then
RandomEngine := System.Random.Create
else
RandomEngine := System.Random.Create(RandSeed);
LastRandSeed := RandSeed;
end;
end;
procedure Randomize;
begin
LastRandSeed := -1;
RandSeed := 0;
end;
function Random(const ARange: Integer): Integer;
begin
InitRandom;
Result := RandomEngine.Next(ARange);
end;
function Random: Extended;
begin
InitRandom;
Result := RandomEngine.NextDouble;
end;
//—————————————–Borland.Delphi.System.pas—
2.5.5 其它.其它
Borland.Delphi.System单元虽然比Delphi中的System单元小的多,
但其中也充斥着大量常用但是实现代码枯燥的函数。如
数字处理函数集
字符串处理函数集
命令行信息获取函数集(CmdLine/ParamCount/ParamStr)
格式化输出函数集(Format等)
文本文件(即Text类型,而File类型文件不提供支持)开/关/读/写等函数集
动态数组管理(System.Array类型的简单包装)
当前路径及目录操作函数集
集合类型(CLR中并无集合概念,Set实现上是字节数组的简单包装)
其它一些杂项函数
等等等等
这些零散代码基本上都是对BCL相应功能的简单包装,这里就不一一详述了。
2.5.6 小结
至此,对Delphi.NET中核心单元Borland.Delphi.System单元的介绍
就告一段落了。通过对此单元的分析,我们大致了解了Delphi.NET中对于Delphi
一些核心概念的实现或模仿思路,但不排除在正式版中实现有所改变。
题外话:
首先感谢大家的热心支持,这是督促我这个懒人写完文章(哪怕是草草结束)的最大动力,
也希望这篇文章能够对大家了解即将到来的Delphi.NET、迎接.NET时代有所帮助。
这个系列文章到这里估计也就暂时告一段落了,因为时间仓促、准备不足而且
笔者水平有限,只涉及到Delphi.NET在实现上与Delphi不同的部分内容,
与Delphi.NET的改变来说只是冰山一角而已。本来还想扩大一点分析面,
但考虑到Delphi.NET中RTL其它单元大多只是对原有Delphi代码的BCL封装移植
技术难度并不大,对Delphi熟悉的读者直接阅读源程序可能比看我的文章更容易一些。
因此在分析完涉及到一些底层只是的Borland.Delphi.System后就此打住,
虽然有些虎头蛇尾之嫌,但总免得背画蛇添足之骂名 :)
至于构建在Delphi.NET的RLT之上的应用层架构VCL和以后可能要支持的CLX,
我就没有太多精力写文章介绍了。因为就目前实现的VCL代码来看,只是将以前的VCL代码
managed化而已,实现上还是使用Windows那套传统API管理窗口,与BCL的
System.Windows.Forms.Form根本不搭界。这样一来在Delphi.NET中又多了一个选择
VCL or CLX or System.Windows.Forms.Form…sigh,是好是坏只能待时间评判。
文中如果有解释不够清楚的地方,大家可以跟贴提出。也欢迎来信
于我讨论Delphi.NET和CLR相关问题。 再次感谢大家的支持!
Delphi.NET 内部实现分析(4)
2.4 消息
对于类的可重载方法而言,最常见的实现方法是构建一张VTable表,每个方法占一个slot。
但这种处理方法受到空间和时间上的限制,在处理大量方法如众多窗口消息的处理方法时有局限性。
为处理这个矛盾,MFC使用宏定义一套独立于类的消息处理函数表,ATL干脆要求编译器在合适
时候不使用VTable以此来进行优化。而Delphi则通过提供类似于虚拟方法的动态方法
dynamic method来解决这个矛盾。
虚方法virtual method保存在VMT中,生成代码为使用效率进行优化,适合完成普通的类继承
重载方法的实现;动态方法则根据一个编号保存在一张单独的表中,生成代码为程序代码大小优化,
适合实现由祖先类定义的大量可重载但并不经常被使用的方法,最典型的例子就是Windows消息处理函数。
因此我们在定义一个Windows消息处理函数时,后面会跟一个message关键字,指定动态方法编号。
//—————————————–Forms.pas–
type
TScrollingWinControl = class(TWinControl)
begin
procedure WMSize(var Message: TWMSize); message WM_SIZE;
…
//—————————————–Forms.pas–
也可以使用dynamic关键字定义动态方法,由系统自动分配方法编号。
在Delphi中,这张动态方法表的指针保存在VMT的vmtDynamicTable域中,
动态方法被调用时会使用TObject.Dispatch方法调用GetDynaMethod函数
从动态方法表中,根据传入消息结构的第一个字段编号,查找编号符合的动态方法,
如果不存在匹配的编号,则调用TObject.DefaultHandler虚方法处理异常情况。
而在Delphi.NET中,Borland无法再使用VMT用作数据存储,但为了提供源代码级
兼容性,只能设法模拟Delphi中的语义。
好在CLR提供了特性Attribute这种非常好用而且有用的功能,能够将任意的静态或
动态的数据及功能附加到任意的语法元素上。具体原理和使用请参考前面提过的书籍和文章。
Delphi.NET在预览版中,对Attribute的支持还很弱,不过就我测试,在2002-11-14
的测试版本中,已经可以不只局限于Delphi.NET文档中所说只能用于类型了,估计正式版
发布时应该能够提供完整的支持。
Delphi.NET中对动态方法的模拟就是通过MessageMethodAttribute完成的。
当用户使用dynamic定义动态方法,或者使用message定义消息处理函数时,编译器
会自动给此方法添加一个[MessageMethodAttribute(1)]特性,并指定编号。
而TObjectHelper.Dispatch则通过枚举类型所有方法,找出带有MessageMethodAttribute
特性的动态方法,根据MessageMethodAttribute.ID判断应该调用的动态方法。
如果没有找到则还是调用TObject.DefaultHandler方法处理异常。为了提高查找效率,
Borland.Delphi.System单元中还实现了一个缓存,使用Hash表MethodMaps
将搜索过的类与其动态方法列表缓存起来,下次使用直接可以定位获取。
//—————————————–Borland.Delphi.System.pas–
type
MessageMethodAttribute = class(TCustomAttribute)
private
FID: Integer;
public
constructor Create(AID: Integer);
property ID: Integer read FID;
end;
TObjectHelper = class helper for TObject
…
procedure Dispatch(var Message);
end;
//—————————————–Borland.Delphi.System.pas–
MessageMethodAttribute特性定义供内部使用,在dyanic或message关键字
被使用时将自动附加到相应的动态方法上。如
procedure SayHello(var Message: TMessage); dynamic;
procedure SayHello(var Message: TMessage); message 1;
[MessageMethodAttribute(1)]
procedure SayHello(var Message: TMessage);
这几种写法基本上是等价的,只不过dynamic关键字不指定动态方法编号。
//—————————————–Borland.Delphi.System.pas–
function GetMessageID(Obj: TObject): Integer;
var
Field: FieldInfo;
begin
Field := Obj.GetType.GetFields[0];
Result := Integer(Field.GetValue(Obj));
end;
//—————————————–Borland.Delphi.System.pas–
对TObject.Dispatch而言,参数Message可变,但第一个字段必须是一个Integer,
指明此Message需要被Dispatch到哪个编号的动态方法。GetMessageID函数就负责
用Reflection从一个类型中获取消息编号。注意这里并没有做异常情况检测,意味着如果
传递无效的类型进来可能导致异常。
TObjectHelper.Dispatch代码主要是通过在MethodMaps中查表定位动态方法,
如果没有缓存就使用TMethodMap.Create构造类型与动态方法的映射关系,代码略过。
Delphi.NET 内部实现分析(3.4) 由此我们可以看出,Delphi.NET中使用了从内嵌子类到class helper种种方法,
才总算解决了从传统继承模型和内存模型迁移到CLR以及FCL类树的过程,迁移过程不可谓不艰辛。
虽然这种解决方法不能算是完美,但相信Borland也是在综合评估了诸多其它手段之后,
才做出这样的选择,付出了一些代价、如class helper,也取得了不少的成果、源代码级兼容较强。
这种映射模型到底行不行,我想只能有待时间来做评论。
最后我们来看看Delphi的is和as关键字是如何在Delphi.NET中实现的
//—————————————–Borland.Delphi.System.pas–
function _IsClass(Obj:TObject; Cls:TClass): Boolean;
var
t1, t2: System.Type;
begin
if not Assigned(Obj) then
Result := false
else
begin
t1 := Obj.GetType;
t2 := System.Type.GetTypeFromHandle(_TClass(Cls).FInstanceType);
if t1 = t2 then
Result := true
else
Result := t1.IsSubclassOf(t2);
end;
end;
//—————————————–Borland.Delphi.System.pas–
_IsClass函数实现很简单,检测对象有效性后直接通过判断两个类型的继承关系检测。
//—————————————–System.pas–
function _IsClass(Child: TObject; Parent: TClass): Boolean;
begin
Result := (Child <> nil) and Child.InheritsFrom(Parent);
end;
//—————————————–System.pas–
相比之下Delphi的is实现更简单,直接用TObject.InheritsFrom实现。
Delphi.NET之所以不象Delphi那样直接使用TObject.InheritsFrom实现is关键字,
是因为相对于Type.IsSubclassOf方法来说,TObjectHelper.InheritsFrom方法
使用的Type.IsInstanceOfType方法代价较大。
Type.IsSubclassOf方法只是从传入类型开始,一级一级查看其父类是否自己。
//—————————————–Type.cs–
public abstract class Type : MemberInfo, IReflect
{
public virtual bool IsSubclassOf(Type c)
{
Type p = this;
if (p == c)
return false;
while (p != null) {
if (p == c)
return true;
p = p.BaseType;
}
return false;
}
}
//—————————————–Type.cs–
而Type.IsInstanceOfType则要考虑Remoting、COM、接口以及运行时类型等等
诸多复杂因素,因而不适合用在is/as这样频繁使用的关键字实现上。
//—————————————–Borland.Delphi.System.pas–
function _AsClass(Obj:TObject; Cls:TClass): TObject;
begin
Result := Obj;
if not _IsClass(Obj, Cls) then
raise System.FormatException.Create(‘Invalid Cast’);
end;
//—————————————–Borland.Delphi.System.pas–
as操作符的实现,只是简单的赋值加检测而已,因为CLR是单根结构,所以转换总是成功的,
只需在转换后用is操作符检测,抛出异常情况就行。
//—————————————–System.pas–
function _AsClass(Child: TObject; Parent: TClass): TObject;
{$IFDEF PUREPASCAL}
begin
Result := Child;
if not (Child is Parent) then
Error(reInvalidCast); // loses return address
end;
//—————————————–System.pas–
可以看到Delphi中的实现也是非常类似的。
最后一个相关函数是_ClassCreate,用于实现类型的创建与构造。
//—————————————–Borland.Delphi.System.pas–
function _ClassCreate(Cls: TClass; Params: Array of TObject): TObject;
begin
Result := System.Activator.CreateInstance(Cls.SystemType, Params);
end;
//—————————————–Borland.Delphi.System.pas–
与Delphi的System.pas中冗长的_ClassCreate函数实现相比,Delphi.NET无需关心
类的内存获取、构造异常的截获以及Self指针的修正等等,只是简洁的通过System.Activator类
完成所需功能,这就是底层有一个强大完善类库支持的优势所在。
至此,Borland.Delphi.System单元中关于元类、类与对象的相关定义及实现就基本上分析完了,
虽然只有寥寥百来行代码,但它为Delphi在CLR上的映射打下了坚实的基础。
下一节我们将进一步看看Delphi中消息与方法的映射关系是如何在Delphi.NET中模拟的。