跳转到内容

Visual Basic/有效编程

来自维基教科书,开放书籍,构建开放世界

在任何计算机语言中有效编程时,无论是 VB 还是 C++,你的风格都应该保持一致,应该有条理,并且应该尽可能地提高执行速度和资源利用率(如内存或网络流量)。使用成熟的编程技术,可以将错误降到最低并更容易识别,这将使程序员的工作更轻松,更愉快。

编写可靠程序有很多不同的方面。对于一个由作者交互式使用且仅由作者使用的简短程序,为了快速获得答案,打破所有规则是合理的。但是,如果该小程序发展成大型程序,你最终会希望自己一开始就走上了正确的道路。每种编程语言都有自己的优缺点,一种在一种语言中帮助编写良好程序的技术在另一种语言中可能是没有必要的、不可能的或适得其反的;这里介绍的内容特别适用于 VB6,但其中大部分是标准内容,适用于或在 Pascal、C、Java 和其他类似的命令式语言中强制执行。

一般指南

[edit | edit source]

这些建议将在下面更详细地描述。这些都不能称为规则,有些是有争议的,你必须根据成本和效益自己做出决定。

  • 编写注释以解释你为什么要这样做。如果代码或正在解决的问题特别复杂,你应该也解释为什么选择你选择的这种方法而不是其他更明显的方法。
  • 缩进你的代码。这可以使其他人更容易阅读代码,并可以轻松地发现语句是否未正确关闭。这在多重嵌套语句中尤其重要。
  • 声明所有变量,通过在每个代码模块的顶部放置Option Explicit来强制执行此操作。
  • 使用有意义的变量和子例程名称。变量 FileHandle 对我们人类来说比 X 更有意义。还要避免缩写名称的趋势,因为这也会使代码难以阅读。不要使用 FilHan,而 FileHandle 更清晰。
  • 在函数和子例程的参数列表中,将所有参数声明为ByRef。这会强制编译器检查你传入的变量的数据类型。
  • 在尽可能小的范围内声明变量、子例程和函数:优先使用 Private 而不是 Friend,Friend 而不是 Public。
  • 在 .bas 模块中,尽可能少地声明为 Public 的变量;这样的变量对整个组件或程序是公开的。
  • 将相关的函数和子例程分组到一个模块中,为不相关的例程创建一个新模块。
  • 如果一组变量和过程密切相关,请考虑创建一个类来封装它们。
  • 在代码中包含断言以确保例程被提供正确的数据并返回正确的数据。
  • 编写和执行测试
  • 先让程序工作,然后让程序快速工作。
  • 如果一个变量可以保存有限范围的离散值,这些值在编译时是已知的,请使用枚举类型
  • 将大型程序分解成单独的组件(DLL 或类库),以便你仅对需要使用它们的代码部分降低数据和例程的可见性。
  • 使用简单的前缀符号来显示变量的类型和例程的作用域。

声明变量

[edit | edit source]

在本书的前面,你可能被教导使用简单的 Dim 语句声明变量,或者根本不声明。在不同级别声明变量是一项关键技能。将你的程序视为三个分支:模块(对所有窗体开放)、单个窗体和子程序本身。如果在模块中声明一个变量,该变量将在所有窗体中保留其值。Dim 语句将起作用,但按照惯例使用“Public”代替。例如

Public X as Integer

在窗体代码的顶部声明某些内容将使其对该窗体私有,因此,如果在一个窗体中 X=10,而在另一个窗体中 X=20,它们不会相互干扰。如果变量被声明为 public,那么它们之间会相互影响。要在一个窗体中声明某些内容,按照惯例使用“Private”。

Private X as Integer

最后是子程序。仅对子程序设置变量的维度非常有效,这样你就可以在所有子程序中使用默认变量(例如 sum 用于求和),而无需担心某个值因为代码的其他部分而改变。但是,有一个转折。Dim,你习惯的,在子程序完成后不会保留变量的值。因此,在重新运行子程序后,子程序中的所有局部变量都将被重置。要解决此问题,可以使用“Static”。

Static X as Integer

