【问题标题】:Determining the full type of a variable确定变量的完整类型
【发布时间】:2016-02-29 17:02:39
【问题描述】:

变量的完整类型是指您在即时窗口中获得的信息类型:

我想使用 VBA 动态确定类型信息。函数 TypeName() 没有做我想要的,因为它返回一个变体的 subtype 并且不区分例如一个变量变量保存一个范围,一个对象变量保存一个范围,一个范围变量保存一个范围。

作为初步步骤,我编写了一个函数来检测是否有变量传递给它。它通过利用传递引用语义来工作。该代码使用其参数执行的操作只能使用变体完成,因此如果传递的变量实际上不是变体,则会触发错误:

Function IsVariant(var As Variant) As Boolean
    Dim temp As Variant
    Dim isVar As Boolean

    If IsObject(var) Then
        Set temp = var
    Else
        temp = var
    End If

    On Error Resume Next
        Set var = New Collection
        var = "test"
        If Err.Number > 0 Then
            isVar = False
        Else
            isVar = True
        End If
    On Error GoTo 0

    If IsObject(temp) Then
        Set var = temp
    Else
        var = temp
    End If
    IsVariant = isVar
End Function

基于此,我写道:

Function FullType(var As Variant) As String
    If IsVariant(var) Then
        FullType = "Variant/" & TypeName(var)
    Else
        FullType = TypeName(var)
    End If
End Function

测试代码:

Sub TestTypes()
    Dim R As Range
    Dim Ob As Object
    Dim i As Integer
    Dim v1 As Variant
    Dim v2 As Variant

    v1 = 10
    i = 10

    Set v2 = Range("A1")
    Set Ob = Range("A2")
    Set R = Range("A3")

    Debug.Print "v1: " & FullType(v1)
    Debug.Print "i: " & FullType(i)
    Debug.Print "v2: " & FullType(v2)
    Debug.Print "Ob: " & FullType(Ob)
    Debug.Print "R: " & FullType(R)  
End Sub

输出:

v1: Variant/Integer
i: Integer
v2: Variant/Range
Ob: Range
R: Range

这几乎是我想要的——但不区分持有范围的对象变量和持有范围的范围变量。我尝试编写一个名为 IsTypeObject 的函数,它的工作原理与 IsVariant 类似,但似乎无法让它工作:

Function IsTypeObject(var As Variant) As Boolean
    Dim temp As Variant
    Dim isGeneric As Boolean

    If (Not IsObject(var)) Or IsVariant(var) Then
        IsTypeObject = False
        Exit Function
    End If

    Set temp = var
    On Error Resume Next
        Set var = New Collection
        Set var = ActiveWorkbook
        If Err.Number > 0 Then
            isGeneric = False
        Else
            isGeneric = True
        End If
    On Error GoTo 0

    Set var = temp
    IsTypeObject = isGeneric
End Function

测试:

Sub test()
    Dim R As Range
    Set R = Range("A1")
    Debug.Print IsTypeObject(R)
End Sub

但这会打印True,尽管我认为使IsVariant 工作的相同的传递引用语义也应该使IsTypeObject 工作(你不能将集合分配给一个范围)。我尝试了各种调整,但似乎无法区分通用对象变量和特定对象变量(例如范围变量)。

那么——关于如何动态获取变量的完整类型的任何想法? (动机是作为调试日志实用程序的一部分)

【问题讨论】:

  • 变体的传递引用有效,因为 VARIANT 结构 has the VT_BYREF flag 可能会或可能不会被设置。非 Variant 变量没有这样的东西。您在 .NET 中有反射,但在 VBA 中没有。
  • @GSerg 奇怪的是,即使在我的“成功”IsVariant 的情况下,当我传递一个Range 变量并通过在调试器,Set var = New Collection 行实际上并没有触发错误——当我尝试为其分配字符串时,下一行是导致我捕获的错误的原因。我在想 VBA 必须通过某种复制恢复方法实现按引用传递——但奇怪的是下一行会导致错误(而不是返回时的类型不匹配)。
  • 这是因为在值上下文中使用对象时,VB 会尝试使用对象的default property。假设var 是一个对象,var = "test" 会尝试将"test" 分配给var 的默认属性,但它没有(错误438,这不是类型不匹配错误)。
  • 我认为你可以通过manually analyzing the vt memberVariant 重写你的IsVariant 函数,使其更加友好。它很可能有VT_BYREF 标志和what it actually points to 标志。但是为了判断一个变量是在代码As Range还是As Object中声明的,你需要成为编译器。
  • @GSerg 这解释了很多。 locals 窗口可能通过解析Dim 语句和通过type 函数动态获取其信息。我怀疑如果我真的想获得完整的类型,某种形式的解析是不可避免的,但希望我忽略了一些(相对)简单的东西。

