【问题标题】:Declare a 0-Length String Array in VBA - Impossible?在 VBA 中声明一个长度为 0 的字符串数组 - 不可能?
【发布时间】:2022-04-27 13:16:08
【问题描述】:

真的不能在VBA中声明一个长度为0的数组吗?如果我试试这个:

Dim lStringArr(-1) As String

我收到一个编译错误,说范围没有值。如果我尝试像这样在运行时欺骗编译器和 redim:

ReDim lStringArr(-1)

我得到一个下标超出范围的错误。

我已经改变了上面的一点,但没有运气,例如

Dim lStringArr(0 To -1) As String

用例

我想将变量数组转换为字符串数组。变量数组可能为空,因为它来自字典的 Keys 属性。 keys 属性返回一个变体数组。我想在我的代码中使用一个字符串数组,因为我有一些函数可以处理我想使用的字符串数组。这是我正在使用的转换功能。由于 lMaxIndex = -1,这会引发下标超出范围错误:

Public Function mVariantArrayToStringArray(pVariants() As Variant) As String()
    Dim lStringArr() As String
    Dim lMaxIndex As Long, lMinIndex As Long
    lMaxIndex = UBound(pVariants)
    lMinIndex = LBound(pVariants)
    ReDim lStringArr(lMaxIndex)
    Dim lVal As Variant
    Dim lIndex As Long
    For lIndex = lMinIndex To lMaxIndex
        lStringArr(lIndex) = pVariants(lIndex)
    Next
    mVariantArrayToStringArray = lStringArr
End Function

破解

返回一个包含空字符串的单例数组。注意——这不是我们想要的。我们想要一个空数组——这样循环它就像什么都不做。但是包含空字符串的单例数组通常可以工作,例如如果我们稍后想将字符串数组中的所有字符串连接在一起。

Public Function mVariantArrayToStringArray(pVariants() As Variant) As String()
    Dim lStringArr() As String
    Dim lMaxIndex As Long, lMinIndex As Long
    lMaxIndex = UBound(pVariants)
    lMinIndex = LBound(pVariants)
    If lMaxIndex < 0 Then
        ReDim lStringArr(1)
        lStringArr(1) = ""
    Else
        ReDim lStringArr(lMaxIndex)
    End If
    Dim lVal As Variant
    Dim lIndex As Long
    For lIndex = lMinIndex To lMaxIndex
        lStringArr(lIndex) = pVariants(lIndex)
    Next
    mVariantArrayToStringArray = lStringArr
End Function

自回答后更新

这是我用于将变量数组转换为字符串数组的函数。 Comintern 的解决方案似乎更先进、更通用,如果我仍然在 VBA 中编码,我可能会切换到那个:

Public Function mVariantArrayToStringArray(pVariants() As Variant) As String()
    Dim lStringArr() As String
    Dim lMaxIndex As Long, lMinIndex As Long
    lMaxIndex = UBound(pVariants)
    lMinIndex = LBound(pVariants)
    If lMaxIndex < 0 Then
        mVariantArrayToStringArray = Split(vbNullString)
    Else
        ReDim lStringArr(lMaxIndex)
    End If
    Dim lVal As Variant
    Dim lIndex As Long
    For lIndex = lMinIndex To lMaxIndex
        lStringArr(lIndex) = pVariants(lIndex)
    Next
    mVariantArrayToStringArray = lStringArr
End Function

注意事项

  • 我使用选项显式。这无法更改,因为它保护了模块中的其余代码。

【问题讨论】:

  • 以下作品Dim a(-1 To 0) As Variant
  • IIRC 仅声明没有指定大小的数组将使其保持“空”,尽管尝试访问该数组而不对其应用 redim 会导致错误(即声明为 Dim test() As String)跨度>
  • 我想Dim results As Variant 是不可能的?传递类型数组总是让人头疼,我早就决定选择我的战斗并将Variant 用于任何类型的数组。变体子类型String 在变体数组中吗?如果是这样,您正在执行不需要发生的处理。
  • @Nathan_Sav 我不确定您的建议。看起来它不会实现问题的要求。如果您找到了解决方案,请在答案中详细说明。
  • arrOutput = Split(vbNullString) 给出一个 LBound 为 0 且 UBound 为 -1 的数组。这与没有内存黑客的情况一样接近。

