如何在Excel VBA中使用实现?

68

我正在尝试为工程项目实现一些形状,并将其抽象出来以用于一些常见函数,以便我可以拥有一个广义程序。

我想做的是创建一个名为cShape的接口,并让cRectanglecCircle实现cShape

我的代码如下:

cShape 接口

Option Explicit

Public Function getArea()
End Function

Public Function getInertiaX()
End Function

Public Function getInertiaY()
End Function

Public Function toString()
End Function

cRectangle

Option Explicit
Implements cShape

Public myLength As Double ''going to treat length as d
Public myWidth As Double ''going to treat width as b

Public Function getArea()
    getArea = myLength * myWidth
End Function

Public Function getInertiaX()
    getInertiaX = (myWidth) * (myLength ^ 3)
End Function

Public Function getInertiaY()
    getInertiaY = (myLength) * (myWidth ^ 3)
End Function

Public Function toString()
    toString = "This is a " & myWidth & " by " & myLength & " rectangle."
End Function

cCircle

Option Explicit
Implements cShape

Public myRadius As Double

Public Function getDiameter()
    getDiameter = 2 * myRadius
End Function

Public Function getArea()
    getArea = Application.WorksheetFunction.Pi() * (myRadius ^ 2)
End Function

''Inertia around the X axis
Public Function getInertiaX()
    getInertiaX = Application.WorksheetFunction.Pi() / 4 * (myRadius ^ 4)
End Function

''Inertia around the Y axis
''Ix = Iy in a circle, technically should use same function
Public Function getInertiaY()
    getInertiaY = Application.WorksheetFunction.Pi() / 4 * (myRadius ^ 4)
End Function

Public Function toString()
    toString = "This is a radius " & myRadius & " circle."
End Function

问题是每当我运行测试用例时,就会出现以下错误:

编译错误:

对象模块需要为接口“~”实现“~”


@L42 为什么要设置悬赏?您能解释一下您的需求吗? - Daniel Dušek
6个回答

95

这是一个玄学的面向对象编程概念,使用自定义形状集合需要更多的操作和理解。

您可能首先想要查看此答案,以便对VBA中的类和接口有一个基本的理解。


按照以下说明进行操作

首先打开记事本并复制下面的代码

VERSION 1.0 CLASS
BEGIN
  MultiUse = -1
END
Attribute VB_Name = "ShapesCollection"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

Dim myCustomCollection As Collection

Private Sub Class_Initialize()
    Set myCustomCollection = New Collection
End Sub

Public Sub Class_Terminate()
    Set myCustomCollection = Nothing
End Sub

Public Sub Add(ByVal Item As Object)
    myCustomCollection.Add Item
End Sub

Public Sub AddShapes(ParamArray arr() As Variant)
    Dim v As Variant
    For Each v In arr
        myCustomCollection.Add v
    Next
End Sub

Public Sub Remove(index As Variant)
    myCustomCollection.Remove (index)
End Sub

Public Property Get Item(index As Long) As cShape
    Set Item = myCustomCollection.Item(index)
End Property

Public Property Get Count() As Long
    Count = myCustomCollection.Count
End Property

Public Property Get NewEnum() As IUnknown
    Attribute NewEnum.VB_UserMemId = -4
    Attribute NewEnum.VB_MemberFlags = "40"
    Set NewEnum = myCustomCollection.[_NewEnum]
End Property

将该文件保存为ShapesCollection.cls并保存到桌面。

确保你要保存的文件扩展名是 *.cls而不是ShapesCollection.cls.txt

现在打开Excel文件,进入VBEALT+F11并在Project Explorer中右键单击。从下拉菜单中选择Import File并导航到该文件。

enter image description here

NB:您需要先将代码保存在.cls文件中,然后再导入它,因为VBEditor不允许使用属性。这些属性允许您在迭代中指定默认成员并在自定义集合类上使用for each循环。

了解更多信息:

现在插入3个类模块。相应地重新命名并复制粘贴代码。

cShape 这是您的接口

Public Function GetArea() As Double
End Function

Public Function GetInertiaX() As Double
End Function

Public Function GetInertiaY() As Double
End Function

Public Function ToString() As String
End Function
cCircle (中文:c圆)
Option Explicit

Implements cShape

Public Radius As Double

Public Function GetDiameter() As Double
    GetDiameter = 2 * Radius
End Function

Public Function GetArea() As Double
    GetArea = Application.WorksheetFunction.Pi() * (Radius ^ 2)
