引子
余好程序,喜BCB(Borland C++ Builder)。一日见C#之属性声明,顿觉清爽。其后偶有所启,遂整以条理、载以文字,且冠文章之名。于夫同好而喜BCB者,或可有益。若是,其旨达矣。
背景描述——宏的历史地位
面向对象的C++语言推出后,曾经在C中极其重要的宏命令似乎变得很少使用了。连C++大师Bjarne Stroustrup在他的经典C++教程《The C++ Programming Language》中也如是说:“关于宏的第一项规则是:绝对不应该去使用它,除非你不得不这样做。因为它将在编译器看到程序的正文之前重新摆布这些正文。” 就连以前用的最多的定义常量在C++中也有了新关键字const了,在加上其他的inline、template、enum、namespace机制等都是为了用做预处理结构的许多传统使用方式的替代品。
不过,Bjarne Stroustrup自己也承认,利用宏我们可以设计出自己的私有语言。只要它能让我们的工作简化,那它就是一个有用的宏。
“可以设计出自己的私有语言”,这对于我们而言意味着什么?又如何使用它来好好改造现有的语言,以便简化我们的工作呢?当然,所谓简化必然是对繁杂而言。那么只要是原有的语法显得繁杂的地方我们都可以使用宏来简化它。
一个很好的典型就是BCB中的属性声明语法。
背景描述——BCB和C#的属性声明语法
要在BCB的类声明中声明一个属性,首先必须声明一个属性数据的存放者——一个数据成员。当然一般而言是在私有部分声明的。然后如果在读或写属性时还有一些其他的操作,那就得声明读或写属性的方法。一般来讲为了保证数据的合法行,往往至少需要一个写属性方法。最后还得在public或__publish(组件用)中发布出来,才能被类的使用者可见。
还是来看看具体的代码吧。
假设一个女孩类CGirl,有个漂亮指数属性Prettiness,并且取值范围是0—3。
0:其丑无比;
1:马马虎虎;
2:相貌端庄
3:倾国倾城。
那么在BC中大抵都得这么写:
头文件CGirl.h中
class CGirl
{
public :
__property int Prettiness={read=FGetPrettiness,write=FSetPrettiness};
...//(以下略去其他公共成员若干)
private:
int FPrettiness;
...//(以下略去其他私有数据成员若干)
int __fastcall FGetPrettiness();
void __fastcall FSetPrettiness (int vPrettiness)
...//(以下略去其他私有方法成员若干)
};
代码文件CGirl.cpp中
int __fastcall CGirl:: FGetPrettiness()
{
return FPrettiness;
}
void __fastcall CGirl:: FSetPrettiness (int vPrettiness)
{
if (vPrettiness<=3 && vPrettiness>=0)
FPrettiness=v FPrettiness;
}
……
对于简单的类,这样的定义也许还不算太麻烦。属性的声明定义还能一目了然。但如果类稍微复杂一点——具体一点如果是一个1000行的类呢?(冒汗)结果往往是在诺大的代码里翻来翻去,为的就是查看和某个属性相关的读或写方法的定义。
而现在,在微软.Net的发布后,.Net Framework一下子成了最流行的东东。它以不可阻挡之势袭来。而且微软为了和SUN的Java争个高下,也不顾现在编程语言的泛滥之势,在Visual Studio中又加入了一种新的语言。我想不用我说大家早就知道了——对就是它:C#,又一个和C++语法十分相似的语言。
对于.Net和C#的故事实在太多了,不可能也不需要在这里说了。但是C#的对于属性声明的语法却给笔者留下了深刻的影响。这恰恰也正是今天我们所关心的。那么来看看C#的属性声明语法吧。同样以上面的CGirl类作为例子,用C#写来可能是下面这个样子:
class CGirl
{
private int FPrettiness;
public int Prettiness //定义属性
{
get //读属性方法
{
return FPrettiness;
}
set //写属性方法
{
if (value<=3 && value>=0)
FPrettiness=value;
}
}
}
不用我多说,孰繁孰简已经很明显了。
下面,我们就来动手看看如何通过宏命令把BCB繁杂的属性声明语法变的一如C#般简洁。这不是变戏法但其精彩却一点也不亚于它。
技术基础——宏定义语法
首先,我们得了解宏的定义格式:#define <宏名> [展开内容]
其实这样的东西几乎是什么都不能做的,但要是给它带上参数就不一样了:
//带参数的宏定义格式
#define <宏名(参数1[,参数2,参数3……])<展开内容>
比如一个带参宏的声明:#define WRITEN(a) cout<<a<<endl
在代码中假如这么写:WRITEN("巧用宏命令,改造C++ Builder");
那么,其经过宏扩展后就会变成这样:cout<<"巧用宏命令,改造C++ Builder"<<endl;
这就是带参宏的扩展规律了,并不复杂。为了要把BCB的属性声明语法变成C#,我们另外还需要知道两个操作符:/
和 ##
。
-
/
操作符:当宏定义的扩展内容不止一行时的连接符。
要换行写宏定义的话,在行尾加上它就行了。如:
#define TITLE 巧用宏命/
令,改造C++ Builder
就等效于:
#define TITLE 巧用宏命令,改造C++ Builder
-
##
操作符:参数扩展连接符。
它可以把前面的内容和参数连接成一个没有间隔的整体。还是来看看例子:
定义一个宏:#define VAR(i, j) (i##j)
那么如果在代码中这样写:VAR(x, 6)
,它将会被扩展为x6
。利用这个特性我们就可以对给定的宏参数加以修饰,以便适应我们的要求了。
技术实现——开始改造BCB
基础的东西就这么多了,那就来看看需要怎么做来改造BCB。
首先,确定宏的名称,这里暂时就以PROPERTY为宏名。
其次,确定宏的参数。
我们的目的要使宏使用起来和C#的语法尽量相似,那么宏使用起来多少应该像这样:
PROPERTY
(
属性的可见性 属性类型 属性名
{
get
{
//读属性代码
}
set
{
//写属性代码
}
}
)
但上面这样的伪代码虽然最接近C#语法,却无法定义出适合的宏。原因是宏所需要的一些重要的参数元素无法从整体的代码中分隔出来。所谓重要的参数元素就是上面伪代码中描述的 “属性的可见性”、“属性类型”、“元素名”、“读属性代码部分”以及“写属性部分”这些东西。而上面伪代码中从“属性的可见性”开始一直到最后一个大括号是一个整体,成为一个参数。这自然无法正确扩展了。我们应该把这五个重要的元素分开为一个个的参数,原则当然是和上面的伪代码形式越相似越好:
PROPERTY
(
属性的可见性, 属性类型, 属性名,
get
{
//读属性代码
}
,
set
{
//写属性代码
}
)
这样就形式上而言达到了把每个元素分开而独立成为一个宏参数的要求。
然后,就上面的使用形式,我们来设计这个宏。
先给上面的每个元素取个标识,分别是:
属性的可见性 pRegion
属性类型 pType
属性名 pName
读属性代码 pGetMethod
写属性代码 pSetMethod
还记得在BCB声明一个属性需要的两个方法函数吗,分别对应读和写。在宏定义里必须采用固定的函数名,而且必须和属性名相关。这样我们定为在属性名前分别加FGet和FSet来构成读写方法函数名,其中写方法的参数(对应属性的新值)始终是value(一如在C#中)。
在宏中的定义:
//读方法函数
pType __fastcal FGet##pName () pGetMethod
//写方法函数
void __fastcall FSet##pName(pType value) pSetMethod
别忘了还有一个属性发布语句:
__property pType pName={read= FGet##pName ,write= FSet##pName };
整理一下,宏的声明最后变成下面这样:
PROPERTY(pRegion,pType,pName,pGetMethod,pSetMethod) /
private: /
pType FGet##pName () /
pGetMethod /
void FSet##pName (pType value) /
pSetMethod /
pRegion: /
__property pType pName={ /
read=FGet ##pName /
,write=FSet ##pName /
};
请注意:在宏的使用代码中,读或写方法函数还有get或set标识符,而它们必须被屏蔽,否则无法通过把pGetMethod或pSetMethod附加在函数声明后而形成完整的函数实现。
通过下面的两句宏声明来屏蔽get或set:
#define get
#define set
到这里,这个宏定义就算基本成功了。然而,由于属性可能有只读的、只写的或不需要读方法函数的(很多属性只要直接读取成员数据的值就可以了,不需要额外的代码)种类区别,我们还需要另外几个可以适用于这些情况的宏。参看具体代码。
最后,使用这样的宏有几个值得特别注意的地方:
- 注意分隔各个宏参数的逗号,特别是get和set方法之间的不能漏掉。
- 注意get和set方法实现不能颠倒位置。
完整实现——具体的代码
////////////////////////////////////Goldroc Opus///////////////////////////////////////
//属性定义宏(For C++ Builder)
//说明:仿照C#中的属性声明语法
//(头文件中)
// 定义可读写属性:PROPERTY(属性可见性,属性类型,属性名,读属性代码,写属性代码)
// 定义只读属性: PROPERTY_READONLY(属性可见性,属性类型,属性名,读属性代码)
// 定义只写属性: PROPERTY_WRITEONLY(属性可见性,属性类型,属性名,写属性代码)
// 定义不需要读方法的属性宏:PROPERTY_DIRECT_READ(属性可见性,属性类型,属性名,属性对应的数据,写属性代码)
// 定义不需要读方法的只读属性宏:PROPERTY_DIRECT_READ(属性可见性,属性类型,属性名,属性对应的数据)
//
/////////////////////////////////////////////////////////////////////////////////////////
//下列宏定义一般的规则如下:
//在private 中放记录属性数据的成员变量,取名为在属性名前加F
//在private 中放设置属性数据的成员变量的函数,取名为在成员变量名前加FGet或FSet
// 行参取名为value
//如:属性 int Left
// PROPERTY
// ( public,int,Left,
// get
// {
// return _Left; //在属性名前加前缀_表示属性的当前值
// }
// , //注意这个逗号不能丢!!!
// set
// {
// _Left=value; //新的值为value
// }
// )
//
//注意:get和set方法不能颠倒位置!
//
// 只读属性 int Left
// PROPERTY_READONLY
// ( public,int,Left,
// get
// {
// return _Left; //在属性名前加前缀_表示属性的当前值
// }
// )
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
#ifndef PROPERTY_MACROH
#define PROPERTY_MACROH
//---------------------------------------------------------------------------
#define get /*get*/
#define set /*set*/
#define PROPERTY(pRegion,pType,pName,pGetMethod,pSetMethod) /
private: /
pType FGet##pName () const /
pGetMethod /
void FSet##pName (const pType& value) /
pSetMethod /
pRegion: /
__property pType pName={ /
read=FGet ##pName /
,write=FSet ##pName /
};
//只读属性宏
#define PROPERTY_READONLY(pRegion,pType,pName,pGetMethod) /
private: /
pType FGet##pName () const /
pGetMethod /
pRegion: /
__property pType pName={ /
read=FGet ##pName /
};
//只写属性宏
#define PROPERTY_WRITEONLY(pRegion,pType,pName, pSetMethod) /
private: /
void FSet##pName (const pType& value) /
pSetMethod /
pRegion: /
__property pType pName={ /
write=FSet ##pName /
};
//不需要读方法的属性宏
#define PROPERTY_DIRECT_READ(pRegion,pType,pName,pData,pSetMethod) /
private: /
void FSet##pName (const pType& value) /
pSetMethod /
pRegion: /
__property pType pName={ /
read= pData /
,write=FSet ##pName /
};
//不需要读方法的只读属性宏
#define PROPERTY_DIRECT_READONLY(pRegion,pType,pName,pData) /
pRegion: /
__property pType pName={ /
read= pData /
};
#endif
//---------------------------------------------------------------------------
(以上代码在BCB6中调试通过)
这样,把以上代码保存为一个头文件,如property_macro.h。以后在自己的代码中#include
包含进这个头文件即可。根据同样的原理,我们甚至可以把BCB的整个类声明语法都改造成类C#的,这里不再累述了,读者可以自己动手试试。
现在,让我们来尝尝在BCB中写“C#”的感觉吧。