65.9K
CodeProject 正在变化。 阅读更多。
Home

一个简单的类来封装 VARIANT

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.39/5 (28投票s)

2004年3月19日

CPOL

13分钟阅读

viewsIcon

202639

downloadIcon

2211

在 C++ 代码中使用 Variants。

引言

最近我一直在处理 MSHTML 控件,它的大部分参数都是 `VARIANT` 类型。我都能想象你已经皱起了眉头。一个体面的 C++ 程序员怎么会去弄那些 VB/脚本语言的数据类型呢?嗯,这是两害相权取其轻。要么我学习 `VARIANT`,要么我写我自己的 HTML 解析器和编辑器。你觉得哪个更容易?我知道你会同意我的 :)

老实说,我认为 `VARIANT` 的概念其实很酷。将你的数据封装在一个漂亮的小包里,带上一两个类型描述符,将它跨函数调用边界传递,让另一端去处理。如果处理得当,它可以解决许多原本棘手的问题。我只是希望它们更容易使用!

好了,道歉的话就说到这里。让我们来看看 `VARIANT` 是什么。

为什么需要 VARIANT?

与 C++ 这种强类型语言相比,Visual Basic 和许多脚本语言都是弱类型的。这意味着在强类型语言中,你必须将函数编写时期望的确切类型的参数传递给它。如果函数期望一个指向 `string` 的指针,你就不能传递一个整数。尝试这样做会得到一个编译时错误。

弱类型语言允许你传递不匹配预期类型的参数。那么问题就来了——如果你可以向函数传递错误的参数类型,语言会如何响应?大多数弱类型语言会“强制转换”(coerce)传入的值为预期的类型。这是什么意思?这意味着语言运行时会尝试将传入的数据转换为正确的数据类型。例如,如果你将一个整数传递给一个期望 `string` 的函数,最自然的“强制转换”是将整数转换为 `string` 表示形式。如果将日期传递给期望 `string` 的地方,自然的“强制转换”是将其转换为 `string` 表示形式。

作为 C++ 程序员,我们已经在小范围内习惯了强制转换——我们习惯了编译器可以将 `short` 提升为 `int` 等等。弱类型语言只是在这方面做得更进一步。

那么,这与 `VARIANT` 有什么关系呢?

想象一下你在设计自己的编程语言。你知道你想支持哪些数据类型。你知道你想要哪些内置运算符。你可以设计你的编译器来跟踪程序中所有数据的类型,这样当程序员将错误的数据类型传递给函数时,你的编译器就会知道,并可以插入必要的代码来转换数据。

现在想象一下,你不仅需要支持自己的语言,还需要支持另一种语言(例如 C++)。你完全控制自己的语言,但对第二种语言完全没有控制。然而,你希望能够与该语言进行互操作。既然是你想要与你无法改变的东西进行互操作,那么你就必须适应“你无法改变的东西”。因此,你设计你的数据类型,使其包含足够的信息,除了它封装的数据之外,还能让其他人解读其内容。

引出 VARIANT

`VARIANT` 是一种解决这个问题的、并非过于复杂的方式。简化来说,`VARIANT` 看起来是这样的

struct tagVARIANT
{
    union 
    {
        VARTYPE vt;
        WORD wReserved1;
        WORD wReserved2;
        WORD wReserved3;
        union 
        {
            LONG lVal;
            BYTE bVal;
            SHORT iVal;
            FLOAT fltVal;
            DOUBLE dblVal;
            VARIANT_BOOL boolVal;
            DATE date;
            BSTR bstrVal;
            SAFEARRAY *parray;
            VARIANT *pvarVal;
        };
    };
};

这是 `VARIANT` 定义的一个**非常**简化的版本,你可以在你最近的 `oaidl.h` 文件中找到完整的定义。我不知道 `wReserved` 值是什么意思,我也不关心。