End Function

''Inertia around the X axis
Public Function GetInertiaX() As Double
    GetInertiaX = Application.WorksheetFunction.Pi() / 4 * (Radius ^ 4)
End Function

''Inertia around the Y axis
''Ix = Iy in a circle, technically should use same function
Public Function GetInertiaY() As Double
    GetInertiaY = Application.WorksheetFunction.Pi() / 4 * (Radius ^ 4)
End Function

Public Function ToString() As String
    ToString = "This is a radius " & Radius & " circle."
End Function

'interface functions
Private Function cShape_getArea() As Double
    cShape_getArea = GetArea
End Function

Private Function cShape_getInertiaX() As Double
    cShape_getInertiaX = GetInertiaX
End Function

Private Function cShape_getInertiaY() As Double
    cShape_getInertiaY = GetInertiaY
End Function

Private Function cShape_toString() As String
    cShape_toString = ToString
End Function

cRectangle

Option Explicit

Implements cShape

Public Length As Double ''going to treat length as d
Public Width As Double ''going to treat width as b

Public Function GetArea() As Double
    GetArea = Length * Width
End Function

Public Function GetInertiaX() As Double
    GetInertiaX = (Width) * (Length ^ 3)
End Function

Public Function GetInertiaY() As Double
    GetInertiaY = (Length) * (Width ^ 3)
End Function

Public Function ToString() As String
    ToString = "This is a " & Width & " by " & Length & " rectangle."
End Function

' interface properties
Private Function cShape_getArea() As Double
    cShape_getArea = GetArea
End Function

Private Function cShape_getInertiaX() As Double
    cShape_getInertiaX = GetInertiaX
End Function

Private Function cShape_getInertiaY() As Double
    cShape_getInertiaY = GetInertiaY
End Function

Private Function cShape_toString() As String
    cShape_toString = ToString
End Function

现在您需要插入一个标准的模块,并复制粘贴以下代码:

Module1

Option Explicit

Sub Main()

    Dim shapes As ShapesCollection
    Set shapes = New ShapesCollection

    AddShapesTo shapes

    Dim iShape As cShape
    For Each iShape In shapes
        'If TypeOf iShape Is cCircle Then
            Debug.Print iShape.ToString, "Area: " & iShape.GetArea, "InertiaX: " & iShape.GetInertiaX, "InertiaY:" & iShape.GetInertiaY
        'End If
    Next

End Sub


Private Sub AddShapesTo(ByRef shapes As ShapesCollection)

    Dim c1 As New cCircle
    c1.Radius = 10.5

    Dim c2 As New cCircle
    c2.Radius = 78.265

    Dim r1 As New cRectangle
    r1.Length = 80.87
    r1.Width = 20.6

    Dim r2 As New cRectangle
    r2.Length = 12.14
    r2.Width = 40.74

    shapes.AddShapes c1, c2, r1, r2
End Sub

运行 Main 子程序并在 Immediate Window 中检查结果 CTRL+G

输入图像说明


注释和解释:

在你的 ShapesCollection 类模块中,有两个子过程用于向集合中添加项。

第一个方法 Public Sub Add(ByVal Item As Object) 简单地接受一个类实例并将其添加到集合中。您可以在Module1中像这样使用它

Dim c1 As New cCircle
shapes.Add c1

Public Sub AddShapes(ParamArray arr() As Variant)允许您同时添加多个对象,用逗号,分隔,与AddShapes()子程序完全相同。

这比单独添加每个对象要好得多,但选择使用哪个取决于您。

注意在循环中我已经将一些代码注释掉了。

Dim iShape As cShape
For Each iShape In shapes
    'If TypeOf iShape Is cCircle Then
        Debug.Print iShape.ToString, "Area: " & iShape.GetArea, "InertiaX: " & iShape.GetInertiaX, "InertiaY:" & iShape.GetInertiaY
    'End If
Next

如果你从'If'End If行中删除注释,就可以仅打印cCircle对象。如果你能在VBA中使用委托,这将非常有用,但事实并非如此,所以我已经向你展示了打印单个对象的另一种方法。你可以根据需要修改If语句,或者简单地打印所有对象。再次强调,你如何处理数据取决于你自己 :)