你们中的一些人可能想走捷径,对所有东西都使用 Public。但是,最好在尽可能小的级别上声明。通常,参数是将变量从一个子程序发送到另一个子程序的最佳方法。这是因为参数使跟踪变量在哪里被更改(以防出现逻辑错误)变得容易得多,并且几乎限制了错误代码部分可以造成的损害。声明变量在使用默认变量时同样有用。常见的默认变量是

 I for loops
 J for loops
 Sum(self explanatory)
 X for anything

因此,与其创建变量 I 和 II 或 Sum1、Sum2,你可以看到为什么将变量保持本地是一个有用的技能。

注释

[edit | edit source]

我认识的每个程序员都不喜欢写注释。我不是说那些只会写一些脑残的 Visual Basic Script 来删除可怜的祖母电脑中所有文件的文盲脚本小子,我说的是他们以及所有那些拥有多个博士学位和荣誉博士学位的人。

所以,如果你难以说服自己注释是一个好主意,那么你并不孤单。

不幸的是,这不是你我与他们一样好的情况,而是他们与我们一样糟糕的情况。良好的注释对程序的持久性至关重要;如果维护程序员无法理解你的代码应该如何工作,他可能不得不重写它。如果他这样做,他将不得不编写更多注释和更多测试。他几乎肯定会引入更多错误,而这将是你的错,因为你没有礼貌地解释你的程序为什么按这种方式编写。维护程序员通常不属于原始团队,因此他们没有共同的背景来帮助他们理解,他们只有错误报告、代码和截止日期。如果你不写注释,你可以肯定没有人会在以后添加注释。

注释需要与代码本身一样仔细和认真地编写。仅仅重复代码内容的随意编写的注释是在浪费时间,最好什么也不说。对于与代码相矛盾的注释也是如此;读者应该相信什么,代码还是注释。如果注释与代码相矛盾,有人可能会“修复”代码,结果却发现实际上是注释出了问题。

以下是一些示例注释

Dim cbMenuCommandBar As Office.CommandBarButton 'command bar object

这来自我自己的一个程序,我通过对别人提供的模板进行黑客攻击而创建了它。他为什么添加注释我不得而知,它对代码没有任何帮助,代码中本来就两次出现了 CommandBar 这个词!

这是来自类似程序的另一个例子

Public WithEvents MenuHandler As CommandBarEvents 'command bar event handler

同样程度的无意义。这两个例子都来自我每天使用的程序。

来自同一程序的另一个例子,它展示了一个好的注释和一个毫无意义的注释

  Public Sub Remove(vntIndexKey As Variant)
    'used when removing an element from the collection
    'vntIndexKey contains either the Index or Key, which is why
    'it is declared as a Variant
    'Syntax: x.Remove(xyz)
  
    mCol.Remove vntIndexKey
  End Sub

第一行注释只是重复了名称中包含的信息,最后一行注释只告诉了我们从声明中可以轻松推断出来的事情。中间两行说了一些有用的东西,它们解释了Variant数据类型令人费解的使用。我的建议是删除第一行和最后一行注释,不是因为它们不正确,而是因为它们毫无意义,并且会使真正重要的注释难以看到。

总结:好的注释解释为什么而不是什么。它们告诉读者代码不能告诉他们的东西。

你可以通过阅读代码来了解正在发生的事情,但通常很难或不可能知道为什么代码这样编写。

解释代码的注释确实有其位置。如果算法很复杂或很精巧,你可能需要用简单的语言对其进行概述。例如,实现方程求解器的例程需要附带对所用数学方法的描述,并附带教科书的参考文献。每一行代码都可能非常清楚,但总体计划可能仍然不清楚;用简单的语言进行概述可以使其变得清晰。

如果你使用了你知道很少使用的 VB 功能,你可能需要指出它为什么有效,以防止善意的维护程序员将其清理掉。

如果你的代码正在解决一个复杂的问题或为了速度而进行了高度优化,它将比其他代码需要更多更好的注释,但即使是简单的代码也需要注释来解释它存在的原因并概述它的功能。通常情况下,在文件开头放置一个叙述性内容,而不是在单个代码行上放置注释,会更好。这样,读者就可以阅读摘要,而不是代码。

摘要

