您的位置:首页 > 编程语言 > C语言/C++

宏定义的极致发挥---让你的普通C++类轻松支持IDispatch自动化接口

2009-09-05 09:25 417 查看
    这篇文章其实有两个主题:一个是充分利用宏的特性把代码最大程度简化;另一个是如何在现有的项目中添加自动化支持。要我说哪一个主题更重要,对不起我也不知道,因为两个主题是紧密关联的,后面的介绍将以宏的使用为主,自动化接口只是宏的一种应用。
    熟悉MFC和ATL/WTL的人一定都很熟悉它们的消息映射表,利用宏进行填表是一种非常简洁非常优雅的编码方式,通俗易懂维护方便。对于宏来说,使用越是简单,其内部构造就越复杂。很多人都说MFC把宏定义运用到了极致,以前我同意,现在有所保留,为什么?因为MFC的框架太古老了,它使用的也是早期的宏特性。社会在发展,技术在进步,如今的标准已经对宏的特性进行了扩展。充分利用扩展的宏特性,能制作出更加简洁的代码。
    说了很多废话,言归正传。在大多数C++软件项目中,往往没有自动化需求,程序员通常会创造出下面的代码:
class CDog : public CAnimal


{


public:


CString Name;


long Height;


 


void Drink();


bool Eat(long lType, long lNum);


};


 


