基础知识

第十三篇、VBA基础知识(二):代码调试和优化

  接上一篇:代码优化,是在保证代码可读性的基础上,让VBA运行效率得到提升。」

  注释符和操作(运算符),它们在代码行中无处不在,上篇介绍了注释符的使用技巧、操作(运算符)是如何实现复杂的逻辑运算的......。

  对于开发高效、稳定的VBA程序,代码的调试和优化至关重要。

为什么要对代码进行调试和优化?
  • 任何程序开发,无论大小都不太可能从头到尾一气呵成,然后期待它运行快速、结果准确。
  • 现实是运行中时常伴随着错误(bug)、耗时、未达成预期结果。
代码调试和优化一般指什么?
  • 通过一系列工具和技术来识别、定位和修复程序中的这些问题的过程就是代码调试,它能帮助开发者确保代码按预期运行,并处理可能出现的情况。
  • 通过改进代码结构、算法和资源利用方式,使代码运行更快、更高效、更节省资源,同时保持可读性和可维护性的过程,就是代码优化

  本篇就来介绍"代码调试(工具及其应用场景)" 和 "代码优化(技巧)"。

本篇视频:

|01 代码调试

  为什么要调试代码?肯定是运行时发现了问题!这些个问题要么是“错误”导致运行中断、要么是未按照预期得到结果,我们就需要找到错误并纠正它!

一、一个关于代码调试的典型流程:

  在以上“典型流程”中提到的定位、断点、单步执行、本地/监视/立即窗口,它们是通过VBA编辑器(VBE)提供的工具来完成的。每个(调试)工具适用于不同的应用场景。这些工具可以帮助完成调试,即是帮助完成:①发现错误②帮助理解代码行为③验证逻辑④优化性能的这个过程。

比如:

①当发生运行终止的错误时,在程序中用“断点”,可以找出代码中发生错误的行段,配合“单步执行”,帮助我们找出是语法错误、逻辑错误还是其他;②“单步执行”,配合“监视/立即窗口”,帮助我们跟踪代码执行流程,观察变量和对象状态的变化,就是“理解代码行为”

二、关于VBE中的调试工具和应用场景:
对调试工具的设置方法、作用的小结:
点击放大的图片
三、一些常见的问题类型场景,对应的调试工具组合:

  程序运行时不同的问题,表现不同,对应调试时使用的工具组合也不同。

点击放大的图片
示例代码:

  示例代码展示的调试过程在“本篇视频”中有记录

点击放大的图片
复制成功!
1

Sub 语法错误的调试()
    '代码无法运行,根据弹出的提示调试

    '1-变量未定义
    'Dim my_i As Long
    my_i = 10000
    Debug.Print "指定的数是" & my_i

    '2-对象变量缺少关键字set
    Dim sht As Worksheet
    Set sht = ThisWorkbook.Sheets(1) 'set
    Debug.Print sht.Name

    '3-循环或判断语句不完整,缺失结束语句
    If my_i > 0 Then
        Debug.Print "正确"
    'End If '缺失

    For Each sht In ThisWorkbook.Sheets
        Debug.Print sht.Name
    'Next '缺失

    '4-计算错误
    Dim a As Integer, b As Integer
    a = 5
    b = 1
    'Debug.Print a / (a - b * a) '除数为0

End Sub