标签: vba excel


【解决方案1】:

是的,你可以这样做:它需要一点指针知识和“解除引用”的概念......

这是执行此操作的代码:

Public Function VariantTypeName(ByRef MyVariant) As String
' Returns the expanded type name of a variable, indicating
' whether it's a simple data type (eg: Long Integer), or a
' Variant containing data of that type, eg: "Variant/Long"
Dim iType As Integer Const VT_BYREF = &H4000&
CopyMemory iType, MyVariant, 2
' iType now contains the VarType of the incoming parameter ' combined with a bitwise VT_BYREF flag indicating that it ' was passed by reference. In other words, it's a pointer, ' not the data structure of the variable (or a copy of it)
' So we should have VT_BYREF - and we'd always expect to ' when MyVariant is a Variant, as variants are a structure ' which uses a pointer (or pointers) to the stored data...
' However, the VBA implementation of a variant will always ' dereference the pointer - all the pointers - passing us ' straight to the data, stripping out all that information ' about references...
If (iType And VT_BYREF) = VT_BYREF Then ' Bitwise arithmetic detects the VT_BYREF flag: VariantTypeName = TypeName(MyVariant) Else ' No VT_BYREF flag. This is a Variant, not a variable: VariantTypeName = "Variant/" & TypeName(MyVariant) End If
End Function

CopyMemory API 函数的声明在后面几段)。

这需要一些解释,因为 Visual Basic 语言系列旨在保护您免受变量及其类型的实现细节 - 尤其是指针的概念 - 而我的代码确实涉及一些横向思考。

简单来说,您的变量有一个名称 - 您在代码中看到的类似“intX”的字符串;分配用于包含实际数据的内存区域;以及该内存的地址。

该地址实际上是分配给变量的内存的起始地址,变量将实现作为内存中的结构,由实际数据的偏移量定义,大小(或长度)的数据 - 对于复杂类型,通过偏移到内存中其他结构的地址。这些大小和偏移量是预定义的:它们是变量的实际实现,我们 VBA 开发人员很少需要知道这一点——我们声明了类型,这一切都为我们完成了。

今天你需要知道的第一件事是 VBA 中变量地址的前两个字节是the enumerated var type:这就是 VarType() 函数的工作原理。

当程序传递该地址时,它不会传递内存中数据的复制分配,而是将该地址作为指针传递。是的,我将其中的某些内容过于简单化了,但 VBA 开发人员确实知道获取指针和数据副本之间的区别:它在我们声明函数时用于传入参数的 ByRefByVal 标识符中.

VBA 和 VB 非常非常擅长为我们屏蔽细节:太好了,以至于我们无法使用 VarTypeTypeName 来检测我们已经传递了一个值或对它的引用;甚至是对引用的引用,对引用的引用。

这很重要,因为变体是其他变量的包装器,并且该结构为您提供了一个指向它包含的变量的指针 var 类型来描述它:但是,我们无法知道在VBA - 我们直接沿着地址指示的行向下传递,一直到我们将要使用的数据,而 VBA varType 永远不会告诉我们我们通过连续地址通过几跳间接到达那里由指针定义。

但是,如果您准备使用 API 调用查看指针后面的这两个字节,则该信息确实存在。

正如我所说,这两个字节包含 var 类型 - 但还有更多:它们包含 var 类型和按位标记 VT_BYREF 的组合,表示这是对存储数据类型的引用或指针,而不是数据本身。因此,这段代码将可靠地告诉您您的 var 类型,稍微横向思考在我们不希望 VBA 提供帮助时克服它:

Public Function DereferencedType(ByRef MyVar) As Long
Dim iType As Integer
Const VT_BYREF = &H4000&
' The first two bytes of a variable are the type ID with a ' bitwise OR to VT_BYREF if we were passed the variable by ' reference... Which is exactly what this function does:
CopyMemory iType, MyVar, 2
DereferencedType = iType ' Mod VT_BYREF
'Use "Mod VT_BYREF" to separate out the type if you want
End Function
乍一看,这个函数似乎弄巧成拙:我通过引用传递变量 - 变体或简单类型,所以它总是与VT_BYREF 结合使用。无论如何,我已经注释掉了“模”算术......

...这就是它的实际工作方式:传递一个简单的变量,它会告诉你你通过引用传递了变量:

Dim str1 As String
str1 = "One Hundred"
Debug.Print "String Variable: " & DereferencedType(str1)
...你会得到输出vbString OR VT_BYREF

String Variable: 16392

但是,如果您向我们的函数传递一个字符串变体,VBA 的 Variant 实现将使您免受指针和通过引用传递的所有复杂性 - 一直到数据 - 并为您提供删除所有不需要的信息的数据:

Dim varX As Variant
varX = "One Hundred"
Debug.Print "String Variant:  " & DereferencedType(varX)
...你得到输出:

String Variant:  8

我会让您使用VT_BYREF 对返回的值进行 OR 或 NOT 操作编码,以便为您的 Variant/String 和 Variant/Long 输出的扩展字符串描述符提供“Variant/”标签。

[编辑:做到了,它在答案的顶部,实现为VariantTypeName]

我建议您如图所示声明 CopyMemory API 调用,并为您可能遇到的所有环境使用条件编译器常量:


#If VBA7 And Win64 Then    ' 64 bit Excel under 64-bit Windows
                           ' Use LongLong and LongPtr
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _ (Destination As Any, _ Source As Any, _ ByVal Length As LongLong)
#ElseIf VBA7 Then ' 64 bit Excel in all environments ' Use LongPtr only, LongLong is not available
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _ (Destination As Any, _ Source As Any, _ ByVal Length As Long)
#Else ' 32 bit Excel
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _ (Destination As Any, _ Source As Any, _ ByVal Length As Long) #End If

同时,更难的问题 - 获取 Variant/Object/Range - 将需要进一步的工作。我可以告诉你你的变体包含一个范围,我可以说它是一个变体,而不是一个范围:但我不能沿着声明链去揭示一个对象被声明为“对象”,因为它指向一个范围:

VarX 设置为等于范围对象变量: varX: type=8204 Range Dereferenced Type=9 rng1: type=8204 范围取消引用类型=16393
VarX 被设置为一个范围对象的值,一个二维数组: varX: type=8204 Variant() 取消引用类型=8204 arr1: type=8204 Variant() 取消引用类型=8204
数组变量被擦除为 Empty()。检查 varX: varX: type=8204 Variant() 取消引用类型=8204 arr1: type=8204 Variant() 取消引用类型=8204
VarX 设置为等于一个“对象”变量,该变量已设置为一个范围: varX: type=8204 Range Dereferenced Type=9 obj1:类型=8204 范围取消引用类型=16393

这是生成该代码的代码,以及完整的输出:


Public Sub TestVar()
Dim varX As Variant Dim str1 As String Dim lng1 As Long Dim rng1 As Excel.Range Dim arr1 As Variant Dim obj1 As Object
Debug.Print "Uninitialised:" Debug.Print Debug.Print vbTab & "varX: type=" & VarType(varX) & vbTab & vbTab & TypeName(varX) & vbTab & "Dereferenced Type=" & DereferencedType(varX) Debug.Print vbTab & "str1: type=" & VarType(str1) & vbTab & vbTab & TypeName(str1) & vbTab & "Dereferenced Type=" & DereferencedType(str1) Debug.Print vbTab & "lng1: type=" & VarType(lng1) & vbTab & vbTab & TypeName(lng1) & vbTab & "Dereferenced Type=" & DereferencedType(lng1) Debug.Print
varX = "One Hundred" str1 = "One Hundred" lng1 = 100 Debug.Print "varX and str1 are populated with the same literal:" Debug.Print Debug.Print vbTab & "varX: type=" & VarType(varX) & vbTab & vbTab & TypeName(varX) & vbTab & "Dereferenced Type=" & DereferencedType(varX) Debug.Print vbTab & "str1: type=" & VarType(str1) & vbTab & vbTab & TypeName(str1) & vbTab & "Dereferenced Type=" & DereferencedType(str1) Debug.Print vbTab & "lng1: type=" & VarType(lng1) & vbTab & vbTab & TypeName(lng1) & vbTab & "Dereferenced Type=" & DereferencedType(lng1) Debug.Print
varX = 100 lng1 = 100 Debug.Print "varX and lng1 are populated with the same integer:" Debug.Print Debug.Print vbTab & "varX: type=" & VarType(varX) & vbTab & vbTab & TypeName(varX) & vbTab & "Dereferenced Type=" & DereferencedType(varX) Debug.Print vbTab & "str1: type=" & VarType(str1) & vbTab & vbTab & TypeName(str1) & vbTab & "Dereferenced Type=" & DereferencedType(str1) Debug.Print vbTab & "lng1: type=" & VarType(lng1) & vbTab & vbTab & TypeName(lng1) & vbTab & "Dereferenced Type=" & DereferencedType(lng1) Debug.Print
varX = str1 Debug.Print "VarX is set equal to str1:" Debug.Print Debug.Print vbTab & "varX: type=" & VarType(varX) & vbTab & vbTab & TypeName(varX) & vbTab & "Dereferenced Type=" & DereferencedType(varX) Debug.Print vbTab & "str1: type=" & VarType(str1) & vbTab & vbTab & TypeName(str1) & vbTab & "Dereferenced Type=" & DereferencedType(str1) Debug.Print
varX = lng1 Debug.Print "VarX is set equal to lng1:" Debug.Print Debug.Print vbTab & "varX: type=" & VarType(varX) & vbTab & vbTab & TypeName(varX) & vbTab & "Dereferenced Type=" & DereferencedType(varX) Debug.Print vbTab & "lng1: type=" & VarType(lng1) & vbTab & vbTab & TypeName(lng1) & vbTab & "Dereferenced Type=" & DereferencedType(lng1) Debug.Print
Set varX = ActiveSheet.Range("A1:C3") Debug.Print "VarX is set equal to a range:" Debug.Print Debug.Print vbTab & "varX: type=" & VarType(varX) & vbTab & vbTab & TypeName(varX) & vbTab & "Dereferenced Type=" & DereferencedType(varX) Debug.Print
Set rng1 = ActiveSheet.Range("A1:C3") Set varX = Nothing Set varX = rng1 Debug.Print "VarX is set equal to a range object variable:" Debug.Print vbTab & "varX: type=" & VarType(varX) & vbTab & vbTab & TypeName(varX) & vbTab & "Dereferenced Type=" & DereferencedType(varX) Debug.Print vbTab & "rng1: type=" & VarType(rng1) & vbTab & vbTab & TypeName(rng1) & vbTab & "Dereferenced Type=" & DereferencedType(rng1) Debug.Print
arr1 = rng1.Value2 Set varX = Nothing varX = arr1 Debug.Print "VarX is set equal to a range object's value, a 2-dimensional array:" Debug.Print vbTab & "varX: type=" & VarType(varX) & vbTab & vbTab & TypeName(varX) & vbTab & "Dereferenced Type=" & DereferencedType(varX) Debug.Print vbTab & "arr1: type=" & VarType(rng1) & vbTab & vbTab & TypeName(arr1) & vbTab & "Dereferenced Type=" & DereferencedType(arr1) Debug.Print
Erase arr1 Debug.Print "The array variable is erased to Empty(). Inspect varX:" Debug.Print vbTab & "varX: type=" & VarType(varX) & vbTab & vbTab & TypeName(varX) & vbTab & "Dereferenced Type=" & DereferencedType(varX) Debug.Print vbTab & "arr1: type=" & VarType(rng1) & vbTab & vbTab & TypeName(arr1) & vbTab & "Dereferenced Type=" & DereferencedType(arr1) Debug.Print
Set obj1 = ActiveSheet.Range("A1:C3") Set varX = Nothing Set varX = obj1 Debug.Print "VarX is set equal to an 'object' variable, which has been set to a range:" Debug.Print vbTab & "varX: type=" & VarType(varX) & vbTab & vbTab & TypeName(varX) & vbTab & "Dereferenced Type=" & DereferencedType(varX) Debug.Print vbTab & "obj1: type=" & VarType(rng1) & vbTab & vbTab & TypeName(obj1) & vbTab & "Dereferenced Type=" & DereferencedType(obj1) Debug.Print
End Sub

