一. 前言
类(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
上不是很合适.
从上面的内容可以知道, 抽象类方法对于管理代码, 标准化函数的命名显然具有强制性.
九. 总结