基础知识
第十四篇、VBA基础知识(三):函数和数组、字典、集合
在上一篇🔗代码调试和优化的章节中,①使用工作表的内置函数和方法②减少和Excel的交互可以使用数组③算法级优化中“使用字段和集合”等这些技巧,使用它们可以代码更简洁且更易读。
比如,使用sumifs这个条件求和函数,就远比使用用变量+循环+if判断,简洁的多也易读的多;用数组在内存中运算完成后再和Excel交互,远比每次循环都交互更有效率,也更易修改!
本篇结合示例,介绍“函数”和“数组、字典与集合”的知识!
本篇视频:
|01 函数
VBA支持两种类型的函数,内置函数和自定义函数,内置函数无需定义可以直接调用,自定义函数是用户按照自己的特定需求编写并定义的函数。
一、内置函数
内置函数通常指的是Excel本身提供的、可以直接使用的函数,你安装了Excel就会有它们。在VBA中它的调用也相对简单。在工作表中会使用(知道函数的使用方法、参数意义等),在VBA中,增加调用的前缀对象Application.WorksheetFunction(最严谨的的使用方式)可同样使用。
比如=sumifs(sum_range,criteria_range,ctriteria,...),在VBA中调用就是:Application.WorksheetFunction.sumifs(sum_range,criteria_range,ctriteria,...)。
还有就是部分可以直接使用的自带函数,如Len()、Format()、InStr()。它们无需增加Application.WorksheetFunction前缀对象,可以直接在代码中应用。
内置函数调用方法小结:
示例:
数据示例:条件求和,求分车型、能源类型的销量
Sub 内置函数调用()
Dim i As Integer
i = 2
With ThisWorkbook.Sheets("内置函数")
For i = 2 To 7
.Cells(i, 12) = Application.WorksheetFunction.SumIfs(.Columns(5), .Columns(3), .Cells(i, 9), .Columns(4), .Cells(i, 10)) '直接调用
Next i
End With
End Sub
看到没?内置函数的在VBA代码中的使用非常简单!
- 注意参数传递的格式:在表格中使用时,参数是单元格地址,比如A1,C:C,在VBA代码中调用时,参数必须是显式使用Range\cells()对象,而非直接写单元格地址!"A1"就不可以,Range("A1")或cells(1,1)才可以。
二、自定义函数
自定义函数,顾名思义就是自己开发并定义它功能的函数。当现存的内置函数无法满足特定业务逻辑需求,又需要重复使用这个计算过程时,就需要自己开发函数了。
关于自定义函数的开发
开发自定义函数的意义在于:
- ①解决内置函数的局限性;
- ②提升工作效率;
- ③增强可维护性;
- ④实现某些特殊的功能。
内置函数的功能都是预设好的,但现实中有些特定计算是无法满足的,内置函数就显得有些局限;:
自定义函数能封装计算逻辑,不限次的复用能提升效率;多处调用自定义函数,一处修改即全局生效,可维护性大大增强;处理格式、颜色等非数值数据的特殊功能,也可以自定义成函数。:
2-1、自定义函数开发的基本步骤:
要开发一个自定义函数,一般需要以下步骤:
- 在VBA编辑器中插入模块;
- 使用Functin关键字声明函数,并制定函数名称和参数;
- 在函数体内编写计算逻辑(部分需要添加错误处理);
- 将计算结果赋值给函数名(作为返回值);
我们通过一个自定义函数的标准示例过程,了解开发步骤:
比如,汽车4S店的销售佣金计算,如果销售额超过15万(含15万)且办理了分期,佣金8%;销售额超过了15万(含15万)但未办理分期、或者低于15万且有分期,佣金都是6%;低于15万但是客户未分期,只有4%。
示例:
数据示例:开发一个自定义函数,参数是"销售额"和"是否分期",计算佣金!
'步骤1:在VBA编辑器中插入模块
'步骤2:使用Functin关键字声明函数,并制定函数名称和参数
Function Sales_Commission(Sales As Double, YesNo As String) As Variant
' 规则:
' 1. 销售额 >= 15万 且 有分期 → 佣金8%
' 2. 销售额 >= 15万 且 无分期 → 佣金6%
' 3. 销售额 < 15万 且 有分期 → 佣金6%
' 4. 销售额 < 15万 且 无分期 → 佣金4%
'步骤3:在函数体内编写计算逻辑(添加错误处理)
Dim result As Double
Select Case True
Case Sales >= 150000 And YesNo = "是" '规则1
result = Sales * 0.08
Case Sales < 150000 And YesNo = "否" '规则4
result = Sales * 0.04
Case Else '规则2、3结果一致
result = Sales * 0.06
End Select
Sales_Commission = result '步骤4:将计算结果赋值给函数名(作为返回值)
'错误处理
If YesNo <> "是" And YesNo <> "否" Then
Sales_Commission = CVErr(xlErrNA)
End If
End Function
Sub 自定义函数调用()
Dim i As Integer
With ThisWorkbook.Sheets("自定义函数")
For i = 2 To 7
'直接调用自定义函数
.Cells(i, 5) = Sales_Commission(.Cells(i, 2), .Cells(i, 3)) '直接调用
Next i
End With
End Sub
示例代码很清晰展示了一个自定义函数的一般步骤,也诠释了开发自定义函数的意义!
- 在工作表中使用时,和使用工作表的内置函数方式是一样的,函数名(参数1,参数2,...),在单元格返回结果。=Sales_Commission(A1,A2);
- 在VBA代码中调用自定义函数,可以直接使用,即:Sales_Commission(Range("A1"),Range("A2"))
2-2、自定义函数的注意事项和作用域说明:
- 单一职责:即一个函数只做一件事;
- 明确命名:和过程名一样,要能表达出函数的目的,且避免和内置函数同名的冲突;
- 完整注释:函数的目的;
- 避免交互操作:避免使用Msgbox、InputBox等交互元素,否则会频繁弹出窗口;
- 参数验证:在函数开头检查参数有效性,使用Variant类型可以提供灵活性。
和VBA的变量一样,自定义函数也有自己的作用域,就是它在哪个范围可以被调用?
自定义函数的作用域:
🔗第五篇、Excel VBA变量和变量类型章节中,有过对“变量作用域”的介绍,它们是相通的,也比较容易理解!
掌握这些注意事项和作用域规则,可以创建出高效、稳定且易于维护的VBA自定义函数,能显著提升Excel的自动化处理能力。
|02 数组、字典与集合
数组、字典与集合有着不同的数据存储方式和用途。
- 数组(Array)是一种线性数据结构,用于存储相同类型的元素。元素通过索引访问,索引通常从0开始;数组在内存中连续存储,访问速度非常快,常用来帮助处理工作表区域的连续数据;
- 字典(Dictionary)是一种键值对的集合,用于存储具有对应关系的数据。每个键key、唯一标识一个值value,并且一个键只能对应一个值。它是无序的(就是元素的存储顺序不一定按照插入顺序保持),字典常用来处理快速查找;
- 集合(Collection)是一种无序的元素集合,里面的元素可以是任何类型,但是元素必须是唯一的存在;集合常用于去除重复元素、进行数学运算(并集、交集)等操作。
特点、创建方法:
初次接触可能会不好理解,我们通过几个相关示例中,创建和使用的方法,来详细了解它们。
一、数组
静态数组示例,了解数组的空间、如何给数组赋值、数组是如何被使用的?
动态数组示例,了解它的空间扩展、两种扩展方式的差异,
结合debug.print打印结果在立即窗口、和Excel表格的交互来演示数组。
示例:
示例代码展示的过程在“本篇视频”中有记录
Sub 静态数组()
'静态数组:在声明时就指定空间大小(元素个数)的数组,就是静态数组,大小不可变化,否则会溢出
Dim Arr1(0 To 6) As Integer '7个元素(0-6)
Dim Arr2(1 To 7) As Integer '同样是7个元素(1-7)
Debug.Print UBound(Arr1) 'UBound函数是Visual Basic和VBA中用于确定数组维度边界的核心函数,它返回数组的上标即6
Debug.Print UBound(Arr2) '返回7
Dim i As Integer
For i = 1 To 7 '给数组赋值
Arr1(i - 1) = i 'Arr1下标是从0开始的
Arr2(i) = i * i 'Arr2下标是从1开始的
Next i
Arr2(8) = 8 '超出下标的赋值,会出现“越界”的错误
Dim j As Variant '数组中的元素可以用for each遍历
For Each j In Arr1
Debug.Print j '在立即窗口打印每个元素
Next
For i = 1 To UBound(Arr2) '也可以用索引引用
Debug.Print Arr2(i) '在立即窗口打印每个元素
Next i
'以上是一维数组,也可以多维的
Dim Matrix1(1 To 7, 1 To 7) As String '一个7 * 7的二维数组,
Dim a As Integer, b As Integer
For a = 1 To 7 '双层循环给数组赋值,相当于用行\列的二维坐标索引给二维数组赋值
For b = 1 To 7
Matrix1(a, b) = a & "R" & b & "C"
Next b
Next a
'这里在工作表中,从A1单元格开始,.Resize扩展一个和数组大小一致的区域
ThisWorkbook.Sheets("数组").Range("A1").Resize(7, 7) = Matrix1 '将这个二维数组,写入到工作表
'当然已可以把工作表中的数据(多行、列)赋值给一个(变体)数组,让它像数组一样遍历获取元素
Dim Matrix2 '不在指定类型
Matrix2 = ThisWorkbook.Sheets("数组").Range("A1").Resize(7, 5) '把工作表的7 * 5,即7行\5列的区域赋值给Matrix2
For a = 1 To UBound(Matrix2, 1) '双层循环,依次用数组的行/列坐标索引,打印元素
For b = 1 To UBound(Matrix2, 2)
Debug.Print Matrix2(a, b) '在立即窗口打印出数组的元素,即是单元格A1:E7的内容,先行后列
Next b
Next a
For Each j In Matrix2 '当然也可以用for each 遍历,注意二维数组的遍历,是从外层即行开始的
Debug.Print j
Next j
'可以修改数组的值
Debug.Print Arr1(5)
Arr1(5) = 100 '修改第五个元素的值
Debug.Print Arr1(5)
Debug.Print Matrix1(3, 4)
Matrix1(3, 4) = "你好" '修改二维数组中元素的值
Debug.Print Matrix1(3, 4)
End Sub
Sub 动态数组()
'动态数组:用数组存储内容,但不确定数组的大小需求,而需要在程序运行过程中不断扩展的,就需要动态数组
Dim DyArr() As Variant '定义一个动态数组,未指定空间
ReDim DyArr(1 To 4) '初始分配一个空间,只是初始化
Dim i As Integer
For i = 1 To 3 '动态数组的三个元素赋值
DyArr(i) = i
Next
Dim j
For Each j In DyArr
Debug.Print j '在立即窗口打印数组元素,循环5次(分配的空间是5个),只打印出赋值的3个元素值
Next j
ReDim Preserve DyArr(1 To 5) ' 如果要扩展数据大小,用来继续存储不确定的数据,必须要用 ReDim Preserve,这样会保留数据并、扩展大小
DyArr(4) = 4
DyArr(5) = 5
For Each j In DyArr
Debug.Print j 'Redim Preserve保留原数据,打印1、2、3、4、5
Next j
ReDim DyArr(1 To 5) 'ReDim,相当于重新定义空间,原来1-3的元素会被清空,不会保留
DyArr(4) = 4
DyArr(5) = 5
For Each j In DyArr
Debug.Print j 'Redim清空原数据,打印三个空值和4、5
Next j
End Sub
二、字典
“先引用在声明”方式的初始设置:
示例:
Sub 字典()
'如果先引用了 Microsoft Scripting Runtime库,直接声明一个字典变量即可
Dim dict As New Scripting.Dictionary
'添加元素
dict.Add "东风日产", 536 '字典的.add方法,添加键和值
dict.Add "别克", 937
dict("奥迪") = 300 '简写方式,直接给键付一个值也是可以的
'检查键是否存在,.Exists("键")的方法
If dict.Exists("东风日产") Then
Debug.Print dict("东风日产")
End If
If dict.Exists("雪佛兰") Then
Debug.Print dict("雪佛兰") '不存在则什么都不会在立即窗口打印
End If
'dict.Add "东风日产", 396 '尝试给字典增加一个已存在的键,看会发生什么?错误457,该关键字*****
Debug.Print dict("东风日产")
dict("东风日产") = 396 '如果要修改一个键对应的值,重新赋值就可以了
Debug.Print dict("东风日产")
'关于字典的遍历,For_each每个元素 和 用字索引都可以
Dim key As Variant
For Each key In dict.Keys '循环每个键
Debug.Print key & ":" & dict(key) '获取每个键和对应的值
Next key
Dim value As Variant
For Each value In dict.Items '循环每个值
Debug.Print value
Next value
'用字典的索引
Dim i As Long
For i = 0 To dict.Count - 1 '注意字典的索引是从0开始的,.count方法用于元素的计数,有3个元素,循环从0开始,则是0、1、2,上标要减去1
Debug.Print dict.Keys(i) & ":" & dict.Items(i)
Next i
Debug.Print dict.Count '获取字典元素的数量
dict.Remove "奥迪" ' 删除项
Debug.Print dict.Count '数量变成2
dict.RemoveAll ' 清空字典,在代码优化中提到的“内存管理和对象释放”,数组是使用后及时释放掉,不占用内存,也可以提高效率!
Debug.Print dict.Count '数量变成0
' 没有事先引用 Microsoft Scripting Runtime库,后期绑定是一样的,可以尝试下
Dim dict_x As Object
Set dict_x = CreateObject("Scripting.Dictionary")
End Sub
利用字典的特性,可以快速对表格数据进行:不重复计数\求和、快速查找等操作,
🔗13、VBA基础知识(二):代码调试和优化章节中,有使用字典不重复求条件和的示例,且声明对象的方式是没有事先引用,通过后期绑定,有必要去重新回顾下!
三、集合
集合和对象的方法有很多相似之处,比如都可以通过.add添加元素,.count返回元素个数,可以通过索引或键访问元素等等。
不过集合是内置对象,无需引用额外的库,它也支持有序存储(插按插入顺序或指定的位置保存元素),它不要求每个元素都必须有键,所以查找的性能就低于字典(尤其在大型数据集中)。
示例:
Sub 集合()
Dim Col As New Collection '创建一个集合
'集合添加元素的方式和字典类似,用.add方法
Col.Add 536 '集合的元素可以不带键
Col.Add 937, key:="别克" '集合的元素可以带键,第二个元素是键,显示的用key:=赋值
Col.Add 300, "奥迪" '不显示的键,放在第二个参数位置即可
'获取元素,同样可以通过for_each 和 索引
Dim itm As Variant
For Each itm In Col
Debug.Print itm '获取每个元素值,集合是不要求必要有键的,元素只返回值
Next itm
Dim i As Long
For i = 1 To Col.Count '注意集合的索引是从1开始的,集合同样有.count方法,返回元素的个数
Debug.Print Col.item(i)
Next i
'Col.Add 400, "奥迪" '元素可以没有key(键),但是key元素必须是唯一的,错误****
'和字典的通过重新赋值改变键对应的值不同,字典里修改元素,必须先移除,在添加
Col.Remove "奥迪" '移除元素
Col.Add 400, "奥迪"
Debug.Print Col.item("奥迪")
'也可以通过索引移除
Col.Remove 3 '奥迪是我们插入的第三个元素,通过索引移除
Col.Add 500, "奥迪"
Debug.Print Col.item("奥迪")
Debug.Print Col.item(3)
'集合支持有序存储,.add添加的顺序可支持位置
Col.Remove 3 '移除第三个元素,“奥迪”
Col.Add 500, "奥迪", 2 '在第二个位置前插入,“奥迪”即在第二个位置,
'Col.Add 500, key:="奥迪", before:=2 '显示指定
Debug.Print Col.item("奥迪")
Debug.Print Col.item(2)
End Sub
函数(内置和自定义)、数组、字典与集合,我们是从上一节中的"代码优化"中延伸出来介绍的基础知识点。遇到更复杂的问题时,查阅资料一直是被倡导的正确且全面了解知识的方式”。这些基础知识只是帮助你初步了解它们,能否灵活运用还要看日常实践中遇到的问题。遇到新问题就解决它,慢慢也就会融会贯通!
下一篇:进阶知识:制图、窗体和插件、Office协同
还记得第一篇🔗1、初始设置和编写第一个VBA程序的“迈出关键的第一步”吗?如果当时有立了flag—“用10行代码替代2小时的手工操作,实现自动化办公、深度定制‘节约时间、高强度处理数据的代码’”,截止到当前章节,掌握的知识点,就已经能做到了!恭喜!
"藏在Excel里的编程魔法,让表格学会自己工作"这个是DeepSeek对什么是VBA的回答。
如果购买过市面上关于VBA学习的书籍并完整学习过,对比我分享的第1至14篇内容,会发现知识点的篇章顺序存在诸多差异。大多数书籍内容庞杂,对每个知识点的介绍都过于详尽,这对初学者而言不是内容不足,而是信息过载。我认为从实际工作解决问题的角度入门,再逐步完善知识点的方式最适合我。只有切身体会到VBA带来的帮助后,再从一个知识点延伸、再延伸,逐渐形成体系,直至知识点丰富起来!此时再回头阅读大部头著作,反而是一个系统认知的过程,将其作为参考书最为合适。
我的学习经历也是从各种不同的大部头图书开始的,但迟迟无法入手,因为内容太系统化,不清楚它能具体帮助解决什么问题。后来,我从一个能解决当前问题的小程序入手,随着使用越来越熟悉,也更加愿意使用它。当然,最初使用的这个小程序背后,肯定也离不开图书中的系统知识,只是图书提供的是100%的知识体系,而小应用程序只利用了其中的20%而已。如果能只从这20%入手,有针对性地解决当前问题,再在解决其他问题的过程中逐步掌握剩余的80%,对于实用主义者来说,就再合适不过了!”
当下我仍从事着与数据和表格打交道的工作。但愿我对VBA学习过程的记录总结,能给面临同样问题的职场人带来帮助——从0到80,轻松入门VBA!
这些代码在专业开发人员眼里会显得零碎、不系统,也不是最优解,但它们能解决现实中的问题啊。有多少人能成为专业开发人员呢?职场打工人不都是为了解决工作中的实际问题,才逢山开路、遇水搭桥的吗?
有几个相对高阶的知识点在日常工作中使用得特别少。我认为这些知识属于基础代码知识之外的范畴,主要用于提升用户交互体验、分发代码解决方案以及扩展Excel应用。有兴趣的读者可以进一步阅读。
下一篇几个进阶知识:制图、窗体和插件、Office协同,也是我分享基础课程的最后一篇!
请开发者喝杯咖啡!