为什么我不能将我的类对象声明为这样?

4

我正在创建一个VBA文件的类对象,其目的是充当范围字典,可以传递单个单元格。 如果此单元格包含在某个范围内,则返回与相应范围键相关联的值。 类名为“rangeDic”。

它正在制作中,因此其功能尚未实现。 以下是代码:

Private zone() As String
Private bounds() As String
Private link As Dictionary
Const ContextId = 33

'Init zone
Private Sub Class_Initialize()

    Set link = New Dictionary
    ReDim zone(0)
    ReDim bounds(0)
    
End Sub

'properties
Property Get linkDico() As Dictionary
    Set linkDico = link
End Property

Property Set linkDico(d As Dictionary)
    Set link = d
End Property

Property Get pZone() As String()
    pZone = zone
End Property

Property Let pZone(a() As String)
    Let zone = a
End Property

'methods
Public Sub findBounds()

    Dim elmt As String
    Dim i As Integer
    Dim temp() As String
    
    i = 1
    
    For Each elmt In zone
        ReDim Preserve bounds(i)
        temp = Split(elmt, ":")
        bounds(i - 1) = temp(0)
        bounds(i) = temp(1)
        i = i + 2
    Next elmt

End Sub

我试图在测试子程序中实例化它,以便在设计中调试。这是代码:

Sub test()

    Dim rd As rangeDic
    Dim ran() As String
    Dim tabs() As Variant
    Dim i As Integer
    
    i = 1
    
    With ThisWorkbook.Worksheets("DataRanges")
        While .Cells(i, 1).Value <> none
            ReDim Preserve ran(i - 1)
            ReDim Preserve tabs(i - 1)
            ran(i - 1) = .Cells(i, 1).Value
            tabs(i - 1) = .Cells(i, 3).Value
            i = i + 1
        Wend
    End With
    
    Set rd = createRangeDic(ran, tabs)

End Sub

Public Function createRangeDic(zones() As String, vals() As Variant) As rangeDic

    Dim obje As Object
    Dim zonesL As Integer
    Dim valsL As Integer
    Dim i As Integer
    
    zonesL = UBound(zones) - LBound(zones)
    valsL = UBound(vals) - LBound(vals)
    
    If zonesL <> valsL Then
        Err.Raise vbObjectError + 5, "", "The key and value arrays are not the same length.", "", ContextId
    End If
    
    Set obje = New rangeDic

    obje.pZone = zones()
    
    For i = 0 To 5
        obje.linkDico.add zones(i), vals(i)
    Next i
    
    Set createRangeDic = obje
End Function

请看第2行的Public Function createRangeDic。我必须将我的对象声明为“Object”,如果我尝试将其声明为“rangeDic”,Excel会在obje.pZone = zones()处崩溃。在查看Windows事件日志后,我可以看到一个“错误1000”类型的应用程序未知错误导致崩溃,其中“VB7.DLL”是有问题的包。为什么会这样?我做错了什么吗?感谢您的帮助。编辑:我使用的是Excel 2016。

1
obje.pZone = zones() 对我来说有点奇怪。如果去掉 () 会发生什么? - Vincent G
1
你是否打开了本地窗口或监视窗口?属性过程具有非常严格的类型规则,如果声明的类型与实际值不完全匹配,可能会导致崩溃。尝试将属性获取交换为函数,然后将属性设置为子程序,以分配私有变量。 - Greedo
1
@Greedo,所以切换到Sub和Function可以解决问题,我可以将obje声明为rangeDic。但是,我想知道为什么属性不能使用... - MirageHF
1
我很抱歉,我没有别的答案除了“它不应该这样”...例如,如果您使用x = Range("A1:B4"),即使它应该是x = Range("A1:B4").value,Excel也能猜出返回一个数组,或Set x = Range("A1:B4")返回一个范围。特别是在类模块中,变量必须以非常明确的方式声明,以免强迫Excel猜测。根据您添加的参考文献,特定的变量类型可能会对VBA产生困惑。 - FaneDuru
1
@MirageHF,我想知道你是否有时间测试我在答案中提到的解决方法(在传递给Let属性之前复制数组)。 - Cristian Buse
显示剩余12条评论
1个回答

7

看起来这是一个漏洞。我的Excel没有崩溃,但我收到了一个“内部错误”的提示。

首先让我们澄清一些事情,因为你是从Java背景过来的。

数组只能按引用传递

在VBA中,数组只能通过引用传递给另一个方法(除非您将其包装在变体中)。因此,这个声明:

Property Let pZone(a() As String) 'Implicit declaration

这相当于这个:

Property Let pZone(ByRef a() As String) 'Explicit declaration

当然,还有这个:

Public Function createRangeDic(zones() As String, vals() As Variant) As rangeDic

这等价于这个:
Public Function createRangeDic(ByRef zones() As String, ByRef vals() As Variant) As rangeDic

如果您尝试像这样声明方法参数:ByVal a() As String,那么您将只会得到一个编译错误。
数组被复制后才能被分配。
假设有两个名为ab的数组,当执行类似于a = b的操作时,b数组的一个副本将被分配到a。让我们测试一下。在标准模块中添加以下代码:
Option Explicit

Sub ArrCopy()
    Dim a() As String
    Dim b() As String
    
    ReDim b(0 To 0)
    b(0) = 1
    
    a = b
    a(0) = 2
    
    Debug.Print "a(0) = " & a(0)
    Debug.Print "b(0) = " & b(0)
End Sub

运行 ArrCopy 后,我的立即窗口如下所示:
enter image description here

如图所示,当改变数组 a 时,数组 b 的内容不受影响。

属性 Let 始终接收其参数 ByVal,无论您是否指定 ByRef

让我们进行测试。创建一个名为 Class1 的类,并添加以下代码:

Option Explicit

Public Property Let SArray(ByRef arr() As String)
    arr(0) = 1
End Property

Public Function SArray2(ByRef arr() As String)
    arr(0) = 2
End Function

现在创建一个标准模块并添加此代码:

Option Explicit

Sub Test()
    Dim c As New Class1
    Dim arr() As String: ReDim arr(0 To 0)
    
    arr(0) = 0
    Debug.Print arr(0) & " - value before passing to Let Property"
    c.SArray = arr
    Debug.Print arr(0) & " - value after passing to Let Property"
    
    arr(0) = 1
    Debug.Print arr(0) & " - value before passing to Function"
    c.SArray2 arr
    Debug.Print arr(0) & " - value after passing to Function"
End Sub

运行 Test 后,我的即时窗口看起来像这样:
enter image description here 因此,这个简单的测试证明了,即使数组只能通过ByRef传递,Property Let仍会复制数组。 错误原因 你原来的变量ran(Sub test) 以新名称zonesByRef传递给createRangeDic,然后再次被pZone (Let属性) ByRef传递。通常情况下,通过ByRef传递数组多次不应该存在任何问题,但在这里似乎出现了问题,因为Property Let正在尝试复制。有趣的是,如果我们在createRangeDic中替换它:
obje.pZone = zones()

使用这个:

Dim x() As String
x = zones
obje.pZone = x

即使将obje声明为As rangeDic,该代码也可以正常运行。这是因为x数组是zones数组的一个副本。
看起来Property Let不能复制已经通过ByRef多次传递的数组,但如果它只被传递了一次,则可以完美地工作。也许是由于调用堆栈中添加堆栈帧的方式,存在内存访问问题,但很难确定。无论问题是什么,这似乎都是一个错误。
与问题无关,但我必须补充几点:
  1. 在循环中使用ReDim Preserve不是一个好主意,因为每次都会为新的(更大的)数组分配新的内存,并且每个元素都从旧数组复制到新数组。这非常慢。相反,像@DanielDušek在评论中建议的那样,使用Collection,或者尽量减少ReDim Preserve的调用次数(例如,如果您知道将有多少个值,那么就只需要在开始时将数组的维数设定一次)。
  2. 逐个单元格读取Range非常慢。通过使用Range.ValueRange.Value2属性将整个Range读入数组中(我更喜欢后者),这两种方法都会返回一个数组,只要该范围具有多于一个单元格。
  3. 如果私有成员对象负责类的内部工作,则永远不要公开该对象。例如,您不应该公开自定义集合类中的私有集合,因为它会破坏封装性。在您的情况下,linkDico公开了内部字典,这可以从主类实例的外部进行修改。也许在您的特定示例中没有任何问题,但是值得一提。另一方面,Property Get pZone() As String()是安全的,因为它返回内部数组的副本。
  4. 在所有模块/类的顶部添加Option Explicit,以确保强制执行正确的变量声明。对我而言,您的代码无法编译,因为VBA中不存在none,除非您在项目的其他地方使用了它。一旦打开选项,我还发现了其他一些问题。

1
清晰全面,除了核心问题外还非常有帮助——真正的技巧宝库+ :) - T.M.

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