引子
余好程序,喜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#”的感覺吧。