Sub 运行和逻辑检查()
    '代码中断、崩溃、逻辑错误时,通过设置断点、单步执行
    '观察本地窗口的变量变化是否符合预期
    '立即窗口打印调试的信息,可在调试时执行命令
    '监视窗口添加表达式,监视变量的变化,

    '某个时间段的汽车销量存储在表格中,计算燃油车各车型大类的销量和个数
    Dim aData, acx, s
    Dim i As Long, jc_i As Long, suv_i As Long, mpv_i As Long, cp_i As Long
    Dim jc_Tol As Long, suv_Tol As Long, mpv_Tol As Long, cp_Tol As Long

    aData = ThisWorkbook.Sheets("示例").Range("A1").CurrentRegion

    '立即窗口查看需要循环的总行数
    jc_i = 0: suv_i = 0                         '各车型个数从0开始计数
    mpv_i = 0: cp_i = 0
    '循环前设置断点,
    For i = 1 To UBound(aData)                  '循环数据源的每一行
        If aData(i, 4) = "燃油" Then            '判断能源类型是否是燃油
            Select Case aData(i, 3)             'select case条件判断车系分类
                Case Is = "轿车"                '对该车系计数 和 累加销量
                    jc_i = jc_i + 1
                    jc_Tol = jc_Tol + aData(i, 5)
                Case Is = "SUV"
                    suv_i = suv_i + 1
                    suv_Tol = suv_Tol + aData(i, 5)
                Case Is = "MPV"
                    mpv_i = mpv_i + 1
                    mpv_Tol = mpv_Tol + aData(i, 5)
                Case Is = "跑车"
                    cp_i = cp_i + 1
                    cp_Tol = cp_Tol + aData(i, 5)
            End Select

        End If
    Next i

    Debug.Print "轿车是个数是:" & jc_i & vbCrLf & "销量是: " & jc_Tol  '立即创建输出 车系计数和总销量
    Debug.Print "SUV是个数是:" & suv_i & vbCrLf & "销量是: " & suv_Tol
    Debug.Print "MPV是个数是:" & mpv_i & vbCrLf & "销量是: " & mpv_Tol

End Sub

  通过以上关于代码调试的“典型过程”说明和不同错误发生时工具的使用示例,总结:

|02 代码优化

  VBA代码优化是提高代码的执行效率、减少资源消耗和增强可维护性的关键过程,特别是在处理大数据量时,效果更为显著。但是一定要注意保持代码的可读性,避免过度优化带来的维护成本!

  “最好的优化是不做不必要的操作。”——‌Donald Knuth(唐纳德·克努特)

一、代码优化的核心原则:

  和代码调试相似,每遇到不同的程序问题就有不同的调试工具和方法。同样,围绕代码优化的几个核心原则,也有不同优化技术

比如:

①“减少交互次数”,常用VBA把数据写入Excel和从Excel中取出内容,可以每循环一次就写入(交互)一次、直到循环结束,也可以先循环存储在数组内,然后一次性写入,发现会大大提升效率;②“避免重复计算”,把一行可以直接计算出结果的代码不小心放在了循环中,每次循环都会是同样结果,这样无效的重复计算也会影响效率。

二、代码优化的技术:

  VBA的代码优化技术总结:

点击放大的图片

  代码的优化技术里,出现了几个我们之前未使用过的名词,比如“数组”、“字典”、“集合”,学习过其他编程知识的,对这个概念不会陌生,不过暂时不了解也没关系,当前我们只需要知道:

  • ①数组和集合都是VBA的内置对象,用Dim声明,存储数据的;
  • ②字典对象是需要创建;
  • ③它们都需要分配了内存空间才可以使用;

  这些就可以了。想了解的它们的可以查找下资料,或者之后专门分享一篇它们使用的介绍,当前通过代码示例简单了解下,它们是如何在代码优化中被使用的即可。

  优化是主要的目的是为了提升运行效率,我们在示例中会加入优化前后的性能对比,看这些技术能带来多少的变化!

示例代码:

  代码的展示的优化过程在“本篇视频”中有记录

复制成功!
1

Sub 减少与Excel的交互()
    Dim startTime_1 As Double, startTime_2 As Double

    '每次循环都访问单元格,看写入10000个数据的耗时
    startTime_1 = Timer '获取开始时间
    Dim i As Long
    For i = 1 To 10000
        ThisWorkbook.Sheets("示例-代码优化").Cells(i, 1).Value = i
    Next i
    Debug.Print "执行时间: " & Format(Timer - startTime_1, "0.000") & " 秒"

   '内存中循环,一次写入,即和Excel只交互一次,看写入10000个数据的耗时
    startTime_2 = Timer '获取下个代码段的开始时间
    Dim aData(1 To 10000, 1 To 1) As Long '声明一个数组
    For i = 1 To 10000
        aData(i, 1) = i '内存操作
    Next i
    ThisWorkbook.Sheets("示例-代码优化").Range("B1:B10000").Value = aData  '一次写入
    Debug.Print "执行时间: " & Format(Timer - startTime_2, "0.000") & " 秒"

End Sub

