2015-11-26 17 views
59

Entro la piena tipo di una variabile intendo il tipo di informazioni che si ottiene nella finestra immediata:Determinare il tipo completo di una variabile

enter image description here

vorrei per determinare le informazioni sul tipo utilizzare dinamicamente VBA. La funzione TypeName() non fa ciò che voglio in quanto restituisce il sottotipo di una variante e non fa distinzione tra ad es. una variabile variante che contiene un intervallo, una variabile oggetto che contiene un intervallo e una variabile di intervallo che mantiene un intervallo.

Come fase preliminare, ho scritto una funzione che rileva se viene passata una variante. Funziona sfruttando la semantica pass-by-reference. Il codice fa le cose con il suo argomento che può essere fatto solo con una variante e sarà quindi attivano un errore se la variabile passata non è in realtà una variante:

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 

Sulla base di questo, ho scritto:

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

Testing:

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 

uscita:

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

Questo è quasi ciò che voglio, ma non fa distinzione tra una variabile oggetto che contiene un intervallo e una variabile di intervallo che contiene un intervallo. Ho cercato di scrivere una funzione chiamata IsTypeObject che funziona in modo simile a IsVariant ma non riesco a farlo funzionare:

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 

prova:

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

Ma questa stampe True anche se lo farei pensare che la stessa semantica pass-by-reference che fa funzionare IsVariant dovrebbe anche fare funzionare IsTypeObject (non è possibile assegnare una collezione a un intervallo). Ho provato varie modifiche ma non riesco a distinguere tra le variabili oggetto generiche e le variabili oggetto specifiche come le variabili intervallo.