[edit | edit source]
  • 注释应该增加清晰度和意义,
  • 保持注释简短,除非代码的复杂性需要叙述性的描述,
  • 在每个文件的开头添加注释,以解释它存在的原因以及如何使用它,
  • 对每个函数、子程序和属性进行注释,以解释任何奇怪之处,例如 Object 或 Variant 数据类型的使用。
  • 如果一个函数有副作用,解释它们是什么,
  • 如果例程只适用于一定范围的输入,那么就说明,并指出如果调用者提供了一些意外的内容会发生什么。

练习

[edit | edit source]
  • 拿一段别人写的代码,尝试在不阅读注释的情况下理解它是如何工作的。
  • 尝试找到一些不需要任何注释的代码。解释为什么它不需要注释。这样的代码存在吗?
  • 在网络、你自己的代码或同事的代码中搜索好的注释的示例。
  • 把自己放在一个维护程序员的位置,他被叫来修复你自己的代码中一个难以处理的错误。添加注释或改写现有的注释,使工作更容易。

避免防御性编程,而是快速失败

[edit | edit source]

防御性编程指的是编写代码来尝试弥补数据中的一些错误,编写代码来假设调用者可能会提供不符合调用者和子程序之间合同的数据,并且子程序必须以某种方式处理它。

通常会看到这样的属性

  Public Property Let MaxSlots(RHS as Long)
    mlMaxSlots = RHS
  End Property 
  
  Public Property Get MaxSlots() as Long
    if mlMaxSlots = 0 then
      mlMaxSlots = 10
    End If
    MaxSlots = mlMaxSlots
  End Property

这样编写的理由可能是,编写该属性所在的类的程序员担心使用该属性的代码的作者会忘记正确初始化该对象。因此,他或她提供了一个默认值。

问题是,MaxSlots 属性的程序员无法知道客户端代码需要多少插槽。如果客户端代码没有设置 MaxSlots,它可能会失败,或者更糟的是,行为异常。最好像这样编写代码

  Public Property Let MaxSlots(RHS as Long)
    mlMaxSlots = RHS
  End Property 
  
  Public Property Get MaxSlots() as Long
    if mlMaxSlots = 0 then
      Err.Raise ErrorUninitialized, "MaxSlots.Get", "Property not initialized"
    End If
    MaxSlots = mlMaxSlots
  End Property

现在,当客户端代码在调用 MaxSlots Let 之前调用 MaxSlots Get 时,会引发一个错误。现在,解决问题或传递错误的责任就落到了客户端代码的肩上。无论如何,错误会比我们提供默认值时更早被发现。

另一种看待防御性编程快速失败之间区别的方式是将快速失败视为严格的合同实施,而将防御性编程视为宽容。是否宽容始终是一个好主意,这在人类社会中经常会争论不休,但在计算机程序中,它仅仅意味着你不信任程序的所有部分都遵守程序的规范。在这种情况下,你需要修复规范,而不是宽恕违反规范的行为。

练习

[edit | edit source]
  • 取一个正在运行的非平凡程序,并搜索代码中的防御性编程,
  • 改写代码,使其快速失败,
  • 再次运行程序,看看它是否失败,
  • 修复程序的客户端部分,以消除错误。

参考文献

[edit | edit source]

有关此主题的简明文章,请参阅 https://martinfowler.com.cn/ieeeSoftware/failFast.pdf

断言和契约设计

[edit | edit source]

断言是一个断言某些事情为真的语句。在 VB6 中,你可以像这样添加断言

Debug.Assert 0 < x

如果语句为真,程序将继续运行,就好像什么也没发生一样,但如果不是,程序将在该行停止。不幸的是,VB6 有一个特别弱的断言形式,它们只在代码在调试器中运行时执行。这意味着它们在已编译的程序中没有任何效果。不要让这一点阻止你使用它们,毕竟,这在许多常见的编程语言中根本不存在。

如果你真的需要在已编译的程序中测试断言,你可以这样做

  If Not(0 < x) Then
    Debug.Assert False
    Err.Raise ErrorCodes.AssertionFailed
  End If

现在,程序将在 IDE 中运行时在失败处停止,并在编译后运行时引发错误。如果你计划在多个地方使用这种技术,声明一个子程序来执行它将是明智之举,以减少混乱

  Public Sub Assert(IsTrue as Boolean)
    If Not IsTrue Then
      Debug.Assert False
      Err.Raise ErrorCodes.AssertionFailed
    End If
  End Sub