我们感兴趣的是 `vt` 值和联合体(union)。`vt` 是 `valuetype`(值类型),联合体(union)是值。你会看到联合体包含了 `LONG`、`BYTE`、`SHORT`、`FLOAT` 等(有很多很多)。`vt` 告诉我们如何解释这个值,使用成员名称。在 C++ 中,你可以这样做:

void SomeFunc(VARIANT& v)
{
    USES_CONVERSION;

    if (v.vt == VT_I4)
        printf(_T("variant value is %d\n"), v.lVal);
    else if (v.vt == VT_BSTR)
        printf(_T("variant value is %s\n"), W2A(v.bstrVal));
}

这会检查 `VARIANT` 的 `vt` 成员。如果它是 `VT_I4`,那么我们想要的数据就包含在联合体的 `lVal` 成员中。由于 `lVal` 成员是一个 `LONG`,我们可以在 `printf` 调用中使用 `%d` 作为格式说明符。如果它是 `VT_BSTR`,那么数据就是包含在联合体的 `bstrVal` 成员中的 `BSTR`。

请注意 `VARIANT` 如何使用 `BSTR` 数据类型来传递 `string` 数据。这样做是为了让 `VARIANT` 能够在进程边界上传递,而无需进行封送(marshalling)开销。还有许多其他数据类型(本文未讨论)需要封送才能跨越进程边界,但 `string` 的传递非常普遍,使用 `BSTR` 来规避封送是一个很好的优化。

将 VARIANT 封装在一个简单的类中

基于我们之前看到的代码片段,将 `VARIANT` 的复杂细节隐藏在一个类中是很有意义的。我们可以这样做:

class CVariant : public VARIANT
{
public:
                CVariant();
                CVariant(int iValue);
                CVariant(LPCTSTR szValue);

    LPCTSTR     ToString() const;
    int         ToInt() const;
};

例如,`CVariant(int iValue)` 重载构造函数的实现可能看起来像这样:

CVariant::CVariant(int iValue)
{
    vt = VT_I4;
    lVal = iValue;
}

而 `ToString()` 函数的实现可能看起来像这样:

LPCTSTR CVariant::ToString() const
{
    USES_CONVERSION;

    if (VT_BSTR == vt)
        return W2A(bstrVal);

    //  It's not a string so return an empty string
    return _T("");
}

这通过隐藏处理 `VARIANT` 类型或转换其内容等复杂细节来稍微简化了代码,但这还不足以创建一个新类,更不用说写一篇文章来介绍了。

将 VARIANT 封装在一个更复杂的类中

上面显示的简单类可能足以满足大多数偶然的 `VARIANT` 使用。对于我在引言中提到的 MSHTML 控件来说,它绝对是足够的。但对于其他环境来说,它可能不够。例如,几年前,我编写了大量使用 Microsoft Chat Protocol 控件的软件,该控件似乎是由一个成员们只懂 VB 的委员会设计的。主机和控件之间传递的几乎所有数据都通过 `VARIANT`,其中一些 `VARIANT` 是数组。`VARIANT` 使用 `SAFEARRAY` 结构来表示数组。

`SAFEARRAY` 的定义看起来是这样的(这是 Win32 定义——对于 WinCE 来说稍微有点不同)。

typedef struct tagSAFEARRAY
{
    USHORT cDims;     // How many dimensions in this array
    USHORT fFeatures; // Allocation control flags
    ULONG cbElements; // The size of each array element
    ULONG cLocks;     // Array lock count.
    PVOID pvData;     // Points at the data in the array
    SAFEARRAYBOUND rgsabound[1];
} SAFEARRAY;

你一定会喜欢 `SAFEARRAYBOUND` 成员的用途。它是一个结构,用于指定此维度的元素数量和下界。这允许 `SAFEARRAY` 的某个维度的索引可以从任意数字开始,而不是我们 C/C++ 程序员所熟悉的 0。有一个这些结构的数组,每个 `cDim` 对应一个。