Quindi, qualche idea su come ottenere dinamicamente il tipo completo di una variabile? (La motivazione è come parte di un'utilità di debug-log)

+0

Il pass-by-reference della variante funziona perché la struttura 'VARIANT' [ha il flag' VT_BYREF'] (http://stackoverflow.com/a/31637346/11683) che può o non può essere impostato. Non c'è niente di simile per le variabili non Variant. Hai una riflessione in. NET, ma non in VBA. – GSerg

+0

@GSerg La cosa strana è, anche nel caso del mio "successo" 'IsVariant', quando gli passo una variabile' Range' e guardo dove si trova l'errore reale passandolo attraverso il debugger, la riga 'Set var = New Collection' in realtà non fa scattare l'errore - la riga successiva quando provo ad assegnare una stringa ad esso è ciò che causa l'errore che sto intrappolando. Sto pensando che VBA debba implementare il passaggio per riferimento tramite una sorta di metodo di ripristino della copia - ma poi diventa strano che la riga successiva causi un errore (al contrario di una mancata corrispondenza di tipo al reso). –

+3

Questo perché quando un oggetto viene utilizzato in un contesto di valori, VB prova a utilizzare la [proprietà predefinita] (http://stackoverflow.com/a/19200523/11683) dell'oggetto.A patto che 'var' sia un oggetto,' var = "test" 'prova ad assegnare' "test" 'alla proprietà predefinita di' var', e non ne ha uno (errore 438, che non è un errore di mancata corrispondenza di tipo). – GSerg

risposta

10

Hai il codice che determina se la variabile è già Variant. Ora tutto ciò che devi fare è ottenere il sottotipo, giusto? Esiste una funzione integrata esattamente per questo: VarType.

Tuttavia ha delle limitazioni. Funziona solo per i tipi nativi. Restituisce sempre vbUserDefinedType (36) per i tipi definiti dall'utente. Anche se, suppongo che potresti avere un caso speciale con una chiamata allo TypeName per terminare il lavoro.

+1

Grazie, ma non penso davvero che il 'VarType' fa quello che volevo dato che nel codice postato ho usato' TypeName' per gli stessi scopi. Il punto della domanda è che non è stato in grado di distinguere tra ad es. una variabile oggetto che contiene un intervallo e una variabile di intervallo che contiene un intervallo. Stavo principalmente cercando di riprodurre le informazioni sul tipo visualizzate dalla finestra dei locali. Queste informazioni devono essere da qualche parte, ma solo una parte di queste informazioni può essere ricavata da funzioni come 'VarType' o' TypeName' –

13

Sì, lo puoi fare: richiede una minima conoscenza dei puntatori e del concetto di "dereferenziazione" ...

Ecco il codice per farlo:

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

(dichiarazioni per la funzione API CopyMemory sono pochi paragrafi più in basso).

Ciò richiede alcune spiegazioni, perché la famiglia di linguaggi di Visual Basic è progettata per proteggerti dai dettagli di implementazione delle variabili e dei loro tipi - e dal concetto di puntatori in particolare - e il mio codice comporta un po 'di pensiero laterale .

In termini semplici, le variabili hanno un nome: una stringa come "intX" che vedi nel tuo codice; un'area della memoria allocata per contenere i dati effettivi; e un indirizzo per quella memoria.

Tale indirizzo sarà effettivamente per l'inizio della memoria allocata alla variabile, e la variabile sarà implementato come una struttura in memoria definita da offset ai dati effettivi, con le dimensioni (o lunghezza) di dati - e, per i tipi complessi, da offset a indirizzi ad altre strutture in memoria. Quelle dimensioni e offset sono predefinite: sono l'effettiva implementazione della variabile, e noi sviluppatori VBA raramente abbiamo bisogno di saperlo - dichiariamo il tipo e tutto è fatto per noi.

La prima cosa che devi sapere oggi è che i primi due byte all'indirizzo di una variabile in VBA sono the enumerated var type: è così che funziona la funzione VarType().

Quando il programma passa quell'indirizzo, invece di passare un'assegnazione copiata dei dati in memoria, passa quell'indirizzo come puntatore . Sì, sto semplificando un po 'questo, ma gli sviluppatori VBA conoscono la differenza tra ottenere un puntatore e una copia dei dati: è negli identificatori ByRef e ByVal che usiamo per i parametri in entrata quando dichiariamo una funzione.

VBA e VB sono molto, molto buono ci protegge dai dettagli: così buono, che non possiamo usare VarType e TypeName per rilevare che abbiamo superato un valore o un riferimento ad esso; o anche un riferimento a un riferimento, a un riferimento.

Questo è importante, perché una variante è un wrapper per altre variabili e la struttura fornisce un puntatore alla variabile che contiene con il tipo var per descriverlo: tuttavia, non abbiamo modo di saperlo in VBA - passiamo direttamente lungo la linea indicata dall'indirizzo, fino ai dati che useremo, e il VBA varType non ci dice mai che siamo andati lì indirettamente da diversi salti attraverso gli indirizzi successivi definiti dai puntatori.

Tuttavia, tale informazione esiste, se si è pronti a guardare quei due byte dietro il puntatore utilizzando una chiamata API.

Come detto, queste due byte contengono il tipo var - ma non solo: contengono il tipo var associati ad un marker bit VT_BYREF indicando che questo è un riferimento o puntatore al tipo di dati memorizzati, e non il dati stessi Quindi questo codice in modo affidabile vi dirà il tipo di var, con un po 'di pensiero laterale per superare VBA essendo utile quando preferiamo non era:

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
A prima vista, questa funzione sembra essere autodifesa: sto passando la variabile - variante o tipo semplice - come riferimento, quindi sarà sempre combinata con VT_BYREF.E ho comunque commentato l'aritmetica del 'modulo' ...

... Ed è così che funziona: passala una variabile semplice, e ti dirà che hai passato la variabile per riferimento:

Dim str1 As String 
str1 = "One Hundred" 
Debug.Print "String Variable: " & DereferencedType(str1) 
... e si ottiene l'output vbString OR VT_BYREF:

String Variable: 16392 

Ma se si passa la nostra funzione una variante di stringa, l'implementazione di VBA della variante sarà proteggervi da tutto ciò che la complessità dei puntatori e il passaggio per riferimento - fino ai dati - e darti ur dati con tutte le informazioni indesiderate spogliato fuori:

Dim varX As Variant 
varX = "One Hundred" 
Debug.Print "String Variant: " & DereferencedType(varX) 
... e si ottiene l'output:

String Variant: 8 

vi lascio al codice di un OR o NOT sui valori restituiti con VT_BYREF , per darti l'etichetta 'Variant /' per i descrittori di stringa espansi delle uscite Variant/String e Variant/Long.

[Edit: fatto questo, è in cima alla risposta, implementato come VariantTypeName]

Raccomando che si dichiara la chiamata API CopyMemory come mostrato, con le costanti di compilazione condizionale per tutti gli ambienti è molto probabile per incontrare:

 
#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

Nel frattempo, la domanda più difficile - ottenere Variant/Object/Range - avrà bisogno di ulteriore lavoro. Posso dirti che la tua variante contiene un intervallo, e posso dire che è una variante e non è di per sé un intervallo: ma non posso scendere la catena di dichiarazioni per rivelare che un oggetto è stato dichiarato come "oggetto" ora che punta a un intervallo:

 
VarX is set equal to a range object variable: 
    varX: type=8204  Range Dereferenced Type=9 
    rng1: type=8204  Range Dereferenced Type=16393
VarX is set equal to a range object's value, a 2-dimensional array: varX: type=8204 Variant() Dereferenced Type=8204 arr1: type=8204 Variant() Dereferenced Type=8204
The array variable is erased to Empty(). Inspect varX: varX: type=8204 Variant() Dereferenced Type=8204 arr1: type=8204 Variant() Dereferenced Type=8204
VarX is set equal to an 'object' variable, which has been set to a range: varX: type=8204 Range Dereferenced Type=9 obj1: type=8204 Range Dereferenced Type=16393

Ecco il codice che ha generato questo, e l'output completo:

 
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

Risultati:

 
Uninitialised: 
    varX: type=0  Empty Dereferenced Type=0 
    str1: type=8  String Dereferenced Type=16392 
    lng1: type=3  Long Dereferenced Type=16387
varX and str1 are populated with the same literal: varX: type=8 String Dereferenced Type=8 str1: type=8 String Dereferenced Type=16392 lng1: type=3 Long Dereferenced Type=16387
varX and lng1 are populated with the same integer: varX: type=2 Integer Dereferenced Type=2 str1: type=8 String Dereferenced Type=16392 lng1: type=3 Long Dereferenced Type=16387
VarX is set equal to str1: varX: type=8 String Dereferenced Type=8 str1: type=8 String Dereferenced Type=16392
VarX is set equal to lng1: varX: type=3 Long Dereferenced Type=3 lng1: type=3 Long Dereferenced Type=16387
VarX is set equal to a range: varX: type=8204 Range Dereferenced Type=9
VarX is set equal to a range object variable: varX: type=8204 Range Dereferenced Type=9 rng1: type=8204 Range Dereferenced Type=16393
VarX is set equal to a range object's value, a 2-dimensional array: varX: type=8204 Variant() Dereferenced Type=8204 arr1: type=8204 Variant() Dereferenced Type=8204
The array variable is erased to Empty(). Inspect varX: varX: type=8204 Variant() Dereferenced Type=8204 arr1: type=8204 Variant() Dereferenced Type=8204
VarX is set equal to an 'object' variable, which has been set to a range: varX: type=8204 Range Dereferenced Type=9 obj1: type=8204 Range Dereferenced Type=16393

Tutto sommato, una domanda interessante: la versione breve della mia risposta è che è possibile disambiguare varianti e tipi semplici, ma un oggetto dichiarato come "oggetto" non è suscettibile di tale analisi.

+1

Impressionante. Devo pensarci un po 'per capirlo appieno. Sembra che la tua risposta sia una buona spiegazione del perché la domanda sia più difficile di quanto pensassi. Penso che una sorta di analisi (oltre al dereferenziamento) debba essere eseguita per riprodurre il tipo di informazioni fornite dagli strumenti di debug. –

+1

C'è un errore di battitura nel primo blocco di codice. 'VariantType' dovrebbe essere' DereferencedType '. – BrakNicku

+0

Grazie @BrakNicku - lo verificherà e lo correggerà –

Problemi correlati