基础知识

第十四篇、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前缀对象,可以直接在代码中应用。

内置函数调用方法小结:
点击放大的图片
示例:

  数据示例:条件求和,求分车型、能源类型的销量

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

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

'步骤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 数组、字典与集合

  数组、字典与集合有着不同的数据存储方式和用途

特点、创建方法:
点击放大的图片

  初次接触可能会不好理解,我们通过几个相关示例中,创建和使用的方法,来详细了解它们。

一、数组

  静态数组示例,了解数组的空间、如何给数组赋值、数组是如何被使用的?

  动态数组示例,了解它的空间扩展、两种扩展方式的差异,

  结合debug.print打印结果在立即窗口、和Excel表格的交互来演示数组。

示例:

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

复制成功!
1

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

二、字典

  “先引用在声明”方式的初始设置:

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

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返回元素个数,可以通过索引或键访问元素等等。

  不过集合是内置对象,无需引用额外的库,它也支持有序存储(插按插入顺序或指定的位置保存元素),它不要求每个元素都必须有键,所以查找的性能就低于字典(尤其在大型数据集中)。

示例:
复制成功!
1

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协同,也是我分享基础课程的最后一篇!

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