VB6可以不声明数据类型,后期绑定,功率都是Variant
在 VB 中,有个很神奇的类型,它叫 Variant
。这孩子很特别,荤素不忌,我们似乎可以把任何一种类型的数据交给他。有很多刚刚接触 VB 的小伙伴,不喜欢声明变量,或者特别喜欢把所有变量都作为 Variant
处理,这样做很方便,但是,如果我告诉你,每一个 Variant
变量都要占用你 16 字节的内存,而这个占用量是 Long
类型的 4 倍,你还会肆无忌惮地用它吗?哈哈,现在硬件条件好了,我们平时写的程序体量又比较小,所以也不显得资源吃紧,但是,如果一个庞大的工程中,所有的变量、常量、函数、类的属性与方法都是 Variant
,你可以明显的感受到内存快被吃光了,而且程序还异常缓慢。那么,Microsoft 到底为什么要设计出这么个类型?它内部是如何实现的?为什么要占用那么多资源?今天,我们就来扒一扒 Variant
的前世今生。
Variant
类型的实质,是 C 语言的 VARIANT
结构体,其定义如下:
struct tagVARIANT {
union {
struct __tagVARIANT {
VARTYPE vt;
WORD wReserved1;
WORD wReserved2;
WORD wReserved3;
union {
ULONGLONG ullVal; /* VT_UI8 */
LONGLONG llVal; /* VT_I8 */
LONG lVal; /* VT_I4 */
BYTE bVal; /* VT_UI1 */
SHORT iVal; /* VT_I2 */
FLOAT fltVal; /* VT_R4 */
DOUBLE dblVal; /* VT_R8 */
VARIANT_BOOL boolVal; /* VT_BOOL */
_VARIANT_BOOL bool; /* (obsolete) */
SCODE scode; /* VT_ERROR */
CY cyVal; /* VT_CY */
DATE date; /* VT_DATE */
BSTR bstrVal; /* VT_BSTR */
IUnknown * punkVal; /* VT_UNKNOWN */
IDispatch * pdispVal; /* VT_DISPATCH */
SAFEARRAY * parray; /* VT_ARRAY */
BYTE * pbVal; /* VT_BYREF|VT_UI1 */
SHORT * piVal; /* VT_BYREF|VT_I2 */
LONG * plVal; /* VT_BYREF|VT_I4 */
LONGLONG * pllVal; /* VT_BYREF|VT_I8 */
FLOAT * pfltVal; /* VT_BYREF|VT_R4 */
DOUBLE * pdblVal; /* VT_BYREF|VT_R8 */
VARIANT_BOOL *pboolVal; /* VT_BYREF|VT_BOOL */
_VARIANT_BOOL *pbool; /* (obsolete) */
SCODE * pscode; /* VT_BYREF|VT_ERROR */
CY * pcyVal; /* VT_BYREF|VT_CY */
DATE * pdate; /* VT_BYREF|VT_DATE */
BSTR * pbstrVal; /* VT_BYREF|VT_BSTR */
IUnknown ** ppunkVal; /* VT_BYREF|VT_UNKNOWN */
IDispatch ** ppdispVal; /* VT_BYREF|VT_DISPATCH */
SAFEARRAY ** pparray; /* VT_BYREF|VT_ARRAY */
VARIANT * pvarVal; /* VT_BYREF|VT_VARIANT */
PVOID byref; /* Generic ByRef */
CHAR cVal; /* VT_I1 */
USHORT uiVal; /* VT_UI2 */
ULONG ulVal; /* VT_UI4 */
INT intVal; /* VT_INT */
UINT uintVal; /* VT_UINT */
DECIMAL * pdecVal; /* VT_BYREF|VT_DECIMAL */
CHAR * pcVal; /* VT_BYREF|VT_I1 */
USHORT * puiVal; /* VT_BYREF|VT_UI2 */
ULONG * pulVal; /* VT_BYREF|VT_UI4 */
ULONGLONG * pullVal; /* VT_BYREF|VT_UI8 */
INT * pintVal; /* VT_BYREF|VT_INT */
UINT * puintVal; /* VT_BYREF|VT_UINT */
struct __tagBRECORD {
PVOID pvRecord;
IRecordInfo * pRecInfo;
} __VARIANT_NAME_4; /* VT_RECORD */
} __VARIANT_NAME_3;
} __VARIANT_NAME_2;
DECIMAL decVal;
} __VARIANT_NAME_1;
};
且慢!有点吓人了。这个结构体也太长了吧?而且还有一堆不认识的字符,别急,让我们一点一点来。
首先,我们发现了一个叫 union
的关键字。这个东西叫 "共用体" (亦称 "联合体",以下不再重复),是 C 语言中的一种自定义类型,其形式满足:
union unionName{
typeName1 data1;
typeName2 data2;
......
}
与结构体 struct
不同的是,共用体的所有成员占据了相同一块内存,其体量为最大成员的体量,每当我们修改其中一个成员的数据时,所有成员的数据都会被覆盖。以下面这个共用体为例:
union rational{
int number;
float fraction;
double longFraction;
}
这是一个包含三个成员的共用体,其中 double
类型的 longFraction
是体量最大的成员,作为双精度浮点数,它要占据 8 字节的内存,所以 rational
的体量也是 8 字节。如果你不太明白我上面说的"覆盖"是什么意思,那下面这段代码能帮到你:
#include<iostream>
using namespace std;
int main(void){
union rational{
int number;
float fraction;
double longFraction;
} a;
a.longFraction = 1.11111;
cout << a.number << endl;
cout << a.fraction << endl;
cout << a.longFraction << endl;
}
发现了吗?union
之所以叫 共用 体,其本质就是对一块内存空间提出了不同的解读方式。我们都知道内存里的每个比特只能存 0 和 1,并且每 8 个比特打包在一起称为 1 个字节,每种数据类型都会占用若干字节的存储空间,而对这块存储空间采取不同的解读方法,就能得到不同的结果。比如,某 1 字节的内存中存储的数据是 1000 0100
,在 VB 中,当我们以 Byte
类型来解读它时,它是 132
,而当我们以 Boolean
类型解读它时,它便是 True
。C 语言的 union
就是给我们提供了一个手段,约定了一块内存允许有多少种不同的解读方式。
接下来,我们来看看 VARIANT
的成员,显然它只有一个成员:一个共用体。但是,这个共用体内部可太丰富了,首先是另一个结构体,它包含了 5 个成员,其中还有一个是共用体......套娃开始了。为了论述方便,我们把它拎出来看看:
struct __tagVARIANT {
VARTYPE vt;
WORD wReserved1;
WORD wReserved2;
WORD wReserved3;
union {
......
} __VARIANT_NAME_3;
} __VARIANT_NAME_2;
vt
成员的类型是 VARTYPE
,这是一个 2 字节的枚举类型,顾名思义,它标识了存储什么类型的数据,相当于 VBA.vbVarType
(但注意 VB 枚举类型是 4 字节)。其定义如下:
enum VARETYPE{
VT_EMPTY = 0, // VBA.vbVarType.vbEmpty
VT_NULL = 1, // VBA.vbVarType.vbNull
VT_I2 = 2, // VBA.vbVarType.vbInteger
VT_I4 = 3, // VBA.vbVarType.vbLong
VT_R4 = 4, // VBA.vbVarType.vbSingle
VT_R8 = 5, // VBA.vbVarType.vbDouble
VT_CY = 6, // VBA.vbVarType.vbCurrency
VT_DATE = 7, // VBA.vbVarType.vbDate
VT_BSTR = 8, // VBA.vbVarType.vbString
VT_DISPATCH = 9, // VBA.vbVarType.vbObject(包括 Nothing)
VT_ERROR = 10, // VBA.vbVarType.vbError
VT_BOOL = 11, // VBA.vbVarType.vbBoolean
VT_VARIANT = 12, // VBA.vbVarType.vbVariant
VT_UNKNOWN = 13,
VT_DECIMAL = 14, // VBA.vbVarType.vbDecimal
VT_I1 = 16,
VT_UI1 = 17, // VBA.vbVarType.vbByte
VT_UI2 = 18,
VT_UI4 = 19,
VT_I8 = 20,
VT_UI8 = 21,
VT_INT = 22,
VT_UINT = 23,
VT_VOID = 24,
VT_HRESULT = 25,
VT_PTR = 26,
VT_SAFEARRAY = 27,
VT_CARRAY = 28,
VT_USERDEFINED = 29,
VT_LPSTR = 30,
VT_LPWSTR = 31,
VT_RECORD = 36, // VBA.vbVarType.vbUserDefinedType
VT_INT_PTR = 37,
VT_UINT_PTR = 38,
VT_FILETIME = 64,
VT_BLOB = 65,
VT_STREAM = 66,
VT_STORAGE = 67,
VT_STREAMED_OBJECT = 68,
VT_STORED_OBJECT = 69,
VT_BLOB_OBJECT = 70,
VT_CF = 71,
VT_CLSID = 72,
VT_VERSIONED_STREAM = 73,
VT_BSTR_BLOB = 0xfff,
VT_VECTOR = 0x1000,
VT_ARRAY = 0x2000, // VBA.vbVarType.vbArray,这是一个组合值
// 例如 Integer 数组是 VT_ARRAY + VT_I2
VT_BYREF = 0x4000,
VT_RESERVED = 0x8000,
VT_ILLEGAL = 0xffff,
VT_ILLEGALMASKED = 0xfff,
VT_TYPEMASK = 0xfff
};
之后 3 个的 WORD
类型的成员从命名上看是留空了,这 6 字节的内存不投入使用。随后是一个异常庞大的共用体,它应该就是 Variant
能存储所有类型的基础,这个家伙几乎把所有可能出现的类型都列了出来。《MSDN》表示 Variant
数字类型的体量为 16 字节,我们大胆推测是 2 字节的 VARTYPE
,加上 6 字节的留空,再加上 8 字节的共用体。
下面我们来用代码证实上述猜想:
Dim a As Variant: a = CByte(132)
Dim vt As Integer
Dim wReserved1 As Integer
Dim wReserved2 As Integer
Dim wReserved3 As Integer
Dim data As Byte
Call CopyMemory(ByVal VarPtr(vt), ByVal VarPtr(a), 2)
Call CopyMemory(ByVal VarPtr(wReserved1), ByVal VarPtr(a) + 2, 2)
Call CopyMemory(ByVal VarPtr(wReserved2), ByVal VarPtr(a) + 4, 2)
Call CopyMemory(ByVal VarPtr(wReserved3), ByVal VarPtr(a) + 6, 2)
Call CopyMemory(ByVal VarPtr(data), ByVal VarPtr(a) + 8, 1)
Debug.Print "类型标识码为:" & vt & ",而 Byte 类型的标识码 VT_UI8 的值为 17"
Debug.Print "留空的 6 字节里应该不存在有效数据:"
Debug.Print wReserved1
Debug.Print wReserved2
Debug.Print wReserved3
Debug.Print "存储的 Byte 类型数据为:" & data
观察到,立即窗口输出了如下内容:
类型标识码为:17,而 Byte 类型的标识码 VT_UI8 的值为 17
留空的 6 字节里应该不存在有效数据:
0
0
0
存储的 Byte 类型数据为:132
这符合我们的预期。
楼神批注:
如果你获取过在下编写的PowerDebug
类,以下代码能更清晰地展示上述内容:
Dim a As Variant: a = CByte(132)
Call PowerDebug.ShowByte(VarPtr(a), 16, True)
到此,我们解决了 VARIANT
的第一个共用体成员,另一个是 DECIMAL
,这个家伙我们似乎也见过,还记得我们的隐藏类型 Decimal
吗?《MSDN》表示,Decimal
不能被 As
语句显式声明,它必须依附于 Variant
类型,使用 CDec()
函数转换而来。这是否正是它的原型呢?让我们来看看 C 语言中关于 DECIMAL
类型的定义:
typedef struct tagDEC{
USHORT wReserved; // 该值总是 VT_DECIMAL = 14
union{
struct{
char scale; // 浮点数的小数位数,0 - 28 之间的任意整数
char sign; // 有符号数的符号位,0 表示正数,1 表示负数
};
USHORT signscale;
};
ULONG Hi32; // 高 32 位
union{
struct{
#ifdef _MAC
ULONG Mid32;
ULONG Lo32;
#else
ULONG Lo32;
ULONG Mid32;
#endif
};
DWORDLONG Lo64; // 低 64 位
};
} DECIMAL;
显然,这完美契合了《MSDN》中关于 Decimal
类型的描述:
Decimal 类型:
Decimal
变量存储为 96 位(12 个字节)无符号的整型形式,并除以一个 10 的幂数。这个变比因子决定了小数点右面的数字位数,其范围从 0 到 28。变比因子为 0(没有小数位)的情形下,最大的可能值为 ±79,228,162,514,264,337,593,543,950,335。而在有 28 个小数位的情况下,最大值为 ±7.9228162514264337593543950335,而最小的非零值为 ±0.0000000000000000000000000001。
至此,VARIANT
已经完整地展示在我们面前。小 V 很体贴地把它封进了黑箱子,并且告诉我们有种类型叫 Variant
,你可以把任何东西放进去,不必关心它是怎么存储的,"放心吧,都交给我了!" 她如是说。但是,Variant
的效率是非常低下的,它方便了我们随心所欲的写代码,也需要我们付出更大的内存占用量和更长的响应时间。因此,如非必要,请避免隐式使用 Variant
!请使用类型声明语句明确地告诉小 V 你想要什么。时刻告诫自己,它只是初见之时,小 V 为了包容我们的无知做出的让步,但是,不要因她的温柔纵容自己的怠惰。当然,如果你实在神经大条,总是意识不到,也可以要求小 V 对你严厉一点,她为我们提供了这个选项 ------ Option Explicit
!