因此,在 C++ 中访问 `VARIANT` 数组涉及解释 `VARIANT` 的内容为指向 `SAFEARRAY` 的指针,将第一个数组索引与 `cDims` 进行验证,以确保它在范围内,然后根据 `cbElements` 的大小索引到 `pvData`,并考虑 `rgsabound` 数组中该索引项的内容。呼,真是说来话长!

突然之间,似乎一个封装这些东西的类可能会很有用。

类本身

注意事项

这里呈现的类并非涵盖了所有可能性;远非如此。它涵盖了我使用 Microsoft Chat Protocol 控件和 MSHTML 控件时遇到的情况。我怀疑 Visual Basic 中处理 `VARIANT` 类型所有可能性的代码比这里呈现的类要复杂得多。

这个类可以处理带有符号整数数据类型或 `string` 的简单 `VARIANT`。它还可以处理一维数组,其中数组的每个元素都是一个 `VARIANT`,该 `VARIANT` 可以是类处理的任何简单类型。如果你想要更多,可以按照代码查看如何处理额外的类型。我不需要超出支持范围的类型,所以我没有为这些类型编写支持。

好了,免责声明就到这里。这是类的头文件:

class CVariant : public VARIANT
{
public:
                    CVariant();
                    CVariant(bool bValue);
                    CVariant(int nValue);
                    CVariant(LPCTSTR szValue);
                    CVariant(VARIANT *pV);
                    CVariant(int lBound, int iElementCount);
                    ~CVariant(void);

//  Attributes
    BOOL            IsArray(int iElement = 0);
    BOOL            IsString(int iElement = 0);
    BOOL            IsInt(int iElement = 0);
    BOOL            IsBool(int iElement = 0);

//  Conversions
    VARIANT         *operator&()        { return this; }

//  Get operations
    VARIANT         *ElementAt(int iElement = 0);

    CString         ToString(int iElement = 0);
    int             ToInt(int iElement = 0);
    BOOL            ToBool(int iElement = 0);

//  Set operations
    void            Set(LPCTSTR szString, int iElement = 0);
    void            Set(int iValue, int iElement = 0);
    void            Set(bool bValue, int iElement = 0);
};

你已经看到了简单的构造函数。还有两个其他构造函数。第一个构造函数允许你定义一个数组。它接受索引的下界和一个元素的数量。代码如下:

CVariant::CVariant(int lBound, int iElementCount)
{
    //  Set the type to an array of variants...
    vt = VT_ARRAY | VT_VARIANT;
    parray = new SAFEARRAY;

    //  We only support 1 dimensional arrays..
    parray->cDims = 1;
    parray->fFeatures = FADF_VARIANT | FADF_HAVEVARTYPE | FADF_FIXEDSIZE | FADF_STATIC;
    parray->cbElements = sizeof(VARIANT);
    parray->cLocks = 0;

    //  Allocate the array of variants we point to...
    parray->pvData = new VARIANT[iElementCount];
    memset(parray->pvData, 0, sizeof(VARIANT) * iElementCount);
    parray->rgsabound[0].lLbound = lBound;
    parray->rgsabound[0].cElements = iElementCount;
}

根据我之前对 `SAFEARRAY` 结构的描述,这应该很清楚了。我们只支持一维数组,所以我们将新创建的 `SAFEARRAY` 实例的各种成员设置为反映这一点。新的 `SAFEARRAY` 的 `rgsabound[0]` 结构用我们的下界和计数变量设置。重要的是要记住,我们正在创建的 `VARIANT` 可能用于与用其他语言创建的模块进行互操作,而我们不能假设索引从 `0` 开始。你的索引从哪里开始取决于你正在与之互操作的内容。

`fFeatures` 成员需要一些解释。我使用的标志值指定数组包含固定大小的 `VARIANT`,并且是静态的(不在栈上创建)。我指定它是静态的,因为如果我需要分配内存,我将从堆上分配。

另一个构造函数允许你获取一个现有的 `VARIANT`(可能是传递给某个你正在托管的外部对象的事件处理程序)并将其附加到一个 `CVariant`。代码如下:

CVariant::CVariant(VARIANT *pV)
{
    //  Validate the input (and make sure it's writeable)
    ASSERT(pV);
    ASSERT(AfxIsValidAddress(pV, sizeof(VARIANT), TRUE));

    vt = VT_VARIANT;
    pvarVal = pV;
}

如果是调试版本,我们会做一些断言来确保它是一个指向有效内存块的指针,至少足够大,能够容纳一个 `VARIANT`。我们无法进行更多的运行时验证。一旦我们确定它**可能**是一个 `VARIANT`,我们就将指针赋值给 `pvarVal` 成员,并将类型设置为 `VT_VARIANT`。完成之后,我们就可以像自己创建它一样使用 `VARIANT` 的任何其他成员函数了。

警告 警告 警告

听好了。**绝不要**使用 `CVariant::CVariant(VARIANT *pV)` 构造函数来尝试在函数边界之间保留 `VARIANT`。使用此构造函数的唯一原因是将类包装器放在你从别处获取的 `VARIANT` 上。我不想说你获得这种 `VARIANT` 的唯一方式是来自事件,但我会说它几乎是 100% 的情况。这就是为什么没有 `Attach` 函数的原因。`Attach` 模式会诱使你尝试在函数边界之间保留某些东西。对于对象来说,它工作得很好,它们将长期存在,例如窗口句柄,但对于像 `VARIANT` 这样的东西来说,它就行不通,`VARIANT` 是即时创建的,用于与某个其他模块(如你的)通信。

请注意,没有尝试实现复制构造函数。生命太短暂,无法尝试编写这样的野兽。想想看。你的代码将不得不处理每一种可能的变体,并对数组中的数组中的数组进行深度复制。

VARIANT 属性

一旦我们通过任何方法创建了我们的 `CVariant`,我们就可以使用它。你不会用 `VARIANT` 来在程序的同一个函数之间进行通信。你可能也不想在 DLL 边界上传递它。开销太大,使 `VARIANT` 变得不那么吸引人。因此,几乎可以肯定,你正在与一些你自己没有写过的东西进行通信。因此,你可以调用几个函数来检查从你没写过的东西传递过来的东西的**数据类型**。

//  Attributes
BOOL            IsArray(int iElement = 0);
BOOL            IsString(int iElement = 0);
BOOL            IsInt(int iElement = 0);
BOOL            IsBool(int iElement = 0);

这些 `IsAsomething()` 函数镜像了类支持的数据类型。如果你不确定特定 `VARIANT` 的类型,请使用这些函数来确定你即将执行的操作是否有成功的可能性。

为什么我不鼓励通过显式的成员函数访问 `vt` 成员?很高兴你问了。访问该成员会返回确切的类型。为什么这不好?因为它迫使你考虑所有无数的可能性。它可以是 `VT_USERDEFINED`、`VT_BLOB_OBJECT` 或 `VT_DISPATCH`。由于该类不处理这些类型,因此你无法从该信息中获得任何有用的东西。在我看来,更好地询问类:“你是一个 `string` 吗?”或者“你是一个整数吗?”如果答案是肯定的,那么你就可以继续执行有意义的操作。如果不是,你就进行适当的错误处理。

当然,没有什么可以阻止你显式访问 `vt` 成员,但如果你这样做了,你就要自行负责了。

VARIANT 访问

一旦确定了数据类型,就调用相应的访问器。访问器用于简单的 `VARIANT` 访问和数组访问,并接受一个默认值为零的参数。访问器会自动判断你是否有数组,并根据 `VARIANT` 的确切内容执行正确的操作。

数组的索引基数是什么?

由于这些是 `VARIANT` 和 `SAFEARRAY` 操作的 C++ 包装器,因此它们将数组视为 `OPTION BASE 0`。在内部,它们不一定需要(例如,它们可能来自 VB,其中设置了 `OPTION BASE 1`,但内部函数会纠正 `OPTION BASE`)。

访问器使用 `ElementAt()` 辅助函数来访问请求的数据,然后根据数据类型应用相应的数据转换。`ElementAt()` 函数看起来是这样的:

VARIANT *CVariant::ElementAt(int iElement)
{
    if (vt == VT_VARIANT)
        //  It's a pointer to an external VARIANT
        //  so return that variant
        return pvarVal;

    if (!(vt & VT_ARRAY))
        //  It's not an array so return ourselves
        return this;

    //  Calculate our element offset
    int offset = iElement - pvarVal->parray->rgsabound[0].lLbound;

    //  Offset must be zero or greater and less than the bounds
    if (offset >= 0 && offset <= int(pvarVal->parray->rgsabound[0].cElements))
        return &((VARIANT *) pvarVal->parray->pvData)[offset];
    else
        return (VARIANT *) NULL;
}

你可以看到我之前说过的。如果 `VARIANT` 封装了一个从别处获得的 `VARIANT`,我们就返回那个 `VARIANT`。如果 `VARIANT` 不是数组,我们就返回一个指向自身的指针(记住该类派生自 `VARIANT` 结构,并且没有 `vtable`,所以 `this` 等同于指向基 `VARIANT` 结构的指针)。

否则,我们就有一个数组,所以我们计算 `SAFEARRAY` 的偏移量,考虑到存储在 `rgsabound` 结构中的下界。然后我们检查偏移量是否大于或等于 0 并且小于数组中的元素数量,如果符合条件,我们就返回指向 `SAFEARRAY` 元素的指针。如果你指定了一个无效的索引,你将收到一个 `NULL` 指针。

实际的访问器看起来是这样的:

CString CVariant::ToString(int iElement)
{
    USES_CONVERSION;

    //  Get the VARIANT at the iElement offset
    VARIANT *v = ElementAt(iElement);

    //  Must be a valid pointer and must be valid readable memory
    if (v != (VARIANT *) NULL && AfxIsValidAddress(v, sizeof(VARIANT), FALSE) && v->vt == VT_BSTR)
        return W2A(v->bstrVal);

    return _T("");
}

非常简单。其他访问器的工作方式也大致相同。

请注意,`bool` 重载使用了小写的 `bool` 数据类型,而不是 `typedef` 的 `BOOL`。这是为了区分 `int` 和 `bool` 重载。我们需要不同的重载,以便能够创建一个类型为 `VT_BOOL` 的 `VARIANT`。

类型转换

上面呈现的类不执行任何“类型强制转换”(下载中的类实现也不执行)。这是故意的。虽然我接受“强制转换”的概念,但我不认为它适合 C++ 环境。我宁愿在运行时知道发生了错误,而不是被“有用的”类设计所掩盖。该类通过在 `VARIANT` 不是预期的数字数据时返回零,或者在不是 `string` `VARIANT` 时返回空 `string` 来确保安全,但它不执行类型强制转换。但是,如果你想实现类型强制转换,你可以这样做:

LPCTSTR CVariant::ToString() const
{
    USES_CONVERSION;
    CString csTemp;

    switch (vt)
    {
    case VT_BSTR:
        return W2A(bstrVal);

    //  It's not a string, maybe a number?
    case VT_I4;
        csTemp.Format(_T("%d"), lVal);
        break;

    case VT_I2:
        csTemp.Format(_T("%d"), iVal);
        break;

    //  and so forth...
    }

    return csTemp;
}

这段小片段在 `VARIANT` 确实包含 `string` 时返回转换后的 `string`。否则,它会尝试将数字数据转换为 `string` 表示形式并返回。最后,如果 `VARIANT` 类型不在 `switch` 语句的处理范围内,它会返回一个空 `string`。

历史

  • 2004 年 3 月 19 日 - 初始版本
  • 2004 年 3 月 20 日 - 添加了 `bool` 重载
  • 2004 年 3 月 28 日 - 修复了 `ElementAt()` 函数中的一个错误
© . All rights reserved.