结果:

未初始化: varX: type=0 Empty Dereferenced Type=0 str1: type=8 字符串取消引用类型=16392 lng1: type=3 长取消引用类型=16387
varX 和 str1 填充了相同的文字: varX: type=8 字符串取消引用类型=8 str1: type=8 字符串取消引用类型=16392 lng1: type=3 长取消引用类型=16387
varX 和 lng1 填充了相同的整数: varX: type=2 整数取消引用类型=2 str1: type=8 字符串取消引用类型=16392 lng1: type=3 长取消引用类型=16387
VarX 设置为等于 str1: varX: type=8 字符串取消引用类型=8 str1: type=8 字符串取消引用类型=16392
VarX 设置为等于 lng1: varX: type=3 Long Dereferenced Type=3 lng1: type=3 长取消引用类型=16387
VarX 设置为等于一个范围: varX: type=8204 Range Dereferenced Type=9
VarX 设置为等于范围对象变量: varX: type=8204 Range Dereferenced Type=9 rng1: type=8204 范围取消引用类型=16393
VarX 被设置为一个范围对象的值,一个二维数组: varX: type=8204 Variant() 取消引用类型=8204 arr1: type=8204 Variant() 取消引用类型=8204
数组变量被擦除为 Empty()。检查 varX: varX: type=8204 Variant() 取消引用类型=8204 arr1: type=8204 Variant() 取消引用类型=8204
VarX 设置为等于一个“对象”变量,该变量已设置为一个范围: varX: type=8204 Range Dereferenced Type=9 obj1:类型=8204 范围取消引用类型=16393

总而言之,一个有趣的问题:我的回答的简短版本是您可以消除变体和简单类型的歧义,但是声明为“对象”的对象不适合该分析。

【讨论】:

  • 令人印象深刻。我必须考虑一下才能完全理解它。您的回答似乎很好地解释了为什么这个问题比我想象的要难。我认为必须进行某种解析(除了取消引用)才能重现调试工具提供的那种信息。
  • 第一个代码块有错别字。 VariantType 应该是 DereferencedType
  • 虽然看起来很多有用的代码,但恕我直言,我认为它毕竟不是那么有用,基本上是我suggested earlier的一个例子。
  • 已编辑:答案现在具有完全实现的 VariantTypeName() 函数,可为您提供类型名称为“Variant/Long”或“Long”
【解决方案2】:

您有确定变量是否已经是Variant 的代码。现在您需要做的就是获取子类型,对吗?为此有一个内置函数:VarType

但它有局限性。它仅适用于本机类型。对于用户定义的类型,它总是返回 vbUserDefinedType (36)。虽然,我想你可以通过调用TypeName 来完成这项工作。

【讨论】:

  • 谢谢,但我真的不认为VarType 能达到我想要的效果,因为在发布的代码中我使用TypeName 来达到同样的目的。问题的关键是它无法区分例如一个保存范围的对象变量和一个保存范围的范围变量。我主要是试图重现本地窗口显示的类型信息。该信息必须在某处,但只能从 VarTypeTypeName 等函数中收集到部分信息
猜你喜欢
  • 1970-01-01
  • 2012-02-16
  • 2021-03-19
  • 1970-01-01
  • 2021-05-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-26
相关资源
最近更新 更多