.codearea{ color:black; background-color:white; line-height:18px; border:1px solid #4f81bd; margin:0; width:auto !important; width:100%; overflow:auto; text-align:left; font-size:12px; font-family: "Courier New","Consolas","Fixedsys","BitStream Vera Sans Mono", courier,monospace,serif}
.codearea pre{ color:black; line-height:18px; padding:0 0 0 12px !important; margin:0em; background-color:#fff !important}
.linewrap pre{white-space:pre-wrap; white-space:-moz-pre-wrap; white-space:-pre-wrap; white-space:-o-pre-wrap; word-wrap:break-word; word-break:normal}
.codearea pre.alt{ background-color:#f7f7ff !important}
.codearea .lnum{color:#4f81bd;line-height:18px}

每个项目可能会出现一大堆的类定义,并且程序运作良好。忽然某一天,项目主管要求对代码执行自动化改造,或者为了制作出能在浏览器里执行的OCX控件,或者能被某些脚本语言调用,或者能嵌入到IIS中的ASP页面内执行……,总之,老板想让WEB开发人员也能逗这条小狗(CDog)。

    我相信每个人针对这种需求都有这样一些感觉:我有很多改造方案,可是不管哪个方案,工作量都很大。MFC项目天然支持自动化,但它的个头太庞大了。ATL也是纯种的自动化解决方案,但必须在纯种的ATL项目中才能使用它的各种向导功能,如果新建一个ATL项目,把这些现成的类搬进来再改造也不是一件容易的事,尤其是涉及到类与类之间的继承关系调整。有些人可能会想直接利用ATL中的IDispatchImpl<>模板类,想法很好,但几乎实现不了,因为IDispatchImpl内部严重依赖类型库,或者保存在注册表中,或者保存在程序资源中,总之IDL文件是必须要有的,而手工完成这些的工作量还不如新建一个ATL项目来得划算。还有一种方法就是让CDog直接从IDispatch派生,然后实现IDispatch的所有7个方法,在它的Invoke实现中根据DISPID不同分别访问CDog的成员变量或者调用成员函数,或者单独实现一个从IDispatch派生的类专门处理CDog,但这种方式的工作量也是显而易见的,每个类都需要独立派生,或者每个类都需要创建一个对应的自动化支持类。

    超级无敌的宏现身了,这就是我的解决方案。它的最大好处就是用最小的代价完成自动化改造,你的现有项目可以是SDK/MFC/ATL,无需创建新项目,无需IDL文件,无需类型库,无需注册组件。这种解决方案也是类似于消息映射宏,填表即可完成。还是以CDog的改造为例,老板希望这个dog具备Name和Height属性,也具备Drink和Eat方法,那么使用宏解决方案,将会是下面的代码:

class CDog : public CAnimal


{


public:


CString Name;


long Height;


 


void Drink();


bool Eat(long lType, long lNum);


 


Begin_Disp_Map(CDog)


Disp_Property(1, Name, CString)


Disp_PropertyGet(2, Height, long)


Disp_Method(3, Drink, void, 0)


Disp_Method(4, Eat, bool, 2, long, long)


End_Disp_Map()


};


.codearea{ color:black; background-color:white; line-height:18px; border:1px solid #4f81bd; margin:0; width:auto !important; width:100%; overflow:auto; text-align:left; font-size:12px; font-family: "Courier New","Consolas","Fixedsys","BitStream Vera Sans Mono", courier,monospace,serif}
.codearea pre{ color:black; line-height:18px; padding:0 0 0 12px !important; margin:0em; background-color:#fff !important}
.linewrap pre{white-space:pre-wrap; white-space:-moz-pre-wrap; white-space:-pre-wrap; white-space:-o-pre-wrap; word-wrap:break-word; word-break:normal}
.codearea pre.alt{ background-color:#f7f7ff !important}
.codearea .lnum{color:#4f81bd;line-height:18px}

     填完上面的表格,这个CDog已经可以被WEB开发人员牵出来遛了。顺便解释一下这个映射表的用法:

1、映射表以Begin_Disp_Map开始,唯一的参数就是需要改造的类CDog

2、映射表以End_Disp_Map结束

3、每一个属性或者方法占用一行表项

4、属性的用法 Disp_Property(dispid, property_name, property_type),以 Disp_Property(1, Name, CString) 为例,属性的DISPID是1,属性的名称是Name,属性的类型是CString。这个宏表示Name属性是可读写的,如果属性只读(如Height)应该用 Disp_PropertyGet,如果属性只写应该用 Disp_PropertyPut,参数的含义是一致的

5、方法的用法 Disp_Method(dispid, method_name, return_type, param_count, param1_type, …, paramN_type),以 Disp_Method(3, Drink, void, 0) 为例,方法的DISPID是3,方法名称是Drink,方法函数返回值类型是void,方法没有参数。再看看 Disp_Method(4, Eat, bool, 2, long, long) 示例,方法的DISPID是4,方法名称是Eat,方法函数返回类型是bool,方法有2个参数,第一个参数类型是long,第二个参数类型是long

    这个自动化接口怎么用呢?通过下面的例子可以看出来,使用非常简单,因为映射宏暗中添加了 CDog::GetDispatch() 成员函数。

CDog dog;


CComPtr<IDispatch> disp = dog.GetDispatch();


disp.Invoke0(OLESTR("Drink")); // 这句自动化调用将导致 CDog::Drink() 函数被执行


.codearea{ color:black; background-color:white; line-height:18px; border:1px solid #4f81bd; margin:0; width:auto !important; width:100%; overflow:auto; text-align:left; font-size:12px; font-family: "Courier New","Consolas","Fixedsys","BitStream Vera Sans Mono", courier,monospace,serif}
.codearea pre{ color:black; line-height:18px; padding:0 0 0 12px !important; margin:0em; background-color:#fff !important}
.linewrap pre{white-space:pre-wrap; white-space:-moz-pre-wrap; white-space:-pre-wrap; white-space:-o-pre-wrap; word-wrap:break-word; word-break:normal}
.codearea pre.alt{ background-color:#f7f7ff !important}
.codearea .lnum{color:#4f81bd;line-height:18px}

     事实上,我实现的自动化支持的宏定义曾经有两个版本,用法相似,但是实现方法完全不同。第一个版本采用的是自动创建类型库,然后通过 CreateStdDispatch() 函数创建 IDispatch 接口,这种方式有一些限制和缺陷,由于调用了API,我也不清楚内部有多少限制,因此导致第二个版本的诞生,这个版本完全在我自己的“掌控”中,可随时调整和改造。此版本是一个独立的头文件macro.h,里面几乎全部是宏定义,还有几个作为辅助工具的模板类定义和数据结构。任何人都可以随意使用该文件,也可以随意修改拷贝,不用交版税^_^。

    还有必要提醒一下这套解决方案的使用条件:

1、Windows平台,VC版本至少是2005,VC6/2003等早期版本不能使用。

2、用到了少量的ATL头文件,如果实际使用,可能需要自己添加对这些头文件的包含。

3、不依赖任何其它库,任何项目类型都可以使用。

4、变量类型请尽量使用VARIANT中支持的基础类型。

    如果用上面的示例代码进行实际的编译测试,你会发现编译通不过,原因在于 Name 的类型是 CString,这是一种其它库封装的高级类型,VARIANT不能识别。有两种解决方法:一种是懒人用的,把CString改成 CComBSTR,映射表中也需要对应修改;另一种是高级方法,这需要使用者完全理解了我实现的宏定义,通过自己创建特化模板类来扩充对 CString 的支持,例如 template<> class CVarTypeInfo< CString > { … };

    最后,我得申明一下,这套宏代码我自己一直在使用,而且一直在根据自己的实际需求改进和扩充,但本人没有义务一定要把最新改进版本贡献出来。

    最后的最后,提供整个宏定义的 macro.h 头文件,自动化支持的部分可以直接被使用,宏的使用供有心人研究吧。

#ifndef __MACRO_H__


#define __MACRO_H__




#pragma once






//////////////////////////////////////////////////////////////////////////


// 基础工具宏定义




#define __for_each_number(v, ...) /


v(0, __VA_ARGS__) /


v(1, __VA_ARGS__) /


v(2, __VA_ARGS__) /


v(3, __VA_ARGS__) /


v(4, __VA_ARGS__) /


v(5, __VA_ARGS__) /


v(6, __VA_ARGS__) /


v(7, __VA_ARGS__) /


v(8, __VA_ARGS__) /


v(9, __VA_ARGS__) /


v(10, __VA_ARGS__) /


v(11, __VA_ARGS__) /


v(12, __VA_ARGS__) /


v(13, __VA_ARGS__) /


v(14, __VA_ARGS__) /


v(15, __VA_ARGS__)


 


// 数值减 1 的常数


#define __cntdec_0 0


#define __cntdec_1 0


#define __cntdec_2 1


#define __cntdec_3 2


#define __cntdec_4 3


#define __cntdec_5 4


#define __cntdec_6 5


#define __cntdec_7 6


#define __cntdec_8 7


#define __cntdec_9 8


#define __cntdec_10 9


#define __cntdec_11 10


#define __cntdec_12 11


#define __cntdec_13 12


#define __cntdec_14 13


#define __cntdec_15 14


 


#define __cntdec(n) __cntdec_##n






// 连接两个符号


#define __connect2(x, y) x##y


#define __connect(x, y) __connect2(x, y)


 


// 生成不同个数的顺序符号


#define __repeat_0(m, ...)


#define __repeat_1(m, ...)    __repeat_0(m, __VA_ARGS__)  m(1, __VA_ARGS__)


#define __repeat_2(m, ...)    __repeat_1(m, __VA_ARGS__)  m(2, __VA_ARGS__)


#define __repeat_3(m, ...)    __repeat_2(m, __VA_ARGS__)  m(3, __VA_ARGS__)


#define __repeat_4(m, ...)    __repeat_3(m, __VA_ARGS__)  m(4, __VA_ARGS__)


#define __repeat_5(m, ...)    __repeat_4(m, __VA_ARGS__)  m(5, __VA_ARGS__)


#define __repeat_6(m, ...)    __repeat_5(m, __VA_ARGS__)  m(6, __VA_ARGS__)


#define __repeat_7(m, ...)    __repeat_6(m, __VA_ARGS__)  m(7, __VA_ARGS__)


#define __repeat_8(m, ...)    __repeat_7(m, __VA_ARGS__)  m(8, __VA_ARGS__)


#define __repeat_9(m, ...)    __repeat_8(m, __VA_ARGS__)  m(9, __VA_ARGS__)


#define __repeat_10(m, ...) __repeat_9(m, __VA_ARGS__)  m(10, __VA_ARGS__)


#define __repeat_11(m, ...) __repeat_10(m, __VA_ARGS__)  m(11, __VA_ARGS__)


#define __repeat_12(m, ...) __repeat_11(m, __VA_ARGS__)  m(12, __VA_ARGS__)


#define __repeat_13(m, ...) __repeat_12(m, __VA_ARGS__)  m(13, __VA_ARGS__)


#define __repeat_14(m, ...) __repeat_13(m, __VA_ARGS__)  m(14, __VA_ARGS__)


#define __repeat_15(m, ...) __repeat_14(m, __VA_ARGS__)  m(15, __VA_ARGS__)


 


#define __last_repeat_0(m, ...)


#define __last_repeat_1(m, ...)    m(1, __VA_ARGS__)


#define __last_repeat_2(m, ...)    m(2, __VA_ARGS__)


#define __last_repeat_3(m, ...)    m(3, __VA_ARGS__)


#define __last_repeat_4(m, ...)    m(4, __VA_ARGS__)


#define __last_repeat_5(m, ...)    m(5, __VA_ARGS__)


#define __last_repeat_6(m, ...)    m(6, __VA_ARGS__)


#define __last_repeat_7(m, ...)    m(7, __VA_ARGS__)


#define __last_repeat_8(m, ...)    m(8, __VA_ARGS__)


#define __last_repeat_9(m, ...)    m(9, __VA_ARGS__)


#define __last_repeat_10(m, ...) m(10, __VA_ARGS__)


#define __last_repeat_11(m, ...)  m(11, __VA_ARGS__)


#define __last_repeat_12(m, ...)  m(12, __VA_ARGS__)


#define __last_repeat_13(m, ...)  m(13, __VA_ARGS__)


#define __last_repeat_14(m, ...)  m(14, __VA_ARGS__)


#define __last_repeat_15(m, ...)  m(15, __VA_ARGS__)


 


#define __repeat(n, m_begin, m_end, ...) __connect(__repeat_, __cntdec(n))(m_begin, __VA_ARGS__) __connect(__last_repeat_, n)(m_end, __VA_ARGS__)


 


// 基础工具宏结束


//////////////////////////////////////////////////////////////////////////






//////////////////////////////////////////////////////////////////////////


// Add IDispatch to class




//////////////////////////////////////////////////////////////////////////


// 扩充 CVarTypeInfo 模板类的定义


//template<>


//class CVarTypeInfo< void >


//{


//public:


//    static const VARTYPE VT = VT_EMPTY;


//    //static char VARIANT::* const pmField;


//};




template<>


class CVarTypeInfo< bool >


{


public:


static const VARTYPE VT = VT_BOOL;


static VARIANT_BOOL VARIANT::* const pmField;


};


 


__declspec( selectany ) VARIANT_BOOL VARIANT::* const CVarTypeInfo< bool >::pmField = &VARIANT::boolVal;


 


// 扩充 CComBSTR 类型,用这种类型代替BSTR,能防止内存泄露或者内存释放错误


template<>


class CVarTypeInfo< CComBSTR >


{


public:


static const VARTYPE VT = VT_BSTR;


static BSTR VARIANT::* const pmField;


};


 


__declspec( selectany ) BSTR VARIANT::* const CVarTypeInfo< CComBSTR >::pmField = &VARIANT::bstrVal;


 


// END of CVarTypeInfo. 使用者可以自行扩充新的类型,例如用 CString 来保存字符串


//////////////////////////////////////////////////////////////////////////








// 定义多参数的模板类


//////////////////////////////////////////////////////////////////////////


// 方法工具模板类和工具宏


#define __tparam(n, ...) typename T##n,


#define __tparam_end(n, ...) typename T##n


#define __param_type(n, ...) if (FAILED(v[n-1].ChangeType(CVarTypeInfo<T##n>::VT, &dp->rgvarg[dp->cArgs-n]))) return E_INVALIDARG;


#define __funcparam(n, ...) v[n-1].*CVarTypeInfo<T##n>::pmField,


#define __funcparam_end(n, ...) v[n-1].*CVarTypeInfo<T##n>::pmField


#define __funcparam_type(n, ...) T##n,


#define __funcparam_type_end(n, ...) T##n


#define __method_helper_t(n, ...) /


template<class TT, typename rettype, __repeat(n, __tparam, __tparam) rettype (TT::* func)(__repeat(n, __funcparam_type, __funcparam_type_end)) > /


class _MethodHelper_##n /


{ /


public: /


static HRESULT CallMethod (LPVOID pT, DISPPARAMS* dp, VARIANT* pvarResult) /


{ /


if (pT==NULL) return E_FAIL; /


if (dp->cArgs < n) return DISP_E_BADPARAMCOUNT; /


CComVariant v[n+1]; /*加1是为了避免 n==0 时的编译错误*/ /


__repeat(n, __param_type, __param_type) /


CComVariant vRet = (reinterpret_cast<TT*>(pT)->*func)( __repeat(n, __funcparam, __funcparam_end) ); /


if (pvarResult && vRet.vt!=VT_EMPTY) vRet.Detach(pvarResult); /


return S_OK; /


} /


}; /


/* 返回 VOID 的特化模板类*/ /


template<class TT, __repeat(n, __tparam, __tparam) void (TT::* func)(__repeat(n, __funcparam_type, __funcparam_type_end)) > /


class _MethodHelper_##n<TT, void, __repeat(n, __funcparam_type, __funcparam_type) func> /


{ /


public: /


static HRESULT CallMethod (LPVOID pT, DISPPARAMS* dp, VARIANT* pvarResult) /


{ /


if (pT==NULL) return E_FAIL; /


if (dp->cArgs < n) return DISP_E_BADPARAMCOUNT; /


CComVariant v[n+1]; /


__repeat(n, __param_type, __param_type) /


(reinterpret_cast<TT*>(pT)->*func)( __repeat(n, __funcparam, __funcparam_end) ); /


return S_OK; /


} /


};


 


// 预定义个 16 方法调用工具模板类


__for_each_number(__method_helper_t)


 


#define _method_helper(T, name, type, paramcnt, ...) _MethodHelper_##paramcnt<T,type,__VA_ARGS__,&T::name>::CallMethod






//////////////////////////////////////////////////////////////////////////


// 属性GET工具模板类和工具宏


template<class T, typename rettype, rettype T::* member>


class _GetHelper


{


public:


static HRESULT CallGet(LPVOID pT, DISPPARAMS* dp, VARIANT* pvarResult)


{


if (pT==NULL) return E_FAIL;


CComVariant vRet = reinterpret_cast<T*>(pT)->*member;


if (pvarResult) vRet.Detach(pvarResult);


return S_OK;


}


};


 


#define _get_helper(T, name, type) _GetHelper<T,type,&T::name>::CallGet






//////////////////////////////////////////////////////////////////////////


// 属性PUT工具模板类和工具宏


template<class T, typename rettype, rettype T::* member>


class _PutHelper


{


public:


static HRESULT CallPut(LPVOID pT, DISPPARAMS* dp, VARIANT* pvarResult)


{


if (pT==NULL) return E_FAIL;


if (dp->cArgs != 1) return DISP_E_BADPARAMCOUNT;


CComVariant v;


if (FAILED(v.ChangeType(CVarTypeInfo<rettype>::VT, dp->rgvarg))) return DISP_E_BADVARTYPE;


#pragma warning(push)


#pragma warning(disable:4800)


reinterpret_cast<T*>(pT)->*member = v.*CVarTypeInfo<rettype>::pmField;


#pragma warning(pop)


return S_OK;


}


};


 


#define _put_helper(T, name, type) _PutHelper<T,type,&T::name>::CallPut








//////////////////////////////////////////////////////////////////////////


// 映射表工具模板类和映射宏


typedef HRESULT (* fnDispMethod)(LPVOID pT, DISPPARAMS* dp, VARIANT* pVarResult);


struct DispMethodData


{


LPCOLESTR name;        // property or method name


DISPID dispid;            // dispid


fnDispMethod pfnGet;


fnDispMethod pfnPut;


fnDispMethod pfnMethod;


};


 


template<class T>


class DispProvider : public IDispatch


{


private:


T* _owner;


public:


DispProvider() : _owner(NULL) {}


void SetOwner(T* owner) { _owner = owner; }


 


/* IDispatch Methods*/


STDMETHOD_(ULONG, AddRef)() { return 2; }


STDMETHOD_(ULONG, Release)() { return 1; }


STDMETHOD(QueryInterface)(REFIID iid, LPVOID* ppvObj)


{


if (!_owner) return E_UNEXPECTED;


if (!ppvObj) return E_POINTER;


*ppvObj = NULL;


if (IsEqualIID(iid, __uuidof(IUnknown)) ||


IsEqualIID(iid, __uuidof(IDispatch)))


*ppvObj = this;


if (*ppvObj)


{


((LPUNKNOWN)(*ppvObj))->AddRef();


    return S_OK;


}


return E_NOINTERFACE;


}


STDMETHOD(GetTypeInfoCount)(UINT *pctinfo) { *pctinfo=0; return E_NOTIMPL; }


STDMETHOD(GetTypeInfo)(UINT /*iTInfo*/, LCID /*lcid*/, ITypeInfo **ppTInfo) { *ppTInfo = NULL; return E_NOTIMPL; }


STDMETHOD(GetIDsOfNames)(REFIID riid, OLECHAR ** rgszNames, UINT cNames, LCID lcid, DISPID * rgDispId)


{


ATLASSERT(cNames == 1);


if (cNames != 1) return E_NOTIMPL;


if (!_owner) return E_UNEXPECTED;


 


*rgDispId = DISPID_UNKNOWN;


const DispMethodData* pMap = T::__GetDispMapEntry(*rgszNames);


if (pMap)


return *rgDispId = pMap->dispid, S_OK;


return DISP_E_MEMBERNOTFOUND;


}


STDMETHOD(Invoke)(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS * pdispparams, VARIANT *pVarResult, EXCEPINFO * pExcepInfo, UINT * puArgErr)


{


if (!_owner) return E_UNEXPECTED;


 


const DispMethodData* pMap = T::__GetDispMapEntry(NULL, &dispIdMember);


if (pMap)


{


fnDispMethod pfn = (wFlags&DISPATCH_METHOD) ? pMap->pfnMethod : (wFlags==DISPATCH_PROPERTYGET) ? pMap->pfnGet : pMap->pfnPut;


if (pfn)


return pfn(_owner, pdispparams, pVarResult);


}


return DISP_E_MEMBERNOTFOUND;


}


};


 


#define Begin_Disp_Map(classname) /


public: /


DispProvider<classname> __disp; /


IDispatch* GetDispatch() { return __disp.SetOwner(this), (IDispatch*)&__disp; } /


static const DispMethodData* __GetDispMapEntry(LPCOLESTR pszByName=NULL/*find by name*/, DISPID* pByDispid=NULL/*find by dispid*/) /


{ /


typedef classname owner_class; /


static const DispMethodData __map_entry[] = {


 


#define Disp_PropertyGet(dispid, name, type) /


{OLESTR(#name), dispid, _get_helper(owner_class,name,type), NULL, NULL},


 


#define Disp_PropertyPut(dispid, name, type) /


{OLESTR(#name), dispid, NULL, _put_helper(owner_class,name,type), NULL},


 


#define Disp_Property(dispid, name, type) /


{OLESTR(#name), dispid, _get_helper(owner_class,name,type), _put_helper(owner_class,name,type), NULL},


 


#define Disp_Method(dispid, name, type, paramcnt, ...) /


{OLESTR(#name), dispid, NULL, NULL, _method_helper(owner_class,name,type,paramcnt,__VA_ARGS__)},


 


#define End_Disp_Map() /


{NULL, DISPID_UNKNOWN, NULL, NULL, NULL} /


}; /


if (pszByName==NULL && pByDispid==NULL) return __map_entry; /


for (int i=0; i<sizeof(__map_entry)/sizeof(__map_entry[0]) - 1; i++) /


{ /


if (pByDispid) /


{ /


if (__map_entry[i].dispid == *pByDispid) return &__map_entry[i]; /


} /


else /*if (pszByName)*/ /


{ /


if (lstrcmpiW(__map_entry[i].name, pszByName) == 0) return &__map_entry[i]; /


} /


} /


return NULL; /


}


 


 


 


#endif // __MACRO_H__


.codearea{ color:black; background-color:white; line-height:18px; border:1px solid #4f81bd; margin:0; width:auto !important; width:100%; overflow:auto; text-align:left; font-size:12px; font-family: "Courier New","Consolas","Fixedsys","BitStream Vera Sans Mono", courier,monospace,serif}
.codearea pre{ color:black; line-height:18px; padding:0 0 0 12px !important; margin:0em; background-color:#fff !important}
.linewrap pre{white-space:pre-wrap; white-space:-moz-pre-wrap; white-space:-pre-wrap; white-space:-o-pre-wrap; word-wrap:break-word; word-break:normal}
.codearea pre.alt{ background-color:#f7f7ff !important}
.codearea .lnum{color:#4f81bd;line-height:18px}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: