调用方如何知道 VARIANT 中何时有十进制?

How does the caller know when there's a Decimal inside a VARIANT?

本文关键字:何时 十进制 VARIANT 方如何 调用      更新时间:2023-10-16

COMVARIANT类型使用tagVARIANT结构定义,如下所示:

typedef struct tagVARIANT {
union {
struct {
VARTYPE vt;
WORD    wReserved1;
WORD    wReserved2;
WORD    wReserved3;
union {
LONGLONG     llVal;
LONG         lVal;
BYTE         bVal;
SHORT        iVal;
FLOAT        fltVal;
DOUBLE       dblVal;
VARIANT_BOOL boolVal;
VARIANT_BOOL __OBSOLETE__VARIANT_BOOL;
SCODE        scode;
CY           cyVal;
DATE         date;
BSTR         bstrVal;
IUnknown     *punkVal;
IDispatch    *pdispVal;
SAFEARRAY    *parray;
BYTE         *pbVal;
SHORT        *piVal;
LONG         *plVal;
LONGLONG     *pllVal;
FLOAT        *pfltVal;
DOUBLE       *pdblVal;
VARIANT_BOOL *pboolVal;
VARIANT_BOOL *__OBSOLETE__VARIANT_PBOOL;
SCODE        *pscode;
CY           *pcyVal;
DATE         *pdate;
BSTR         *pbstrVal;
IUnknown     **ppunkVal;
IDispatch    **ppdispVal;
SAFEARRAY    **pparray;
VARIANT      *pvarVal;
PVOID        byref;
CHAR         cVal;
USHORT       uiVal;
ULONG        ulVal;
ULONGLONG    ullVal;
INT          intVal;
UINT         uintVal;
DECIMAL      *pdecVal;
CHAR         *pcVal;
USHORT       *puiVal;
ULONG        *pulVal;
ULONGLONG    *pullVal;
INT          *pintVal;
UINT         *puintVal;
struct {
PVOID       pvRecord;
IRecordInfo *pRecInfo;
} __VARIANT_NAME_4;
} __VARIANT_NAME_3;
} __VARIANT_NAME_2;
DECIMAL decVal;
} __VARIANT_NAME_1;
} VARIANT;

通常,当调用者想要使用Variant内部的数据时,它会使用VARTYPE vt标志来查看存储了什么类型的数据,以及最终应该如何解释这些1和0。

DECIMAL存储在Variant中时会发生什么;该定义位于包含vtstruct之外,那么调用方如何确定是存在有效的类型标志还是只有Decimal的一些字节?Decimal需要12*14个字节来存储,Variant可以容纳16个字节,所以可能会利用这些信息,但存储在联合的较小成员的备用2个字节中的信息不是未定义的行为吗?

这是一个有趣的问题。遗憾的是,我一直找不到任何关于这方面的确切文件。我可以从一些思考和实验中做出一些推论。

尽管有官方文档和标头中的类型定义,存储在VARIANT中的DECIMAL似乎确实使用了DECIMALwReserved成员的字节作为重叠的vtVARIANT成员。因此,通过查看vt成员,VARIANT中的DECIMAL以与任何其他VARIANT类型相同的方式进行标识。

我提出了两个经验证明。

1) 我编译了一个VB6程序,将DECIMAL存储在VARIANT(本机代码,无优化,生成符号调试信息)中。然后我使用旧版本的WinDbg来检查变量的位(当前版本的WinDbg与VB6的旧PDB格式不兼容-我想我本可以尝试使用VC6来代替它,但没有考虑它)。

Dim v As Variant
v = CDec(24)

用WinDbg检查v,我获得了v变量的以下布局:

0e 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00 
----- ----- ----------- -----------------------
|     |        |                 |
|     |        |                Lo64
|     |       Hi32
|   signscale
wReserved
(but note it's the same as v.vt == VT_DECIMAL)       

好吧,VB6并没有在奇怪的地方作弊,微软不将Decimal公开为完整类型似乎总是很奇怪(出于某种原因,你不能在VB6中声明Decimal类型的变量;它必须存储在Variant中。Dim的文档听起来像是他们打算支持Decimal,但出于某种原因不得不将其删除)。所以这可能只是一个VB6作弊。但是:

2) 我测试了COM API,看看如果我要求它在VARIANT中放入DECIMAL会做什么。对于踢球,我使用VC6++来测试这个:

VARIANT s;
VARIANT t;
VariantInit(&s);
VariantInit(&t);
V_VT(&s) = VT_I4;
V_I4(&s) = 24;
HRESULT hr = VariantChangeType(&t, &s, 0, VT_DECIMAL);

我确认hr就是S_OK。如果在VARIANT中存储DECIMAL by值在形式上是非法的,我会期望出现错误HRESULT。相反,布局与我使用VB6:的经验相匹配

  • 观察窗口将t的值报告为{24 VT_DECIMAL}
  • t.vt成员设置为14(即VT_DECIMAL)
  • t.decVal成员被列为wReserved==14;Lo64==24;Hi32==0

因此,不管VARIANT的标头声明意味着什么,vt成员都可以并且应该用于确定VARIANT何时包含DECIMAL。事实上,如果您从未详细检查过VARIANT的声明,您永远不会知道DECIMAL被区别对待。


我剩下的问题是"为什么不像其他人一样让DECIMAL适合工会呢?"。

如果不了解VARIANT和DECIMAL的完整历史,可能很难得出完整的答案;但密钥可能不在vt中而是在wReserved1wReserved2wReserved3中。

DECIMAL似乎是后来添加到VARIANT中的。Kraig Brockschmidt的经典著作"Inside Ole"(1995年第2版)给出了VARIANT的声明,但没有提到DECIMAL作为选项之一。这意味着DECIMAL作为VARIANT选项是在之后的某个时刻添加的。在Visual C++6(1998)之前,DECIMAL已经作为VARIANT类型提供。

但是DECIMAL中有趣的部分(14字节)太大,无法容纳预先存在的VARIANT并集。DECIMAL需要使用三个wReservedX字段占用的字节(可能最初用作填充)。我敢肯定,在不改变内存布局和破坏旧二进制文件的情况下,微软不可能重新定义VARIANT并集,使保留字段可用于并集和DECIMAL。

因此,有一种理论认为,微软需要将这种新的14字节长的类型添加到VARIANT中,而VARIANT不可能容纳联盟可用的8字节。根据这一理论,VARIANT的当前布局将是一种在二进制级别潜入DECIMAL而不破坏VARIANT原始声明的方式。编译时,DECIMAL只是"并集"的另一个成员,只是它可能溢出到保留的WORD的空间中。

可能还有另外一个怪癖。Hans Passant在上面的评论中提到,保留字段用于包含货币类型信息。这听起来很可行,但我无法证实,因为我没有发现任何关于DECIMAL旧用法的信息。假设这是真的,微软将在先前存在的DECIMAL类型的布局上受到限制(即,不可能考虑牺牲范围来使其适合作为传统成员)。此外,他们必须决定可以放弃"货币类型"信息,以换取DECIMAL在VARIANT中工作(或者他们可能已经提前放弃了货币类型信息,或者出于其他原因)。如果没有更多关于DECIMAL在被添加为VARIANT类型之前是如何使用的信息,我就无法判断。

如果它有助于解释Decimal数据类型中的Decimal类型,还可以在链接中使用一些方便的Decimals转换函数。

变体类型中的十进制类型的结构。

Public Type DecimalType     ' (when sitting in a Variant)
vt As Integer           ' Reserved, to act as the variable Type when sitting in a 16-Byte-Variant.  Equals vbDecimal(14) when it's a Decimal type.
Base10NegExp As Byte    ' Base 10 exponent (0 to 28), moving decimal to right (smaller numbers) as this value goes higher.  Top three bits are never used.
Sign As Byte            ' Sign bit only (high bit).  Other bits aren't used.
Hi32 As Long            ' Mantissa.
Lo32 As Long            ' Mantissa.
Mid32 As Long           ' Mantissa.
End Type

链接中关于十进制LenB的一句有趣的话:

变量中的小数完全包含在变量中,14Decimal数据为字节,Variant为2字节以指示它持有Decimal类型。因此,对于变量/小数。

然而,这里引用了MSDN的一句话:

如果varname是Variant,Len将其视为String,并且始终返回它包含的字符数。这是给Len的函数,但它对LenB函数的帮助也加倍。所以,LenB在告诉你字节数。

编辑1:我没有测试,但我敢打赌,如果变量(包含十进制)在UDT中,它只计算16个字节。

在阅读上述内容之前,Decimal类型的LenB让我有点困惑。