Sub 控制excel的非必要功能()
    '批量操作工作簿,打开后取值,看关闭屏幕刷新与否的耗时对比
    '用之前合并工作簿表的示例代码,清除掉合并部分,只保留打开和关闭的操作

    Dim FileToOpen As Variant
    Dim i As Long
    Dim startTime_1 As Double, startTime_2 As Double

    '1-不禁用屏幕刷新功能
    FileToOpen = Application.GetOpenFilename("Excel文件,*.xl*", , "请选择要合并的多个工作簿/表", , True)
    startTime_1 = Timer '获取开始时间,从选择工作簿开始后、核心代码行运行前开始计时
    For i = 1 To UBound(FileToOpen)
        Workbooks.Open Filename:=FileToOpen(i)
        ActiveWorkbook.Close  '最后关闭工作簿
    Next i
    Debug.Print "选择" & UBound(FileToOpen) & "个文件打开并关闭的执行时间:" & Format(Timer - startTime_1, "0.000") & " 秒"

    '2-禁用屏幕刷新功能
    Application.ScreenUpdating = False  ' 关闭屏幕刷新
    FileToOpen = Application.GetOpenFilename("Excel文件,*.xl*", , "请选择要合并的多个工作簿/表", , True)
    startTime_2 = Timer '获取下个代码段的开始时间
    For i = 1 To UBound(FileToOpen)
        Workbooks.Open Filename:=FileToOpen(i)
        ActiveWorkbook.Close  '最后关闭工作簿
    Next i
    Debug.Print "选择" & UBound(FileToOpen) & "个文件打开并关闭的执行时间: " & Format(Timer - startTime_2, "0.000") & " 秒"
    Application.ScreenUpdating = True  ' 注意在主程序运行结束后恢复原状

    '其他的几个控制功能
    Application.Calculation = xlCalculationManual   ' 手动计算
    Application.Calculation = xlCalculationAutomatic         '恢复自动计算

    Application.DisplayAlerts = False              '禁止警告,禁止掉新增工作表、重命名、关闭未保存的工作簿等等弹出的提醒,根据需要谨慎使用
    Application.DisplayAlerts = True               '恢复初始设置

    Application.EnableEvents = False    ' 禁用事件
    Application.EnableEvents = True    '恢复初始设置
End Sub

Sub with高效引用对象()
    '对一个单元格赋值,并设置单元格格式,边框、底色、字体颜色等等,最后清除,循环100次看看
    Dim startTime_1 As Double, startTime_2 As Double
    Dim i As Integer

    '常规引用
    startTime_1 = Timer '获取开始时间
    For i = 1 To 100
        ThisWorkbook.Sheets("示例-代码优化").Range("C1").Value = "C1"
        ThisWorkbook.Sheets("示例-代码优化").Range("C1").Font.Bold = True
        ThisWorkbook.Sheets("示例-代码优化").Range("C1").Font.Color = -16776961
        ThisWorkbook.Sheets("示例-代码优化").Range("C1").Interior.Color = vbYellow
        ThisWorkbook.Sheets("示例-代码优化").Range("C1").Clear
    Next i
    Debug.Print "执行时间: " & Format(Timer - startTime_1, "0.000") & " 秒"

    '使用with高效引用变量
    startTime_2 = Timer '获取下个代码段的开始时间
    For i = 1 To 100
        With ThisWorkbook.Sheets("示例-代码优化").Range("C1")
            .Value = "C1"
            .Font.Bold = True
            .Font.Color = -16776961
            .Interior.Color = vbYellow
            .Clear
        End With '结束语
    Next i
    Debug.Print "执行时间: " & Format(Timer - startTime_2, "0.000") & " 秒"
End Sub

Sub 关于循环的优化()
    '1-选择高效的循环,避免在循环中访问对象属性,
    '比如第一个示例"减少和Excel的互动",低效的是在循环中访问对象,高效的是读取到数组后,一次性写回!

    '能循环一次尽量不多次。
    Dim i As Long, Tol_1 As Long, Tol_2 As Long
    For i = 1 To 100
        Tol_1 = Tol_1 + i
        Tol_2 = Tol_2 + i * i
    Next i
    Debug.Print Tol_1 / Tol_2
    '永远好过
    For i = 1 To 100
        Tol_1 = Tol_1 + i
    Next i
    For i = 1 To 100
        Tol_2 = Tol_2 + i * i
    Next i
    Debug.Print Tol_1 / Tol_2

    '使用For Each代替索引处理对象集合,永远会更快
    Dim sht As Worksheet
    For Each sht In ThisWorkbook.Sheets
        sht.Activate
    Next sht
    '永远快过
    For i = 1 To ThisWorkbook.Sheets.Count
        ThisWorkbook.Sheets(i).Activate
    Next i

    '尝试通过数学方法减少循环层数
    For i = 1 To 10
        Tol_1 = Tol_1 + Cells(i, 1)
    Next i
    '远不如,直接用工作表函数
    Tol_1 = Sum(Range("A1:A10"))