标签: arrays vba zero


【解决方案1】:

如 cmets 中所述,您可以通过在 vbNullString 上调用 Split 来“本地”执行此操作,如 documented here

表达式 - 必需。包含子字符串和分隔符的字符串表达式。如果表达式是零长度字符串(""),则Split返回一个空数组,即一个没有元素也没有数据的数组。

如果您需要更通用的解决方案(即其他数据类型,您可以直接调用 oleaut32.dll 中的SafeArrayRedim 函数并请求它将传递的数组重新维度为 0 个元素。您必须跳过几个箍来获取数组的基地址(这是由于 VarPtr 函数的一个怪癖)。

在模块声明部分:

'Headers
Private Type SafeBound
    cElements As Long
    lLbound As Long
End Type

Private Const VT_BY_REF = &H4000&
Private Const PVDATA_OFFSET = 8

Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias _
    "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, _
    ByVal length As Long)

Private Declare Sub SafeArrayRedim Lib "oleaut32" (ByVal psa As LongPtr, _
    ByRef rgsabound As SafeBound)

该过程 - 将一个初始化数组(任何类型)传递给它,它将从中删除所有元素:

Private Sub EmptyArray(ByRef vbArray As Variant)
    Dim vtype As Integer
    CopyMemory vtype, vbArray, LenB(vtype)
    Dim lp As LongPtr
    CopyMemory lp, ByVal VarPtr(vbArray) + PVDATA_OFFSET, LenB(lp)
    If Not (vtype And VT_BY_REF) Then
        CopyMemory lp, ByVal lp, LenB(lp)
        Dim bound As SafeBound
        SafeArrayRedim lp, bound
    End If
End Sub

示例用法:

Private Sub Testing()
    Dim test() As Long
    ReDim test(0)
    EmptyArray test
    Debug.Print LBound(test)    '0
    Debug.Print UBound(test)    '-1
End Sub

【讨论】:

  • 你的意思是 SafeArrayRedimNot (vtype And VT_BY_REF) 检查里面吗?
  • 您可能可以通过使用Public Declare PtrSafe Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" (ByRef Ptr() As Any) As LongPtr 访问指向数组的指针而不是手动复制内存并将字符串数组作为EmptyArray 函数的参数来大大简化此代码
  • @ErikA 这不适用于例如数组字符串或 UDT,因为 VB 在将字符串传递给 Declared 函数之前会对其进行转换,因此 VarPtrArray 将返回无用的临时转换副本的地址。
  • 另外,RtlMoveMemory 的第三个参数是SIZE_T,即LongPtrSafeArrayRedim 返回一个Long,也应该是PtrSafe
【解决方案2】:

每个Comintern's comment

创建一个专用的实用函数,它返回VBA.Strings.Split 函数的结果,处理vbNullString,它实际上是一个空字符串指针,这使得意图比使用空字符串文字"" 更明确,后者也可以:

Public Function EmptyStringArray() As String()
     EmptyStringArray = VBA.Strings.Split(vbNullString)
End Function

现在分支您的函数以检查键是否存在,如果没有则返回 EmptyStringArray,否则继续调整结果数组的大小并转换每个源元素。

【讨论】:

  • 有趣的是,如果 OP 确实按照 cmets 中的建议使用了 untyped 选项,那将只是不带参数地调用 Array()
【解决方案3】:

如果我们仍然要使用 WinAPI,我们也可以使用 WinAPI SafeArrayCreate 函数从头开始干净地创建数组,而不是重新设置它的尺寸。

结构声明:

Public Type SAFEARRAYBOUND
    cElements As Long
    lLbound As Long
End Type
Public Type tagVariant
    vt As Integer
    wReserved1 As Integer
    wReserved2 As Integer
    wReserved3 As Integer
    pSomething As LongPtr
