一. 前言
类(class), 这个话题在各种语言中的入门知识中, 是很多懂哥喜欢装神弄鬼的话题, 假如检索类相关的信息, 很容找到以下相关的内容:
- 多态
- 继承
- 封装
- 抽象
- 类
- 对象
- 实例
- 方法
- 重载
- 对象: 对象是类的一个实例, 有状态和行为. 例如, 一条狗是一个对象, 它的状态有: 颜色, 名字, 品种; 行为有: 摇尾巴, 叫, 吃等.
- 类: 类是一个模板, 它描述一类对象的行为和状态.
大量关于类的信息, 或源于Java这门语言, 或相关概念被Java发扬光大, 其他的语言的学习者在接触到类的时候, 或多或少都会受到来自于Java的影响, 各种生搬硬套, 强行移植概念, 不分场合, 强行为了实现而实现, 用牺牲代码执行效率, 可阅读性等去换取xx概念的实现....
from abc import ABC, abstractmethod
class A(ABC):
@abstractmethod
def say(self):
print('a')
class B(A):
# 假如不重写这个父类的方法, 将直接报错
# TypeError: Can't instantiate abstract class B with abstract method say
def say(self):
print('b')
b = B()
b.say()
以python中的抽象类为例, 上述的代码在于规范代码的书写, 例如希望子类在继承父类必须带有某个方法, 使用@abstractmethod装饰器强制要求.
上述的各种类的概念, 其都是有明确的目的性和作用以及适用场景, 而不在于是否实现类似的概念.
(图: 创建类模块)
相关的概念就不在这里陈述了, 在vba中讨论这些概念也没什么意义, 以下主要讨论如何实现, 以及类的各种细节.
二. 模块
在vbe中, 提供了 5 种不同的模块.
- 工作表模块
- 工作簿模块
- 窗体模块
- 普通模块
- 类模块
前三者可以认为是特殊的类模块, 但是需要注意的是窗体模块的模块级别变量声明(包括public声明的变量), 而不会像其他模块可以保持变量, 除非主动销毁, 其变量的生命周期为窗体关闭即销毁.
在代码作业中, 不建议将普通的代码过多写入前三者中, 尽量保持前三者的整洁干净, 只写入和操作三者相关的代码.
声明全局的public变量(非必要, 不要声明public级别的变量), 建议放置于普通模块中.
三. 为什么需要类
假设一个类是一个播放器, 有开始(play), 有暂停(pause), 有快进(forward).....
面向对象是一种符合人类思维习惯的编程思想. 在程序中使用对象来映射现实中的事物, 使用对象的关系来描述事物之间的联系. 面向对象指以属性和行为的观点去分析现实生活中的事物. 面向对象编程是软件产业化发展的需求. 面向对象编程的核心思想是, 将程序中的实体, 数据和功能抽象为单独的对象, 并在这些对象之间建立联系.
- 物以类聚, 代码更为清晰.
- 内存管控, 更好控制.
- 状态维持/记录.
代码模块化, 堆积木式的代码组合: 易维护, 更高开发效率, 易扩展.
四. 声明变量
要充分理解类, 需要理解声明变量 (VBA) | Microsoft Learn这个东西.
强烈建议在编辑器中启用强制变量声明, 声明变量类型, 这是代码规范的基础.
-
使用 Public 语句声明公共模块级变量.
-
当在模块级别下使用时, Dim 语句等效于 Private 语句. 为了使代码更易于读取和解释, 你可能需要使用 Private 语句.
因为但一个dim在模块声明时, 不容易区分, 加上private, 更容易知道这是在模块中使用的变量.
-
当使用 Static 语句而非 Dim 语句来声明过程中的变量时, 声明的变量将在该过程的两次调用之间保留其值.
这个声明方式, 很少见, 不常用.
声明变量的作用范围
- 函数级
- 模块级
- 全局
Option Explicit
Dim module_2 As Long '模块级别'
Public m_2_var As Long '全局可访问'
Sub tes()
module_2 = module_2 + 1
End Sub
Sub test()
Dim a As Long '函数级别'
a = a + 1
End Sub
要理解三个关键词之间的差异, 主要看变量的存活时间和外部的可访问状态.
Option Explicit
Dim module_2 As Long
Sub tes()
module_2 = module_2 + 1
End Sub
当第二次执行时, module_2这个变量不是初始化的0, 而是保存了上次执行的结果1.
Sub test()
Dim a As Long
a = a + 1
End Sub
Option Explicit
Dim module_2 As Long
Public m_2_var As Long
Sub tes()
module_2 = module_2 + 1
End Sub
Sub test()
Dim a As Long
a = a + 1
End Sub
当在m2模块中创建上述代码, 在m1访问, 可以看到Dim module_2 As Long这个变量并没有访问到, 实际上模块级别的dim实际上等价于private, private只是在名称上区分度更高.
某种程度也可以认为普通模块是类模块的一种特殊存在.
- 函数级, 用完就自动销毁, 外部完全不可访问.
- 模块级, 用完, 需要手动销毁, 模块内可以随意访问, 外部不可访问.
- 全局, 用完, 需要手动销毁.
五. 声明语句
这里只讨论声明sub, function, property部分, 不讨论API声明的其他部分.
| Public | 可选. 指示 Function 过程可由所有模块中的所有其他过程访问. 如果在包含 Option Private 的模块中使用, 那么该过程不能在项目的外部使用. |
|---|---|
| Private | 可选. 指示 Function 过程仅能由声明它的模块中的其他过程访问. |
| Friend | 可选. 仅在类模块中使用. 指示 Function 过程在整个项目中可见, 但是对于对象实例的控制器不可见. |
-
friend
在窗体模块或类模块中修改过程的定义以使其可从类外部的, 但属于定义此类的项目的一部分的模块中进行调用. 标准模块中无法使用 Friend 过程.
最后提一下Friend关键字, 虽然在VBA中几乎没有什么用, 但如果有一天你要制作ActiveX部件, 可能会用到它. 之所以要有Friend关键字, 是因为类的私有部分在类模块外是不可见的, 但有时却需要从外面访问这些私有部分, 这时, 可以使用Friend关键字使属性和方法成为" 友元成员" . 友元成员在本工程中相当于Public, 但在工程外, 它仍是Private .
Public, Friend, 这主要用于一些相对复杂场景, 如从A工作簿访问B工作簿的代码,或者如上的控件的开发中.
5.1 Property
- Get, 读取属性
- Let, 设置普通属性, 不能以这种方式设置属性为对象
set c.o = obj - Set, 设置属性为
Object, 以这种方式设置属性set c.o = obj
六. 示例
Private fso As Object ' file_object
Private log_file As String ' file_path
Private levels() As Variant ' 日志记录的等级
private f As Object ' textstream
' 日志记录
Sub log(ByVal text As String, Optional ByVal level As String = "error")
Dim i As Long
' 返回textstream
If Len(level) > 0 Then
Dim flag As Boolean
For i = 0 To 3
If level = levels(i) Then
flag = True
Exit For
End If
Next
If flag = False Then level = "error"
Else
level = "error"
End If
f.WriteLine CStr(Now()) + ", " + level + " : " + text
End Sub
Private Sub Class_Initialize()
levels = Array("debug", "error", "info", "warning")
log_file = ThisWorkbook.Path + "\log_record.txt"
Set fso = CreateObject("Scripting.FileSystemObject")
Set f = fso.OpenTextFile(log_file, 8, True)
End Sub
Private Sub Class_Terminate()
log_file = ""
f.Close
Set f = Nothing
Set fso = Nothing
End Sub
上述的代码是实现一个简易的日志记录.
其作业流程是这样的:
-
当主程序运行前, 先初始化这个日志类模块.
Set f = fso.OpenTextFile(log_file, 8, True), 这个f object, 将以模块级别的变量进行保存. -
状态维持: 打开日志文件后, 直到主程序运行结束, 这个日志文件一直处于打开的状态, 不需要每次写入日志都重复打开一次日志文件
-
内存控制: 当主程序运行结束, 日志文件关闭, 释放资源
f.Close Set f = Nothing
一个完整的示例
Private status As Boolean
Private control As Long
Private e_obj As Object, f_obj As Object
Private log_lev
' (参数)枚举
Enum log_level
normal = 0
Error = 1
warning = 2
End Enum
' 自定义数据结构
Private Type my_data
date As Long
name As String
End Type
' 在参数中提示输入的类型
Function g(ByVal c_val As log_level) As Long
log_lev = c_val
g = log_lev + 1
End Function
Function h(ByVal c_date As Long, ByVal name As String) As Boolean
Dim md As my_data
md.date = c_date
md.name = name
End Function
' 外部可访问方法
Sub a()
Dim strx(1) As String
End Sub
' 内部方法
Private Sub b()
End Sub
' 外部可访问函数
Function check() As Boolean
End Function
' 内部函数
Private Function abc() As Boolean
End Function
' 外部可访问属性, 但是不能设置
Property Get c() As Boolean
c = status
End Property
' 外部可设置属性
Property Let d(ByVal c_val As Long)
control = c_val
End Property
' 外部可访问属性
Property Get d() As Long
d = control
End Property
' 外部可设置对象
Property Set e(ByRef c_val As Object)
Set e_obj = c_val
End Property
Property Let f(ByRef c_val As Object)
Set f_obj = c_val
End Property
' 类对象初始化
Private Sub Class_Initialize()
control = 0
End Sub
' 类对象销毁
Private Sub Class_Terminate()
Set f_obj = Nothing
End Sub
一个类, 通常应当具有:
- 方法, 执行
- 属性, 执行控制或状态记录与反馈.
七. 事件
event, 事件在编写代码中是非常重要的组成部分.
假设存在 N 多个按钮, 当点击其中的任意按钮时, 需要根据按钮的名称做出不同的响应.
document.onclick = (e) => console.log(e);
JavaScript中的click事件监听, 当鼠标点击页面的任意位置, 都可以捕捉到这个点击的事件, 然后根据点击的事件来执行特定的动作.
- 窗体模块
Option Explicit
Private sets As New Collection
Private Sub UserForm_Initialize()
Dim mycls As myclass
Dim r
' 需要将对应的按钮绑定到一个新的class对象, 使用较为麻烦
For Each r In Me.Controls
If TypeName(r) = "CommandButton" Then
Set mycls = New myclass
Set mycls.my_cmd = r
sets.Add mycls
End If
Next
Set mycls = Nothing
End Sub
Private Sub UserForm_Terminate()
Set sets = Nothing
End Sub
- myclass类模块
Option Explicit
Private WithEvents mycmd As CommandButton
Private Sub mycmd_Click()
Debug.Print mycmd.Caption
End Sub
Property Set my_cmd(ByRef cm As Object)
Set mycmd = cm
End Property
7.1 Excel层级事件
创建一个针对excel整个程序的监听, 打开任意的文件, 都打印出文件的名称.
- app_spy 类模块
Option Explicit
Private WithEvents app As Excel.Application
Property Let set_app(ByRef obj As Object)
Set app = obj
End Property
Private Sub app_WorkbookOpen(ByVal Wb As Workbook)
Debug.Print Wb.Name
End Sub
Private Sub Class_Terminate()
Set app = Nothing
End Sub
- workbook模块
Option Explicit
Private app As app_spy
Private Sub Workbook_Open()
Set app = New app_spy
app.set_app = ThisWorkbook.Application
End Sub
将文件保存为xlam加载项.
类模块, 使得vba在操作excel的各个方面上获得更大的自由度.
八. Implements
先不理会Implements这个单词的中文翻译如何, 但是只要检索vba继承, 就会出来一堆号称继承的示例.
但是当检索英文资料的时候, 会得到相对清晰而准确的描述: vba不支持继承.
正如开篇所言, 类相关的内容, 在中文环境下由于多种原因出现各种混乱的描述.
回来看这个单词的含义:
implement
- **v.**实施; 执行; 落实(政策); 使生效
- **n.**工具; 器具; 〈英〉【法】履行(契约等)
- Web实现; 实现接口; 抽象类是否可实现
(图: pycharm菜单)
Java implements 关键字 (w3schools.cn)
implements关键字用于实现interface接口.
interface关键字用于声明仅包含抽象方法的特殊类型的类.要访问接口方法, 接口必须由另一个具有
implements关键字( 而不是extends) 的类"实现"( 类似于继承) . 接口方法的主体由"implement"类提供.
8.1 继承?
简而言之, 继承的一大特点或者说好处在于: 提高代码的复用.
下面以python为例:
>>> class A:
... status = []
... def say(self, s_val, mode):
... print('hello: ' + s_val)
... self.status.append({s_val: mode})
...
>>> class B(A):
... ...
...
>>> class C(A):
... ...
...
>>> b = B()
>>> b.say('b', True)
hello: b
>>> print(b.status)
[{'b': True}]
>>> c = C()
>>> c.say('c', False)
hello: c
>>> print(c.status)
[{'b': True}, {'c': False}]
B, C 均继承 A 类, 共享了A中的say()(方法) 和 status(属性), 这就是类继承的好处的简单示例, 代码复用.
在回来看vba中提供的Implements
- 常规模块
Option Explicit
Sub test()
Dim x(1) As father_module
Dim i As Long
Set x(0) = New son_a
Set x(1) = New son_b
For i = 0 To 1
x(i).say
Next
End Sub
- father_module, ("父类")类模块
Option Explicit
Sub say()
'不需要任何代码'
End Sub
- son_a, ('子类')类模块
Option Explicit
Implements father_module
Private Sub father_module_say()
Debug.Print "inherit + a"
End Sub
- son_b, ("子类")类模块
Option Explicit
Implements father_module
Private Sub father_module_say()
Debug.Print "inherit + b"
End Sub
可以和上述的python的类继承代码进行比较, vba 中的Implements是否带有 "继承" 的味道? (是否实现代码的复用这一重要特性?)
当删除了其中子类中的"继承"自父类的方法, 就会出现上述的报错.
这不就是最开篇提及python中的类@abstractmethod抽象方法, 子类必须实现继承自父类的方法.
8.2 小结
显然Implements是抽象类方法, 虽然也带有了"继承"的特性, 但是和特性的关系不大, 强行将之和其他语言中的继承套用在vba上不是很合适.
从上面的内容可以知道, 抽象类方法对于管理代码, 标准化函数的命名显然具有强制性.