1
这太棒了!您能解释一下在当前上下文中如何使用 IUnknown 吗?从快速谷歌搜索中,我很难弄清楚...此外,在 ShapesCollection.cls 中,是不是应该使用 myCustomCollection.Remove(index) 而不是 value?真是美妙的一段代码! - Ioannis
2
哦,谢谢@Ioannis,那只是一个简单的错误,我现在已经纠正了。IUnknownstdole2.tlb类型库的一部分。您可以使用OLE/COM对象查看器来发现其实现。更深入地了解IUnknown可能也会成为一个新的独立问题 :) - user2140173
2
非常感谢您详细的回答!我正在尝试建模一堆具有任意横截面形状的建筑柱。您的答案不仅回答了这个问题,还解决了我的下一个问题(如何制作一组柱子):) 现在必须编写最大或最小选择器! - Zigu
3
我刚意识到@mehow就是VBA4All...真是糊涂。 - RubberDuck
2
@mehow 谢谢你的分享,我有几个问题。ShapesCollection类中为什么不使用Class_Initialize和Class_Terminate例程?此外,您认为将索引参数从Long更改为Variant以便可以传递Key进行集合索引会存在任何危险吗? - Cool Blue
显示剩余5条评论

23
以下是对给出答案的一些理论和实践贡献,以便于那些想了解implements/interfaces的人。我们知道,VBA不支持继承,所以我们可以几乎盲目地使用接口来在不同类之间实现共同属性/行为。但我认为有必要描述一下两者的概念差异,以便后来的问题更好地得到解决。
继承:定义了is-a关系(一个正方形是一个图形);
接口:定义了must-do关系(一个典型的例子是“drawable”接口,要求可绘制对象必须实现“draw”方法)。这意味着来自不同根类的类可以实现共同的行为。
继承意味着基类(某种物理或概念原型)被扩展,而接口实现了一组定义特定行为的属性/方法。因此,可以说Shape是一个基类,其他所有形状都继承自它,并且可以实现drawable接口使所有形状可绘制。该接口将是一个契约,保证每个Shape都有一个draw方法,指定如何/在哪里绘制形状:圆可能与正方形不同。
类IDrawable:
'IDrawable interface, defining what methods drawable objects have access to
Public Function draw()
End Function

由于VBA不支持继承,我们被迫选择创建一个接口IShape,保证通用形状(正方形、圆形等)实现特定属性/行为,而不是创建一个抽象的Shape基类来进行扩展。

class IShape:

'Get the area of a shape
Public Function getArea() As Double
End Function

我们遇到麻烦的部分是当我们想要使每个形状都可绘制时。
不幸的是,由于IShape是VBA中的一个接口而不是基类,我们无法在基类中实现可绘制接口。似乎VBA不允许我们一个接口实现另一个接口;经过测试后,编译器似乎没有提供所需的行为。换句话说,我们不能在IShape内实现IDrawable,并期望由此强制IShape的实例实现IDrawable方法。
我们被迫将此接口实现到实现IShape接口的每个通用形状类中,幸运的是,VBA允许实现多个接口。

类cSquare:

Option Explicit

Implements iShape
Implements IDrawable

Private pWidth          As Double
Private pHeight         As Double
Private pPositionX      As Double
Private pPositionY      As Double

Public Function iShape_getArea() As Double
    getArea = pWidth * pHeight
End Function

Public Function IDrawable_draw()
    debug.print "Draw square method"
End Function

'Getters and setters

下面是接口的典型用途/好处。

让我们通过编写一个工厂来开始我们的代码,该工厂返回一个新的正方形。(这只是我们无法直接向构造函数发送参数的解决方法):

模块mFactory:

Public Function createSquare(width, height, x, y) As cSquare
    
    Dim square As New cSquare
    
    square.width = width
    square.height = height
    square.positionX = x
    square.positionY = y
    
    Set createSquare = square
    
End Function

我们的主要代码将使用工厂创建一个新的正方形:
Dim square          As cSquare

Set square = mFactory.createSquare(5, 5, 0, 0)

当您查看可用方法时,您会发现您逻辑上可以访问在cSquare类中定义的所有方法:

enter image description here

我们稍后会看到为什么这很重要。
现在你应该想知道如果你真的想创建一组可绘制对象会发生什么。你的应用程序可能包含不是形状但仍然可绘制的对象。理论上,没有什么能阻止你拥有一个可以绘制的IComputer接口(可能是一些剪贴画或其他东西)。
你可能想要一组可绘制对象的原因是,你可能想在应用程序生命周期的某个时刻循环渲染它们。
在这种情况下,我将编写一个装饰器类来包装集合(我们将看到原因)。 class collDrawables:
Option Explicit

Private pSize As Integer
Private pDrawables As Collection

'constructor
Public Sub class_initialize()
    Set pDrawables = New Collection
End Sub

'Adds a drawable to the collection
Public Sub add(cDrawable As IDrawable)
    pDrawables.add cDrawable
    
    'Increase collection size
    pSize = pSize + 1
    
End Sub

装饰器允许您添加一些原生VBA集合不提供的便利方法,但实际重点在于该集合仅接受可绘制的对象(实现IDrawable接口)。如果我们尝试添加一个不可绘制的对象,则会抛出类型不匹配的错误(只允许可绘制的对象!)。
因此,我们可能希望循环遍历一组可绘制的对象以进行渲染。允许非可绘制的对象进入集合将导致错误。渲染循环可能如下所示:
Option Explicit

    Public Sub app()
        
        Dim obj             As IDrawable
        Dim square_1        As IDrawable
        Dim square_2        As IDrawable
        Dim computer        As IDrawable
        Dim person          as cPerson 'Not drawable(!) 
        Dim collRender      As New collDrawables
        
        Set square_1 = mFactory.createSquare(5, 5, 0, 0)
        Set square_2 = mFactory.createSquare(10, 5, 0, 0)
        Set computer = mFactory.createComputer(20, 20)
        
        collRender.add square_1
        collRender.add square_2
        collRender.add computer
        
        'This is the loop, we are sure that all objects are drawable! 
        For Each obj In collRender.getDrawables
            obj.draw
        Next obj
        
    End Sub

请注意,上述代码增加了很多透明度:我们将对象声明为IDrawable,这使得循环永远不会失败,因为绘制方法在集合中的所有对象上都可用。
如果我们尝试将Person添加到集合中,如果此Person类没有实现可绘制接口,则会抛出类型不匹配的异常。
但也许最重要的原因是,将对象声明为接口很重要,因为我们只想公开在接口中定义的方法,而不是那些在个别类中定义的公共方法,正如我们之前所看到的。
Dim square_1        As IDrawable 

enter image description here

我不仅确定square_1有一个draw方法,而且它还确保只有由IDrawable定义的方法被暴露出来。
对于正方形来说,这个好处可能并不立即清晰,但让我们来看一个更加清晰的Java集合框架的类比。

想象一下你有一个通用接口叫做IList,它定义了不同类型列表适用的一组方法。每种类型的列表是一个特定的类,实现了IList接口,定义了它们自己的行为,并可能在其上添加更多自己的方法。

我们如下声明列表:

dim myList as IList 'Declare as the interface! 

set myList = new ArrayList 'Implements the interface of IList only, ArrayList allows random (index-based) access 

在上面的代码中,将列表声明为IList可以确保您不会使用ArrayList特定的方法,而只能使用接口规定的方法。想象一下,您将列表声明如下:
dim myList as ArrayList 'We don't want this

您将可以访问ArrayList类中明确定义的公共方法。有时这可能是需要的,但通常我们只想利用内部类行为,而不是由类特定公共方法定义。
如果在代码中使用此ArrayList 50次以上,并突然发现最好使用LinkedList(允许与此类型的列表相关的特定内部行为),则受益会变得清晰明了。
如果我们遵循接口,就可以更改以下行:
set myList = new ArrayList

到:

set myList = new LinkedList 

而且,只要接口确保合同得到履行,即仅使用在IList上定义的公共方法,因此其他代码都不会出现问题,因此可以随时交换不同类型的列表。

最后一件事(在VBA中可能较少人知道的行为)是您可以为接口提供默认实现。

我们可以按以下方式定义一个接口:

IDrawable:

Public Function draw()
    Debug.Print "Draw interface method"
End Function

还有一个实现绘制方法的类:

cSquare:

implements IDrawable 
Public Function draw()
    Debug.Print "Draw square method" 
End Function

我们可以通过以下方式在不同实现之间进行切换:
Dim square_1        As IDrawable

Set square_1 = New IDrawable
square_1.draw 'Draw interface method
Set square_1 = New cSquare
square_1.draw 'Draw square method    

如果您将变量声明为cSquare,则不可能实现这一点。我暂时想不到什么好的例子可以说明其有用性,但如果您测试它,则在技术上是可行的。

13