然后,你就可以写Debug.Assert,而不是写

Assert 0 < x

断言可用于实现一种形式的契约设计。在每个例程的开头添加断言,断言有关例程参数的值以及有关任何相关模块或全局变量的值的一些内容。例如,一个接受一个必须大于零的单个整数参数的例程将具有与上面所示形式相同的断言。如果它被调用时带有零参数,程序将在带有断言的行上停止。你也可以在例程的退出处添加断言,指定返回值或任何副作用的允许值。

断言与显式验证的不同之处在于,它们不会引发错误,也不会允许程序在断言失败时采取措施。这并不一定是断言概念的弱点,它是断言和验证检查使用方式的不同之处。

断言用于执行以下几件事

  • 它们指定了调用代码和被调用代码必须遵守的契约,
  • 它们通过在已知有错误的最早时间点停止执行来帮助调试。正确编写的断言会在错误导致程序崩溃之前很久就捕获它们。

断言通常有助于在程序开发过程中发现逻辑错误,而验证通常用于捕获来自人类或其他不可靠外部来源的不良输入。程序通常编写为,输入验证失败不会导致程序失败,而是将验证错误报告给更高的权限,并采取纠正措施。如果断言失败,通常意味着程序的两个内部部分未能就双方都应该遵守的契约条款达成一致。如果调用例程发送了一个负数,而预期的是一个正数,并且该数不是由用户提供的,那么无论进行多少验证,程序都无法恢复,因此引发错误毫无意义。在 C 等语言中,断言失败会导致程序停止并输出一个堆栈跟踪,但 VB 在 IDE 中运行时只会停止。在 VB 中,断言在已编译的代码中没有效果。

与全面的测试相结合,断言是编写正确程序的绝佳辅助工具。断言也可以替代某些类型的注释。描述允许的参数值范围的注释最好写成断言,因为它们是程序中实际检查的显式语句。在 VB 中,你必须在 IDE 中运行程序才能获得断言的好处,这是一个小麻烦。

断言还有助于确保程序在添加新代码和修复错误时保持正确。假设一个子程序计算由于对流导致的散热器的等效电导(如果物理知识不熟悉,不要担心)

  Public Function ConvectionConductance(Byval T1 as Double, Byval T2 as Double) as Double
    ConvectionConductance = 100 * Area * (T2 - T1)^0.25
  End Sub

现在,如果你知道物理知识,电导率总是负数,无论温度 T1 和 T2 之间的关系如何。但是,此函数假设 T1 始终大于或等于 T2。这种假设对于正在讨论的程序来说可能是完全合理的,但它仍然是一个限制,因此应该将其纳入此例程与其调用者之间的契约中

  Public Function ConvectionConductance(Byval T1 as Double, Byval T2 as Double) as Double
    Debug.Assert T2 < T1
    ConvectionConductance = 100 * Area * (T2 - T1)^0.25
  End Sub

对例程结果进行断言也是一个好主意。

  Public Function ConvectionConductance(Byval T1 as Double, Byval T2 as Double) as Double
    Debug.Assert T2 <= T1
    ConvectionConductance = 100 * Area * (T2 - T1)^0.25
    Debug.Assert 0 <= ConvectionConductance
  End Sub

在这种特定情况下,断言结果似乎毫无价值,因为如果前提条件满足,那么通过检查就可以明显地发现后置条件也必须满足。在现实生活中,前提条件后置条件断言之间的代码通常要复杂得多,并且可能包含对不受创建函数的人员控制的函数的许多调用。在这种情况下,即使后置条件看起来像是在浪费时间,也应该对其进行指定,因为它可以防止引入错误,并向其他程序员提供函数应遵守的约定。

测试

[edit | edit source]

测试范围从编写程序,然后运行并随意查看其行为,到首先编写全套自动化测试,然后编写程序以使其符合要求。

我们大多数人都在两者之间工作,通常更接近第一个选择而不是第二个选择。测试通常被认为是额外的成本,但就像对物理产品的质量控制系统一样,所谓的质量成本通常是负面的,因为产品的质量提高了。