End Type

WinAPI 声明:

Public Declare PtrSafe Function SafeArrayCreate Lib "OleAut32.dll" (ByVal vt As Integer, ByVal cDims As Long, ByRef rgsabound As SAFEARRAYBOUND) As LongPtr
Public Declare PtrSafe Sub VariantCopy Lib "OleAut32.dll" (pvargDest As Any, pvargSrc As Any)
Public Declare PtrSafe Sub SafeArrayDestroy Lib "OleAut32.dll"(ByVal psa As LongPtr)

使用它:

Public Sub Test()
    Dim bounds As SAFEARRAYBOUND 'Defaults to lower bound 0, 0 items
    Dim NewArrayPointer As LongPtr 'Pointer to hold unmanaged string array
    NewArrayPointer = SafeArrayCreate(vbString, 1, bounds)
    Dim tagVar As tagVariant 'Unmanaged variant we can manually manipulate
    tagVar.vt = vbArray + vbString 'Holds a string array
    tagVar.pSomething = NewArrayPointer 'Make variant point to the new string array
    Dim v As Variant 'Actual variant
    VariantCopy v, ByVal tagVar 'Copy unmanaged variant to managed one
    Dim s() As String 'Managed string array
    s = v 'Copy the array from the variant
    SafeArrayDestroy NewArrayPointer 'Destroy the unmanaged SafeArray, leaving the managed one
    Debug.Print LBound(s); UBound(s) 'Prove the dimensions are 0 and -1    
End Sub

【讨论】:

  • 这不会泄漏数组的内存吗?我认为用SafeArrayCreate 创建的数组需要用SafeArrayDestroy 释放。
  • @Comintern 你是 100% 正确的, s 确实包含数组的副本,并且原始的 0 长度数组没有被破坏。现已调整
  • 我很确定 SafeArrayDestroy 是 VB 如何杀死它“正常”创建的数组,所以应该没有区别,它应该在检测到数组变量具有非零值时调用它指针。但是,如果您创建的数组是 refers to a portion of an existing array,则调用 SafeArrayDestroy 很重要。
【解决方案4】:

SafeArrayCreateVector

另一个选项,在其他地方的答案中提到,1 2 3SafeArrayCreateVectorSafeArrayCreate 返回一个如 Erik A 所示的指针,而这个指针直接返回一个数组。您需要为每种类型声明,如下所示:

 Private Declare PtrSafe Function VectorBoolean Lib "oleaut32" Alias "SafeArrayCreateVector" ( _
 Optional ByVal vt As VbVarType = vbBoolean, Optional ByVal lLow As Long = 0, Optional ByVal lCount As Long = 0) _
 As Boolean()
 
 Private Declare PtrSafe Function VectorByte Lib "oleaut32" Alias "SafeArrayCreateVector" ( _
 Optional ByVal vt As VbVarType = vbByte, Optional ByVal lLow As Long = 0, Optional ByVal lCount As Long = 0) _
 As Byte()

同样适用于CurrencyDateDoubleIntegerLongLongLongObjectSingleStringVariant。 >

如果您愿意将它们填充到一个模块中,您可以创建一个与Array() 一样工作的函数,但带有一个设置类型的初始参数:

Function ArrayTyped(vt As VbVarType, ParamArray argList()) As Variant
    
    Dim ub As Long: ub = UBound(argList) + 1
    Dim ret As Variant 'a variant to hold the array to be returned

    Select Case vt
        Case vbBoolean: Dim bln() As Boolean: bln = VectorBoolean(, , ub): ret = bln
        Case vbByte: Dim byt() As Byte: byt = VectorByte(, , ub): ret = byt
        Case vbCurrency: Dim cur() As Currency: cur = VectorCurrency(, , ub): ret = cur
        Case vbDate: Dim dat() As Date: dat = VectorDate(, , ub): ret = dat
        Case vbDouble: Dim dbl() As Double: dbl = VectorDouble(, , ub): ret = dbl
        Case vbInteger: Dim i() As Integer: i = VectorInteger(, , ub): ret = i
        Case vbLong: Dim lng() As Long: lng = VectorLong(, , ub): ret = lng
        Case vbLongLong: Dim ll() As LongLong: ll = VectorLongLong(, , ub): ret = ll
        Case vbObject: Dim obj() As Object: obj = VectorObject(, , ub): ret = obj
        Case vbSingle: Dim sng() As Single: sng = VectorSingle(, , ub): ret = sng
        Case vbString: Dim str() As String: str = VectorString(, , ub): ret = str
    End Select
    
    Dim argIndex As Long
    For argIndex = 0 To ub - 1
        ret(argIndex) = argList(argIndex)
    Next
    
    ArrayTyped = ret
    