关于VBA和"实现"语句,有两个未记录的添加。

  1. VBA不支持在派生类的继承接口中的方法名中使用下划线字符'_'。例如,如果存在cShape.get_area这样的方法,那么它将不会编译(在Excel 2007下测试):对于任何派生类,VBA都会输出上述编译错误。

  2. 如果一个派生类没有实现与接口同名的自己的方法名,则VBA可以成功编译代码,但是该方法将无法通过派生类类型的变量进行访问。


3
接口类中的函数声明不可以包含下划线。 这个内容应该在文档中用大而粗的字体来标注。 - MathKid

8
我们必须在使用接口的类中实现所有方法。 cCircle 类
Option Explicit
Implements cShape

Public myRadius As Double

Public Function getDiameter()
    getDiameter = 2 * myRadius
End Function

Public Function getArea()
    getArea = Application.WorksheetFunction.Pi() * (myRadius ^ 2)
End Function

''Inertia around the X axis
Public Function getInertiaX()
    getInertiaX = Application.WorksheetFunction.Pi() / 4 * (myRadius ^ 4)
End Function

''Inertia around the Y axis
''Ix = Iy in a circle, technically should use same function
Public Function getIntertiaY()
    getIntertiaY = Application.WorksheetFunction.Pi() / 4 * (myRadius ^ 4)
End Function

Public Function toString()
    toString = "This is a radius " & myRadius & " circle."
End Function

Private Function cShape_getArea() As Variant

End Function

Private Function cShape_getInertiaX() As Variant

End Function

Private Function cShape_getIntertiaY() As Variant

End Function

Private Function cShape_toString() As Variant

End Function

cRectangle Class

Option Explicit
Implements cShape

Public myLength As Double ''going to treat length as d
Public myWidth As Double ''going to treat width as b
Private getIntertiaX As Double

Public Function getArea()
    getArea = myLength * myWidth
End Function

Public Function getInertiaX()
    getIntertiaX = (myWidth) * (myLength ^ 3)
End Function

Public Function getIntertiaY()
    getIntertiaY = (myLength) * (myWidth ^ 3)
End Function

Public Function toString()
    toString = "This is a " & myWidth & " by " & myLength & " rectangle."
End Function

Private Function cShape_getArea() As Variant

End Function

Private Function cShape_getInertiaX() As Variant

End Function

Private Function cShape_getIntertiaY() As Variant

End Function

Private Function cShape_toString() As Variant

End Function

cShape Class

Option Explicit

Public Function getArea()
End Function

Public Function getInertiaX()
End Function

Public Function getIntertiaY()
End Function

Public Function toString()
End Function

enter image description here


3

语法的快速修复

如果接口 ISomeInterface 具有:

Public Sub someMethod()
    ' Interface, no code
End Sub

那么实现就需要像这样:

Implements ISomeInterface

Public Sub ISomeInterface_someMethod()
    '      ^^^^^^^^^^^^^^^  ' If missing: Compile Error 
    ' Code goes here
End Sub

一个不错的方法:

Implements ISomeInterface

Private Sub someMethod()
    ' Business logic goes here
End Sub

Public Sub ISomeInterface_someMethod()
    someMethod ' i.e. Business logic in 1 place: someMethod
End Sub

尽管如此,其他答案也非常值得一读。

2
非常有趣的文章,可以简单地理解接口何时何地有用!但我认为你关于默认实现的最后一个例子是不正确的。对于作为IDrawable实例化的square_1的第一次调用draw方法,会正确打印您给出的结果,但是对于作为cSquare实例化的square_1的第二次调用draw方法是错误的,什么也没有打印。实际上涉及到3种不同的方法:
IDrawable.cls:
Public Function draw()
    Debug.Print "Interface Draw method"
End Function

cSquare.cls:

Implements IDrawable

Public Function draw()
    Debug.Print "Class Draw method"
End Function

Public Function IDrawable_draw()
    Debug.Print "Interfaced Draw method"
End Function

标准模块:

Sub Main()
    Dim square_1 As IDrawable
    Set square_1 = New IDrawable
    Debug.Print "square_1 : ";
    square_1.draw

    Dim square_2 As cSquare
    Set square_2 = New cSquare
    Debug.Print "square_2 : ";
    square_2.draw 

    Dim square_3 As IDrawable
    Set square_3 = New cSquare
    Debug.Print "square_3 : ";
    square_3.draw
End Sub

结果为:

square_1 : Interface Draw method
square_2 : Class Draw method
square_3 : Interfaced Draw method

Class6_Methods_ 是什么? - omegastripes
我的测试模块遗留问题已经纠正,感谢。 - hymced

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接