你可以使用测试来定义函数或程序的规范,方法是首先编写测试,这是极限编程方法的实践之一。然后,逐个编写程序,直到所有测试都通过。对于大多数人来说,这似乎是完美无瑕的建议,并且完全不切实际,但其中一定程度的内容将通过帮助确保组件在集成之前正常工作而带来丰厚的回报。

测试通常被构建为一个单独的程序,该程序使用与可交付程序相同的源代码。只需编写另一个可以使用可交付程序组件的程序,通常就足以暴露设计中的弱点。

要测试的组件越小,编写测试就越容易,但是如果你测试非常小的部分,你可能会浪费大量时间编写对可以通过肉眼轻松检查的事物进行测试。自动化测试最适合那些足够小以至于可以从真实程序中提取出来而不会造成干扰,但又足够大以至于具有一些复杂行为的程序部分。很难做到精确,最好做一些测试而不是不做任何测试,经验会告诉你哪些地方最需要在你的特定程序中付出努力。

你也可以将测试作为程序本身的一部分。例如,每个类都可以有一个测试方法,该方法在测试通过时返回 true,否则返回 false。这样做的好处是,每次编译真实程序时,你也会编译测试,因此任何会导致测试失败的程序接口更改都可能会及早被捕获。因为测试位于程序内部,所以它们还可以测试外部测试例程无法访问的部分。

范围、可见性和命名空间

[edit | edit source]

匈牙利命名法

[edit | edit source]

匈牙利命名法是许多程序员用来表示范围和类型的变量名前缀的名称。这样做的目的是通过消除不断引用变量声明以确定变量或函数的类型或范围的需要来提高代码的可读性。

