End Sub
是不是很简单啊。先看第一句,前三者由于是要返回数据的,所以这里没有用ByVal关键字。而第四个参数,对应我们的树形结构而言,这个就是遍历过程中途经的节点,那么它是需要被压入到递归的栈里去的,所以需要使用ByVal关键字来在内存中驻留备份,否则在二次调用后返回执行下一个内容时就会产生错误。
由于我们在之前构造树形结构时,末端节点和中间节点有个区别,就是末端节点的Item项是一个值而不是如中间节点一样是一个对象,所以上面这段代码使用IsObject函数来判断是否是末端节点。后面的代码就比较好理解了,如果不是末端节点,那么对于每个节点再次调用函数自身,如果是则将相应的信息保存到外部变量里(即前三个参数)并退出该次调用。可以通过F8逐句执行,来看看这段代码是如何进行的。
本小节复习了如何沿已知路径由根部到达某节点,了解了如何通过递归的方法来遍历某初始节点下的所有节点。
应该加一句,对于此节的例子,希望还没有完全了解代码是如何工作的朋友自己动手调整一下代码。调整的方法如下,调整后分别看看产生了什么变化,以下三个方法相互独立:
1、注释掉本节第一段代码中的后三项里的一项,看看产生什么效果 2、更改一下后三项的赋值顺序
3、在赋值最后或中间增加一句 dicLayers(PR_NAME_PROFESSIONAL) = PR_TITLE_PRODSG ,并对应的定义常数 PR_NAME_PROFESSIONAL = \,然后命名 C3 格名称为 PROF_DESGN ,最后手动执行一下 ParseData 过程更新内存。 <未完待续>
三之三、送你把漂亮的解牛小刀
说实在的,写到这里发现夹带里的代码已经基本用完了,不得不一边写代码一边调试,同时再来更新了(怎么感觉像是起点网的小说写手的话呀)。
好吧,有了前面两个小节的基础,我们其实可以写一个对于前面列出的几个案例通用的代码啦。如果你观察前面数个案例,你会发现它们都有如下的共同点:1、具有表头(废话嘛,没表头谁会知道数据是啥,傻子才不放表头呢);2、数据是记录形式的,相对完整的,不存在空列、空行的情况(这也是通常我们组织原始数据的方式);3、列的内容之间存在归属关系,即可以整理成树形结构。
============================
昨天半夜写的这个类,本希望能写一个通用的建立树形结构的类的,今天想着用它把前面的几个案例改写一遍,可发现:1、初始版本的末端节点代码错了(这个改正了);2、调试半天,发现比不用它,也没方便到哪去(也可能是由于我改代码的水平比较差吧)。自己感觉这把小刀既不漂亮还挺钝的,牛估计是不太好解,杀鸡又有些浪费了
所以,决定就不再详细讨论了,不过还是把改好的1.1版放在这里,有兴趣的同学参考一下吧(那个1.0版的我删了) 三之四、上帝的归上帝,凯撒的归凯撒
好了,在我沮丧的决定放弃那把小刀后,让我用这个小节结束多层字典动态构造树形结构的讨论吧。虽然在这个小节中,我实际想聊得和字典对象的关系不大,但既然我把题目写成经验谈,那我想还是聊聊吧。
可能标题会让人不知所云,但如果我说将界面设定从代码中剥离出来并使其数据化,估计就很好理解了。让我们还是以那个保险公司的案例来看吧,我们对它已经作过两次演化了,如果你有看我前面写的内容并注意看了代码的变化,那么相信你对它的功能和特点有了较全面的了解。这一小节中,我们将继续改进它。
附件是再次修改了的代码,我增加了一个工作表,将所有Summary表中有关输出内容、输出的位置等等信息全放在了这个新的表内,并将之命名为Definition_Summary,故名思义它的功能就是用来定义Summary表的(还是让我们称之为报表吧)。这样一来,代码头部的常数部分就仅仅剩下三个了: 复制内容到剪贴板 代码:
Private Const PR_DATA_SHT_NM = \
Private Const PR_DEFINE_SHT_NM = \
Private Const PR_LOB_ALL = \
由于我们能够用代码动态的构建供索引筛选用的树形对象,那么用来定义哪些列是分层列的数组就可以从代码内剥离出来,放 在这个定义工作表中。我们来看看这个工作表的内容吧,如下图所示:
16
110526-Definition_Sheet.png (31.74 KB) 2011-5-27 02:12
纵向的分为了4段,分别是:1、菜单项定义;2、筛选内容计算结果部分;3、二次筛选结果部分;4、一个并列菜单内容。这四部分包含了几乎所有报表中需要动态显示的内容。于是原本报表中设置的名称定义就可以全部删掉了。
这里我就不再把代码贴出来详细讲如何实现的了,相信有了前文的基础,只需要看看处理定义表的部分就可以了。另外,想道个歉,附件的代码应该把读取报表定义数据的部分和其他的分开来,单独写一个过程的,而我偷懒了,全混在了一起。
还是来谈谈为什么要这么做吧,显而易见的是由于现在的代码的适用性更强了,那么如果报表的内容需要变化的时候(很不幸的是在现实生活中,报表的形式和内容都是会经常变化的,尤其当你碰到个双子座的老板的时候),我们肯定不希望去频繁的更改代码来实现。如果,象附件这样的做法,那么需要做的仅仅是在定义表内增加几行内容而已(作为例子,我在C19格显示了另一列的筛选结果平均值)。
接下来,让我们来设想一下,如果我们设计一个对话框,其中有4个Page,用来分别显示这个定义表的4栏不同的内容,同时这个对话框提供了诸如编辑、添加、删除等等功能,用以编辑这个定义表的内容。然后,将这个定义表隐藏起来,再在报表内放个按钮用来调用这个对话框。这样一来,我们实际上为这个报表提供了一个编辑保存属性值的功能。如此一来,这个动态报表就会像个独立软件一样工作了,而作为设计人员的你也不再需要为了增加一个显示内容而忙活了。
这让我想到前一阵看到有个帖子,说什么有1000个工作表的工作簿打开很慢,而又看到有人要合并1000个工作表,让我很是惊讶了一下。1000个表啊!那得是多大的数据量啊!拜读了帖子后,发现是将1000个合同文本之类的数据合并成记录形式的数据表,为什么要合并,因为那实际上是1000个报表!这这这简直是本末倒置!如果你的合同文本格式都一样,那你需要几个表?对了,2个!一个报表,一个数据表,每次填完了,按个按钮把数据填到数据表里,需要查哪个的时候再按个按钮调出来。如果你的合同文本有100个格式,那你需要几个表?嗯,如果你看了这篇文章还说101个的话就太对不起我了,几个?对了,仨!一个报表,一个数据表,还有一个报表定义表。
好了,第三节结束了,接下来的一节是我在开始写这篇文章时才想到的内容,所以目前一行代码也无,呵呵。 <未完待续>
四之一、利用字典动态的构建自定义数据类型
想必大家都很熟悉VB的一个基本功能,就是自定义数据类型,Type ... End Type。利用这个功能,我们可以将描述一个事物的各种属性都做好预先定义,从而使得程序代码更为接近自然语言。同时,在自定义数据类型内部还可以嵌套的声明某属性为另一个自定义数据类型,这样一来就可以描述更为复杂的情况。
但使用自定义数据类型有两个缺陷,其一就是它对于程序代码而言是静态的,也就是说必需要预先定义好才能使用,换而言之需要事先了解其结构。而我们在处理数据时,由于这样那样的原因,这个结构的信息会是变化的,这样一来采用自定义数据类型的办法就不适用了。
而字典对象由于是完全的代码操作,恰恰能胜任这个工作。按自定义数据类型的思路,我们可以将属性名称作为字典对象的索引关键字,而将值赋给这个字典对象元素。细心的朋友可能已经注意到,我们在前文的二之一小节中已经使用了这个方法,我还为这个案例的数据结构画了个树形图。在包号下,除了下一级节点外,还有两个关键字“CHIP”和“MASK”,是用代码直接生成的。
再以之前我们非常熟悉的那个保险公司的例子而言,由于该案例是要在筛选数据后,利用某一特定列的值的特征进行二次筛选,所以我们对末端节点的字典对象的各元素采用了行号为索引关键字,并赋值为该特定列的值。其实,我们完全可以这样来做,同样的采用行号作为末端节点各元素的索引,但将一个用字典对象创建的自定义数据类型赋值给各对应元素。这个字典对象的元素是各非树形结构层的列,其索引是列名即表头,值是对应的该行的值。这样一来,这个树结构就包含了全部源数据信息,而原本的那个全局变量源数据数组就不再需要了。希望这段文字的描述足够准确。
17
接下来,让我们回头再看看二之一小节中构造的这个树,为方便阅读,我把它复制过来: 复制内容到剪贴板 代码: dic (变量名) │
└─托盘型号 │ └─包号 │
├─CHIP -> CHIP 值 │
├─MASK -> MASK 值 │ └─托盘号 │
└─序列号
在这个树里,我们使用各层的编号进行区分索引。再看看我们引用这个树的数据时,即填表的时候,是按包为单位进行填写的。很显然,该包的包号是需要填写到标签里的,但这个包号信息是作为索引值放在树里,当我们将包节点整体传递到子过程的时候,由于该包节点的字典对象内没有这一信息,所以必须将包号作为一个单独的参数传递过去:FillLabelOne(iLabelNo, iPackNo, dicPack, dtPack As Date)。如果我们换个角度来理解这个事情,这意味着每个节点元素不知道自己叫什么。同样的,在我们之前构造的所有树中,任何一个节点都缺乏该节点相对树的信息,如:所在层的层名称、相对于根的路径信息等等。
如果引入了自定义数据类型的概念,那么我们在构造树的时候,就可以将这些信息全都放进来。也就是说对于任何一个节点而言,它都会是一个两层字典,第一层字典是一个自定义数据类型的概念,其包含的可能关键字是希望保留的节点信息,比如 Name(也就是该节点的索引值)、LayerName、LayerCount、甚至 PathArray,当然还有 Data,这个Data 就是第二层字典,也就是原本之前那个简单树的节点字典对象。如果觉得使用上面这些关键字的时候很麻烦,需要敲双引号、区分大小写,那完全可以设置一些常量并赋值为整数,比如 Const NODE_NAME = 1, Const NODE_LAYER_NAME = 2, Const NODE_DATA = 0 等等,现在再写代码的时候是不是会方便很多了。
昨天在网上找到一篇硕士毕业论文,利用VBA在Excel中建立决策分析系统,有兴趣的朋友可以翻翻。她是在EXCEL里画树形图,然后根据数据进行分析。很明显她不了解字典对象的这些应用,导致用了大量的精力用数组来模拟树形结构。如此看来,作为一个与IT混不搭界的我也可以拿个计算机专业的硕士学位啦。
在我们开始下一个小节之前,让我们先来看看下面这段代码: 复制内容到剪贴板 代码: Sub Test()
Dim dTemp, dic
Set dTemp = CreateObject(\ dTemp(1) = \ Set dic = dTemp Debug.Print dic(1) dTemp(1) = \ Debug.Print dic(1) Set dTemp = Nothing Debug.Print dic Is Nothing Debug.Print dic(1) dic.RemoveAll Set dic = Nothing End Sub <未完待续>
18
四之二、利用字典对象动态的构造复杂的数据结构
上小节结束我写了个简单的代码,这段代码的执行输出结果反映了什么呢? 复制内容到剪贴板 代码: Sub Test()
Dim dTemp, dic
Set dTemp = CreateObject(\ dTemp(1) = \ Set dic = dTemp Debug.Print dic(1) dTemp(1) = \ Debug.Print dic(1) Set dTemp = Nothing Debug.Print dic Is Nothing Debug.Print dic(1) dic.RemoveAll Set dic = Nothing End Sub
先看看它表示的含义吧,代码的前两句创建了一个新的字典对象实例 dTemp,然后建立了一个元素索引为 1 值为一个字符串。然后用Set语句将这个对象赋值给了另一个变量 dic,并输出 新变量 dic 的索引为 1 的元素值。好吧到此无甚特别的。接下来,将先前的那个变量 dTemp 的 1 索引元素赋值为另一个字符串,再次 输出 新变量 dic 这个元素,我们发现 它 同步变化了。接下来,我们把初始变量 dTemp 设为 Nothing,据坛子里的很多帖子称是为了释放内存。安全起见代码先输出 新变量 dic 是否是 也同步变为 Nothing,可居然不是?!那么 再再次 输出 dic 的这个元素值,它居然还在?!
这段代码很显然的反映了至少两个事实:1、与我们通常的认识不同,Set ... = Nothing 不能释放内存!2、Set 变量1 = 变量2,并不是创建了变量2的副本,而是建立了一个类似于指针式的链接。关于是否释放内存一事,不妨执行下面的这段代码,然后观察 Stop 前后任务管理器中 EXCEL.EXE 所占内存的变化。 复制内容到剪贴板 代码:
Sub TestMemory() Dim dic, i& Stop
Set dic = CreateObject(\ Stop
For i = 1 to 10000 dic(i) = i Next Stop
Set dic = Nothing Stop End Sub
所以我们要切记一点,如果你确实需要释放字典对象占用的内存的话,记得使用RemoveAll方法,然后再将变量设为Nothing!如果是多层字典,而你又很希望彻底的释放内存,那就需要遍历所有节点,由末端至根部逐级RemoveAll啦。
而对于第2点,如果仅仅是建立了一个链接而非实例的副本,那么势必不会占用很多内存。如果成立的话,结合上一小节的内容,是不是可以将某一节点的字典中的元素指向另一个节点呢?让我们用下面的代码做个测试,并观察一下内存变化吧。 复制内容到剪贴板 代码:
Private Const NODE_NEXT = -1 Private Const NODE_PREVIOUS = 1
19
Private Const NODE_VALUE = 0 Sub BuildChain()
Dim dChain, i&, dTemp
Set dChain = CreateObject(\ For i = 1 to 1000
Set dChain(i) = CreateObject(\ dChain(i)(NODE_VALUE) = i Next Stop
For i = 1 to 1000
If i > 1 Then Set dChain(i)(NODE_PREVIOUS) = dChain(i - 1) If i < 1000 Then Set dChain(i)(NODE_NEXT) = dChain(i + 1) Next
dChain(1)(NODE_PREVIOUS) = FALSE dChain(1000)(NODE_NEXT) = FALSE Stop
Set dTemp = dChain(1) i = 0
Do While IsObject(dTemp(NODE_NEXT)) i = i + dTemp(NODE_VALUE) Set dTemp = dTemp(NODE_NEXT) Loop Debug.Print i For i = 1 to 1000 dChain(i).RemoveAll Set dChain(i) = Nothing Next Stop
dChain.RemoveAll Stop
Set dChain = Nothing End Sub
首先,我在这段代码中设置了数个断点,以便能观察内存的变化,在第一个循环建立了1000个字典实例后,和第二个循环创建链接后,内存没有明显的变化。这也验证了刚才我们的猜想。那么这段代码有什么意义吗?它创建了某种数据结构,这样的一个数据结构被称为双向链表(Double Linked List),在每一个节点中都包含了两个链接,分别指向前后两个节点。而随后由第一个节点开始,遍历了全部节点,并对所有节点的值进行了累加。昨天在网上查到这样一篇博文:VBA实现数据结构中的双向链表类,文中用两个类实现这样的链表,一个是链表的节点类,一个是操作函数类。注意,那个节点类表中,对于前后节点的链接是声明了节点类,也就是说在一个类中申明变量为自身。这在自定义数据类型中也是可以的,但问题是自定义数据类型是直接用 = 号赋值的,也就是说创建的是一个副本。而我们可以用字典对象 动态的 创建双向链表!而对于这样的一个链表的诸如插入、删除节点的程序难道不是很简单的吗,如果不需要取得节点在链中的顺序位置的话,我们仅仅使用简单的Set语句和字典对象的Remove方法就能实现。我们甚至可以在每个节点中加入 首节点 和 末节点 的链接,或则加入隔一个节点的链接,呵呵。
哈,看看我们发现了什么?动态的构建各种数据结构!为什么是各种?因为我们知道所谓的数据结构实际上是指数据间的相互依存关系,无非就是链接、先后顺序等等,字典对象全都可以实现。为什么我总是强调 动态的?因为通常情况下,数据结构都需要在代码内事先设置好相关的诸如自定义数据类型、类等工作。而字典由于在构造数据结构中,完全是在代码内部动态的完成的,这样一来如果数据本身能够描述清楚结构关系的话,我们就可以利用这一特点动态构建,并使得在更大意义上的通用代码成为可能。聊到这,我不得不说Collection对象同样可以胜任这个任务,并且在某种程度上而言可能比字典更合适。
现在让我们回过头来看看上一小节建立的稍微复杂点的树结构,我们在每个节点设置了该节点的一些信息,现在我们甚至可以在每个节点中建立前后兄弟节点、父节点的链接,或则如果你愿意的话,还可以建立一个根节点的链接!这就好比我们如果问一个
20