End Function

这会给出空数组或填充数组,例如Array()。例如:

Dim myLongs() as Long
myLongs = ArrayTyped(vbLong, 1,2,3) '<-- populated Long(0,2)

Dim Pinnochio() as String
Pinnochio = ArrayTyped(vbString) '<-- empty String(0,-1)

ArrayTyped() 函数与 SafeArrayRedim 相同

我喜欢这个功能,但每种类型的所有 API 调用看起来都臃肿不堪。似乎可以使用SafeArrayRedim 完成相同的功能,并且只需一个 API 调用。声明如下:

Private Declare PtrSafe Function PtrRedim Lib "oleaut32" Alias "SafeArrayRedim" (ByVal arr As LongPtr, ByRef dims As Any) As Long

同样的ArrayTyped 函数可能如下所示:

Function ArrayTyped(vt As VbVarType, ParamArray argList()) As Variant
    
    Dim ub As Long: ub = UBound(argList) + 1
    Dim ret As Variant 'a variant to hold the array to be returne
    
    Select Case vt
        Case vbBoolean: Dim bln() As Boolean: ReDim bln(0): PtrRedim Not Not bln, ub: ret = bln
        Case vbByte: Dim byt() As Byte: ReDim byt(0): PtrRedim Not Not byt, ub: ret = byt
        Case vbCurrency: Dim cur() As Currency: ReDim cur(0): PtrRedim Not Not cur, ub: ret = cur
        Case vbDate: Dim dat() As Date: ReDim dat(0): PtrRedim Not Not dat, ub: ret = dat
        Case vbDouble: Dim dbl() As Double: ReDim dbl(0): PtrRedim Not Not dbl, ub: ret = dbl
        Case vbInteger: Dim i() As Integer: ReDim i(0): PtrRedim Not Not i, ub: ret = i
        Case vbLong: Dim lng() As Long: ReDim lng(0): PtrRedim Not Not lng, ub: ret = lng
        Case vbLongLong: Dim ll() As LongLong: ReDim ll(0): PtrRedim Not Not ll, ub: ret = ll
        Case vbObject: Dim obj() As Object: ReDim obj(0): PtrRedim Not Not obj, ub: ret = obj
        Case vbSingle: Dim sng() As Single: ReDim sng(0): PtrRedim Not Not sng, ub: ret = sng
        Case vbString: Dim str() As String: ReDim str(0): PtrRedim Not Not str, ub: ret = str
        Case vbVariant: Dim var() As Variant: ReDim var(0): PtrRedim Not Not var, ub: ret = var
    End Select
    
    Dim argIndex As Long
    For argIndex = 0 To ub - 1
        ret(argIndex) = argList(argIndex)
    Next
    
    ArrayTyped = ret
    
End Function

其他一些资源:

  • 按照here 的逻辑,您也可以使用用户定义的类型来执行此操作。只需像其他人一样添加另一个 API 调用。更多讨论here
  • 如果有人想要多维度的空瓶,还有另一种有趣的方法,使用SafeArrayCreatehere

【讨论】:

    猜你喜欢
    • 2012-05-28
    • 1970-01-01
    • 2021-07-31
    • 2011-01-19
    • 1970-01-01
    • 2012-12-17
    • 1970-01-01
    • 2013-10-22
    • 1970-01-01
    相关资源
    最近更新 更多