经验丰富的 Basic 程序员已经熟悉这种记法的某种形式很久了,因为 Microsoft Basic 已经使用后缀来表示类型(# 表示 Double& 表示 Long,等等)。

在任何给定程序中使用的匈牙利命名法的细微差别并不重要。重点是要保持一致,以便其他阅读你代码的程序员能够快速学习约定并遵守它们。出于这个原因,明智的做法是不过度使用这种记法,如果存在太多不同的前缀,人们会忘记很少使用的前缀的含义,这会违背目的。最好使用一个通用的前缀,这样人们会记住它,而不是使用一大堆人们记不住的模糊的前缀。

编码标准章节中,对匈牙利命名法进行了更详细的说明。

内存和资源泄漏

[edit | edit source]

你可能认为,由于 Visual Basic 没有本机内存分配函数,因此永远不会发生内存泄漏。不幸的是,情况并非如此;Visual Basic 程序可以通过多种方式泄漏内存和资源。对于小型实用程序,内存泄漏在 Visual Basic 中不是一个严重的问题,因为在程序关闭之前,泄漏没有机会变得足够大以至于会威胁到其他资源用户。

但是,在 Visual Basic 中创建服务器和守护程序是完全合理的,并且此类程序会运行很长时间,因此即使是一个小的泄漏最终也会使操作系统不堪重负。

在 Visual Basic 程序中,内存泄漏最常见的原因是循环对象引用。当两个对象相互引用,但不存在对这两个对象的任何其他引用时,就会出现此问题。

不幸的是,内存泄漏的症状很难在运行的程序中发现,你可能只会在操作系统开始抱怨内存不足时才会注意到。

这是一个展示问题的示例问题。

  'Class1
  Public oOther as Class1

  'module1
  Public Sub main()
    xProblem
  End Sub
  
  Private Sub xProblem
    Dim oObject1 As Class1
    Dim oObject2 As Class1
    set oObject1 = New Class1
    set oObject2 = New Class1
    set oObject1.oOther = oObject2
    set oObject2.oOther = oObject1
  End Sub

Class1 是一个简单的类,没有方法,只有一个公共属性。对于真实程序来说这不是好的编程实践,但足以说明问题。xProblem 子例程只是创建了两个 Class1 实例(对象)并将它们链接在一起。请注意,oObject1 和 oObject2 变量对 xProblem 是局部的。这意味着当子例程完成时,这两个变量将被丢弃。当 Visual Basic 这样做时,它会在每个对象中递减一个计数器,如果此计数器变为零,它会执行 Class_Terminate 方法(如果有),然后恢复对象占用的内存。不幸的是,在这种情况下,引用计数器永远不会变为零,因为每个对象都引用了另一个对象,因此即使程序中的任何变量都不引用任何对象,它们也永远不会被丢弃。任何使用简单引用计数方案来清理对象内存的语言都会遇到这个问题。传统的 C 和 Pascal 不会遇到这个问题,因为它们根本没有垃圾收集器。Lisp 及其相关语言通常使用标记和清除垃圾收集的某种变体,这会减轻程序员的负担,但会以资源负载不可预测的变化为代价。

为了证明确实存在问题,请在 Class1 中添加 InitializeTerminate 事件处理程序,这些处理程序只会向 立即窗口打印消息。

  Private Sub Class_Initialize()
    Debug.Print "Initialize"
  End Sub
  
  Private Sub Class_Terminate()
    Debug.Print "Terminate"
  End Sub

如果 xProblem 例程在没有泄漏的情况下工作,你会看到数量相等的 InitializeTerminate 消息。

练习

[edit | edit source]
  • 修改 xProblem 以确保在退出时两个对象都被处置(提示:将变量设置为 Nothing 会降低它指向的对象的引用计数)。

避免和处理循环引用

[edit | edit source]

有许多技术可以用来避免这个问题,从最明显的技术开始,即根本不允许循环引用。

  • 在你的编程风格指南中禁止循环引用,
  • 显式清理所有引用,
  • 通过其他惯用法提供功能。

在实际程序中,禁止循环引用通常不切实际,因为它意味着放弃使用诸如双向链表之类的有用数据结构。

循环引用的一种经典用法是父子关系。在这种关系中,父级对象,并拥有子级子级。父级及其子级共享一些通用信息,因为信息对所有这些信息都是通用的,所以最自然的是让父级拥有和管理它。当父级超出范围时,父级和所有子级都应该被处置。不幸的是,这在 Visual Basic 中不会发生,除非你帮助完成此过程,因为为了访问共享信息,子级必须引用父级。这是一个循环引用。


 ----------       ----------
 | parent | --->  | child  |
 |        | <---  |        |
 ----------       ----------

在这种特定情况下,你通常可以通过引入辅助对象来完全避免子级到父级的引用。如果你将父级的属性分成两个集合:一个包含仅父级访问的属性,另一个包含父级和子级都使用的属性,那么你可以通过将所有这些共享属性放在辅助对象中来避免循环性。现在,父级和子级都引用辅助对象,并且任何子级都不需要引用父级。

 ----------       ----------
 | parent | ----> | child  |
 |        |       |        |
 ----------       ----------
     |                |
     |                |
     |   ----------   |
      -> | common | <-
         ----------  

注意所有箭头都指向远离父节点。这意味着当我们的代码释放对父节点的最后一个引用时,引用计数将变为零,并且父节点将被释放。这反过来又释放了对子节点的引用。现在,由于父节点和子节点都已消失,因此不再有对公共对象的引用,因此它也将被释放。所有引用计数和释放都会自动执行,作为 Visual Basic 内部行为的一部分,无需编写代码来实现它,您只需正确设置结构即可。

请注意,父节点可以拥有任意数量的子节点,例如保存在对象引用的集合或数组中。

这种结构的常见用途是,当子节点需要将有关父节点的一些信息与其自身的一些信息组合在一起时。例如,如果您正在模拟一些复杂的机器,并希望每个部件都具有一个属性来显示其位置。您不希望将其设置为简单的读写属性,因为这样您必须在机器整体移动时显式地更新每个对象上的该属性。最好将其设置为基于父节点位置和一些维度属性的计算属性,这样当父节点移动时,所有计算属性将保持正确,而无需运行任何额外的代码。另一个应用是返回从根对象到子节点的完全限定路径的属性。

以下是一个代码示例

  'cParent
  Private moChildren as Collection
  Private moCommon as cCommon
  
  Private Sub Class_Initialize()
    Set moChildren = New Collection
    Set moCommon = New cCommon
  End Sub
  
  Public Function NewChild as cChild
    Set NewChild = New cChild
    Set NewChild.oCommon = moCommon    
    moChildren.Add newChild
  End Function

  'cCommon
  Public sName As String

  'cChild
  Private moCommon As cCommon
  Public Name as String
  
  Public Property Set oCommon(RHS as cCommon)
    Set moCommon = RHS
  End Property
  
  Public Property Get Path() as String
    Path = moCommon.Name & "/" & Name
  End Property

就目前而言,它实际上只适用于一层级的父子关系,但通常我们会拥有无限级的层次结构,例如在磁盘目录结构中。

我们可以通过认识到父节点和子节点实际上可以是同一个类,并且子节点并不关心父节点路径是如何确定的,只要它来自公共对象即可,来对这种情况进行概括。

  'cFolder
  Private moChildren as Collection
  Private moCommon as cCommon
  
  Private Sub Class_Initialize()
    Set moChildren = New Collection
    Set moCommon = New cCommon
  End Sub
  
  Public Function NewFolder as cFolder
    Set NewFolder = New cFolder
    Set NewFolder.oCommon = moCommon    
    moChildren.Add newFolder
  End Function
  
  Public Property Set oCommon(RHS as cCommon)
    Set moCommon.oCommon = RHS
  End Property
  
  Public Property Get Path() as String
    Path = moCommon.Path
  End Property
  
  Public Property Get Name() as String 
    Name= moCommon.Name
  End Property 
  
  Public Property Let Name(RHS As String) 
    moCommon.Name = RHS
  End Property 

  'cCommon
  Private moCommon As cCommon
  Public Name As String
  
  Public Property Get Path() as String
    Path = "/" & Name 
    if not moCommon is Nothing then 
      ' has parent
      Path = moCommon.Path & Path 
    End If
  End Property

现在,我们可以要求结构中任何级别上的任何对象提供其完整路径,它将返回该路径,而无需引用其父节点。

练习

[edit | edit source]
  • 创建一个使用 cfolder 和 cCommon 类别的简单程序,并证明它有效;也就是说,它既不会泄漏内存,也不会在Path属性方面给出错误的答案。

错误和异常

[edit | edit source]

在讨论各种错误之前,我们将展示 Visual Basic 中如何处理错误。

Visual Basic 没有异常类,而是使用较旧的错误代码系统。虽然这会使某些类型的编程变得笨拙,但它在编写良好的程序中实际上不会造成很大的麻烦。如果您的程序在正常操作期间不依赖于处理异常,那么您对异常类也没有什么用处。

但是,如果您是正在创建由大量组件(COM DLL)组成的庞大程序的团队中的一员,那么很难保持错误代码列表的同步。一种解决方案是维护一个所有项目成员都使用的主列表,另一种解决方案是最终使用异常类。有关在纯 VB6 中实现 mscorlib.dll 中许多类的实现,请参见VBCorLib,该实现为 Microsoft 的 .NET 架构创建的程序提供了基础。

Visual Basic 中有两种语句实现错误处理系统

  • On Error Goto
  • Err.Raise

处理错误的常用方法是在过程的顶部放置一个On Error Goto语句,如下所示

On Error Goto EH

EH是过程末尾的一个标签。在标签之后,您放置处理错误的代码。这是一个典型的错误处理程序

    Exit Sub
  EH:
    If Err.Number = 5 Then
      FixTheProblem
      Resume
    End If
    Err.Raise Err.Number
  End Sub

需要注意以下几点

  • 错误处理程序标签之前有一个Exit Sub语句,以确保当没有发生错误时,程序不会进入错误处理程序
  • 将错误代码与常量进行比较,并根据结果采取行动。
  • 最后一条语句重新引发错误,以防没有显式处理程序。
  • 包含一个resume语句,以便在失败语句处继续执行。

这可能是一个对某些过程非常有用的处理程序,但它也有一些弱点

  • 使用字面常量。
  • 捕获所有Err.Raise语句没有提供任何有用的信息,它只是传递了原始错误代码。
  • Resume重新执行失败的语句。

没有理由在任何程序中使用字面常量。始终将它们声明为单个常量或枚举。错误代码最好像这样声明为枚举

  Public Enum ErrorCodes
    dummy = vbObjectError + 1
    MyErrorCode
  End Enum

当您需要一个新的错误代码时,只需将其添加到列表中即可。代码将按递增顺序分配。实际上,您真的不需要关心实际数字是多少。

您可以以相同的方式声明内置的错误代码,只是您必须显式设置值

  Public Enum vbErrorCodes
    InvalidProcedureCall = 5
  End Enum

错误分类

[edit | edit source]

大体上来说,有三种类型的错误

预期错误
预期错误发生在用户或其他外部实体提供明显无效的数据时。在这种情况下,用户(无论是人类还是其他程序)必须被告知数据中的错误,而不是被告知在程序中发现错误的位置。
未能遵守契约
调用者未能向子例程提供有效的参数,或者子例程未能提供有效的返回值。
意外错误
发生了一个程序规范中未预测到的错误。

预期错误实际上不是程序中的错误,而是呈现给程序的数据中的错误或不一致。程序必须请求用户修复数据并重试操作。在这种情况下,用户不需要堆栈跟踪或其他内部信息,而是非常希望以外部术语清晰完整地描述问题。错误报告应该直接将用户指向数据中的问题,并提供修复建议。不要只说“无效数据”,说出哪些数据无效,为什么无效以及可以接受的值范围。

契约失败通常表明代码中存在逻辑错误。假设所有无效数据都已被前端剔除,那么程序必须工作,除非程序本身存在故障。有关此主题的更多信息,请参见#断言和契约式设计

意外错误是大多数程序员关注的错误。实际上,它们并不十分常见,之所以看起来很常见,是因为程序各个部分之间的契约很少被明确说明。意外错误与预期错误的不同之处在于,用户通常无法预期能够立即从意外错误中恢复。然后,程序需要以一种可以轻松地传输回程序维护人员的形式,向用户提供程序在发现错误时内部状态的所有详细信息。日志文件非常适合此目的。不要只是向用户显示一个标准的消息框,因为没有实际方法可以捕获该描述,以便将其发送给维护人员。

错误引发和处理

[edit | edit source]

预期错误

[edit | edit source]

这些是输入数据中的错误。从程序的角度来看,它们不是异常。程序应该显式地检查有效的输入数据,并在数据无效时显式地通知用户。这种检查应该在用户界面或程序的其他部分中进行,这些部分能够直接与用户交互。如果用户界面无法自行执行检查,那么底层组件必须提供验证数据的方法,以便用户界面可以使用这些方法。用于通知用户的方法应该取决于错误的严重程度和紧迫性,以及与程序交互的一般方法。例如,源代码编辑器可以在不影响用户创意流程的情况下通过突出显示有问题的语句来标记语法错误,用户可以随时修复它们。在其他情况下,模态消息框可能是正确的通知方法。

契约错误

[编辑 | 编辑源代码]

这些错误通过断言过程的前置条件和后置条件的真值来检测,可以使用断言语句,或者在条件不满足时引发错误。这类错误通常表明程序逻辑存在错误,出现时,报告必须包含足够的信息以便复制问题。报告可以非常明确地说明故障的直接原因。在使用 Visual Basic 时,要记住 Debug.Assert 语句仅在 IDE 中执行,因此除了最关键的例程外,在其他例程中引发错误可能会有用。

这是一个简单的契约断言示例,过于简单:

  Public Sub Reciprocal(n as Double) as Double
    Debug.Assert 0 <> n
    Reciprocal = 1 / n
  End Sub

Visual Basic 将在断言 n = 0 时停止,因此如果在 IDE 中运行测试,则会直接跳转到发现错误的位置。

意外错误

[编辑 | 编辑源代码]

这些错误既没有被输入数据验证捕获,也没有被前置条件或后置条件的真值断言捕获。与契约错误一样,它们表明程序中存在逻辑错误;当然,逻辑错误可能存在于输入数据的验证中,甚至可能存在于某个前置条件或后置条件中。

这些错误需要最全面的报告,因为它们显然不常见,而且很难预测,否则它们将在验证和契约检查中被预见。与契约错误一样,应将报告记录到文本文件中,以便可以轻松地将其发送给维护人员。与契约错误不同,很难确定哪些信息是相关的,因此在 Err.Raise 语句中包含所有子例程参数的描述,以确保安全。


上一节:面向对象编程 目录 下一节:优化 Visual Basic
华夏公益教科书