End Sub

Sub 利用字典对象快速查找()

    Dim dict As Object                              '声明一个对象dict
    Set dict = CreateObject("Scripting.Dictionary") '初始化对象为字典

    Dim aData
    Dim i As Long
    aData = ThisWorkbook.Sheets("示例-代码调试").Range("A1").CurrentRegion '把工作表A1单元格的当前区域,赋值给aData,减少和Excel的交互

    '循环对象
    For i = 1 To UBound(aData)
        dict(aData(i, 1)) = dict(aData(i, 1)) + aData(i, 5) '例如字典的“键”不重复的特征,对每个相同的键对应的值累加求和
    Next i

    If dict.exists("东风日产") Then
        Debug.Print "销量合计: " & dict("东风日产")  '用“键”名称,快速查找对应的值
    End If
End Sub

Sub 集合处理唯一值()
    '利用集合的特点,获取不存放重复的元素
    '比循环遍历所有元素,判断单个元素是否在目标区域或数组内出现,依次求得唯一值更快

    Dim uItems As Collection    '新建一个集合对象uItems
    Set uItems = New Collection '初始化为一个新的集合实例

    Dim aData
    Dim i As Long
    aData = ThisWorkbook.Sheets("示例-代码调试").Range("A1").CurrentRegion '把工作表A1单元格的当前区域,赋值给aData,减少和Excel的交互,已能提交效率

    On Error Resume Next '忽略重复错误,往集合中添加元素时,如果已存在,会弹出错误并终止运行,我们的目标是为了获取不重复值,就利用错误处理忽略这类错误
    For i = 1 To UBound(aData)
        uItems.Add aData(i, 1), aData(i, 1)
    Next i
    On Error GoTo 0

    '以上就得到了一个不含重复值的集合
    Dim item As Variant
    For Each item In uItems '遍历集合的每个元素,在立即创建打印出来
        Debug.Print item
    Next item

    '当然也可以逐个写入到工作簿的工作表中,with高效引用对象
    With ThisWorkbook.Sheets("示例-代码调试")
        For i = 1 To uItems.Count
            .Cells(i, 8) = uItems(i)
        Next i
    End With

    '最好是一次写入到工作簿的工作表中,减少和excel的交互
    Dim arr() As Variant
    ReDim arr(1 To uItems.Count)
    For i = 1 To uItems.Count
        arr(i) = uItems(i)
    Next i
    ThisWorkbook.Sheets("示例-代码调试").Range("I1:I" & UBound(arr)).Value = Application.Transpose(arr) '一次写入,这个的arr是一维的(横向),写入到列区域中,用Transpose转换(纵向)
End Sub

  • 通过这些代码示例,看到在相同的场景下,优化前后的运行效率提升了数倍。当遇到大型的对象和数据处理时,优化工作的重要性不可忽视!

下一篇:VBA基础语句(三):自定义函数和数组、字典与集合

   本篇里提到“内置函数(如sum)”和“数组、字典与集合”时是简要带过。它们至关重要、能大幅提升执行效率,代码可读性和可维护性也更好!下个篇幅对它们做详细的了解,包含基础概念、使用方法、应用场景!。

  内置函数是Excel本身提供的、可以直接使用的函数,安装了Excel就会有它们。学习了VBA,就可以根据需求编写属于自己的函数。当需要经常调用某个从A到B的实现过程,当前的内置函数中没有方法可以直接实现,就可以“自定义”一个,在工作表或VBA中直接调用!

  下一篇基础知识(三):自定义函数和数组、字典与集合

请开发者喝杯咖啡 请开发者喝杯咖啡!