基础知识
第十三篇、VBA基础知识(二):代码调试和优化
接上一篇:代码优化,是在保证代码可读性的基础上,让VBA运行效率得到提升。」
注释符和操作(运算符),它们在代码行中无处不在,上篇介绍了注释符的使用技巧、操作(运算符)是如何实现复杂的逻辑运算的......。
对于开发高效、稳定的VBA程序,代码的调试和优化至关重要。
为什么要对代码进行调试和优化?
- 任何程序开发,无论大小都不太可能从头到尾一气呵成,然后期待它运行快速、结果准确。
- 现实是运行中时常伴随着错误(bug)、耗时、未达成预期结果。
代码调试和优化一般指什么?
本篇就来介绍"代码调试(工具及其应用场景)" 和 "代码优化(技巧)"。
本篇视频:
|01 代码调试
为什么要调试代码?肯定是运行时发现了问题!这些个问题要么是“错误”导致运行中断、要么是未按照预期得到结果,我们就需要找到错误并纠正它!
一、一个关于代码调试的典型流程:
- “复现问题”:确定触发错误的条件;
- “定位错误”:通过错误提示定位大致位置、设置断点缩小范围,单步执行观察错误行为;
- “分析状态”:在断点处检查变量值(本地窗口/监视窗口)、在立即窗口测试假设;
- “修复并验证”:修改代码、重新运行测试、确认解决问题。
在以上“典型流程”中提到的定位、断点、单步执行、本地/监视/立即窗口,它们是通过VBA编辑器(VBE)提供的工具来完成的。每个(调试)工具适用于不同的应用场景。这些工具可以帮助完成调试,即是帮助完成:①发现错误②帮助理解代码行为③验证逻辑④优化性能的这个过程。
二、关于VBE中的调试工具和应用场景:
对调试工具的设置方法、作用的小结:
三、一些常见的问题类型场景,对应的调试工具组合:
程序运行时不同的问题,表现不同,对应调试时使用的工具组合也不同。
示例代码:
示例代码展示的调试过程在“本篇视频”中有记录
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
通过以上关于代码调试的“典型过程”说明和不同错误发生时工具的使用示例,总结:
- VBA调试的核心在于“控制执行流程”和“观察程序状态”。熟练掌握断点、单步执行、监视窗口等工具,能提升解决“问题代码”的效率。结合系统化的调试流程(复现→定位→修复→验证),可以应对各类错误。
|02 代码优化
VBA代码优化是提高代码的执行效率、减少资源消耗和增强可维护性的关键过程,特别是在处理大数据量时,效果更为显著。但是一定要注意保持代码的可读性,避免过度优化带来的维护成本!
“最好的优化是不做不必要的操作。”——Donald Knuth(唐纳德·克努特)
一、代码优化的核心原则:
- “减少交互次数”:最小化VBA与Excel的交互(最耗时的操作)
- “避免重复计算”:缓存结果,减少不必要的计算
- “使用高效数据结构”:数组代替单元格操作,集合/字典快速查找
- “合理使用内存”:及时释放对象
- “优化算法逻辑”:选择合适算法
和代码调试相似,每遇到不同的程序问题就有不同的调试工具和方法。同样,围绕代码优化的几个核心原则,也有不同优化技术。
比如:
①“减少交互次数”,常用VBA把数据写入Excel和从Excel中取出内容,可以每循环一次就写入(交互)一次、直到循环结束,也可以先循环存储在数组内,然后一次性写入,发现会大大提升效率;②“避免重复计算”,把一行可以直接计算出结果的代码不小心放在了循环中,每次循环都会是同样结果,这样无效的重复计算也会影响效率。
二、代码优化的技术:
VBA的代码优化技术总结:
代码的优化技术里,出现了几个我们之前未使用过的名词,比如“数组”、“字典”、“集合”,学习过其他编程知识的,对这个概念不会陌生,不过暂时不了解也没关系,当前我们只需要知道:
- ①数组和集合都是VBA的内置对象,用Dim声明,存储数据的;
- ②字典对象是需要创建;
- ③它们都需要分配了内存空间才可以使用;
这些就可以了。想了解的它们的可以查找下资料,或者之后专门分享一篇它们使用的介绍,当前通过代码示例简单了解下,它们是如何在代码优化中被使用的即可。
优化是主要的目的是为了提升运行效率,我们在示例中会加入优化前后的性能对比,看这些技术能带来多少的变化!
示例代码:
代码的展示的优化过程在“本篇视频”中有记录
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中直接调用!
下一篇基础知识(三):自定义函数和数组、字典与集合
请开发者喝杯咖啡!