vb6变体数据类型,Variant 类型的实质

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

相关推荐
懒大王爱吃狼27 分钟前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
秃头佛爷1 小时前
Python学习大纲总结及注意事项
开发语言·python·学习
待磨的钝刨2 小时前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
△曉風殘月〆3 小时前
WPF MVVM入门系列教程(二、依赖属性)
c#·wpf·mvvm
XiaoLeisj3 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师4 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
逐·風4 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
捕鲸叉5 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer5 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq5 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端