三只小豬
莫華楓
小時候聽說過三只小豬的故事,隱約記得故事是講三只小豬用不同方法造房子,對抗老狼。這些天做軟件,遇到一個無比簡單的問題,但在三種不同的語言中,卻有著截然不同的解法。
最近,冷不丁地接到公司下派的一個緊急任務,做手持POS和PC程序之間交換數(shù)據(jù)的程序。各種各樣的麻煩中,有一個小得不起眼的問題,POS機中數(shù)據(jù)的字
節(jié)序和PC相反。這可不算是什么啊。沒錯,是在太簡單了。盡管如此,還是引發(fā)了一場爭論。做POS程序的,希望PC程序做轉換。做PC程序的,希望POS
程序做轉換。(誰都想少做點,對吧;))。最終,作為和事佬的我,為了維護和諧的氛圍,攬下了這件事。當然,到底在那里做,還是要決定的。最終選擇在PC
上,畢竟PC上調(diào)試起來容易。(天煞的,這個POS機沒有debug,也沒有模擬器,顯示屏還沒我手機的大,做起來著實費事)。
其實,我的本意是想在POS上做這個轉換。因為POS用的是C(一個不知什么年代的gcc),可以直接操作字節(jié)。基本的代碼看起來差不多應該是這樣:
unsigned long InvData(unsigned long val, int n) {
unsigned long t=val, res=0;
for(; n >0; n--)
{
res = res << 8;
res |= (unsigned char)t;
t = t >> 8;
}
return res;
}
n是數(shù)據(jù)類型的字節(jié)長度。這里使用了最長的無符號整數(shù)類型。這是核心轉換函數(shù),各種類型的轉換函數(shù)都可以從中派生:
long InvDataLong(long val) {
return (long)InvData((unsigned long)val, sizeof(val));
}
short InvDataShort(short val) {
return (short)InvData((unsigned short)val, sizeof(val));
}
...
最后,有一個比較特殊的地方,float。float的編碼不同于整型,如果直接用(unsigned
long)強制類型轉換,只會把float數(shù)值的整數(shù)部分賦予參數(shù),得不到正確的結果。正確的做法,應當是把float占用的四個字節(jié)直接映射成一個
unsigned long:
float InvDataFloat(float val) {
float val=InvData(*(unsigned long*)(&val), sizeof(val));
return *(float*)(&val);
}
通過將float變量的地址強制轉換成unsigned long*類型,然后再dereference成unsigned
long類型。當然還有其他辦法,比如memcpy,這里就不多說了。至于double類型,為了簡化問題,這里將其忽略。如果有64位的整型,那么
double可以采用類似的解法。否則,就必須寫專門的處理函數(shù)。
當然,最終我還是使用C#寫這個轉換。相比之下,C#的轉換代碼顯得更具現(xiàn)代風味。基本算法還是一樣:
public static ulong DataInv(ulong val, int n)
{
ulong v1_ = val, v2_ = 0;
for (; n > 0; n--)
{
v2_ <<= 8;
v2_ |= (byte)v1_;
v1_ >>= 8;
}
return v2_;
}
對于習慣于C/C++的同學們注意了,long/ulong在C#中不是4字節(jié),而是8字節(jié)。也就是C/C++中的longlong。以這個函數(shù)為基礎,其它整數(shù)類型的字節(jié)序轉換也就有了:
public static ulong DataInv(ulong val)
{
return DataInv(val, sizeof(ulong));
}
public static uint DataInv(uint val)
{
return (uint)DataInv((ulong)val, sizeof(uint));
}
public static int DataInv(int val)
{
return (int)DataInv((uint)val);
}
...
然而,面對float,出現(xiàn)了麻煩。在C#中,沒有指針,無法象C那樣將float展開成ulong。(unsafe代碼可以執(zhí)行這類操作,但這不是C#嫡親的特性,并且是違背C#設計理念的。這里不做考慮)。C#提供了另一種風格的操作:
public static float DataInv(float val)
{
float res_ = 0;
byte[] buf_ = BitConverter.GetBytes(val);
byte t = 0;
t = buf_[0];
buf_[0] = buf_[3];
buf_[3] = t;
t = buf_[1];
buf_[1] = buf_[2];
buf_[2] = t;
res_ = BitConverter.ToSingle(buf_, 0);
return res_;
}
這個做法盡管有些累贅,但道理上很簡單:把float變量轉換成一個字節(jié)流,然后把相應的位置對調(diào),就獲得了字節(jié)反序的float。相比C的float轉
換,C#明顯不夠簡練。原因很簡單,C#根本不是用來干這個的。C是一種非常底層的語言,它的內(nèi)存模型是完全直觀的,與硬件系統(tǒng)相對應的。因而,對于這種
與機器相關的操作,當然也顯得游刃有余。而C#定位于高層開發(fā)的高級語言,底層的內(nèi)存模型是被屏蔽的,程序員無需知道和關心。
不過,C#的代碼卻擁有它的優(yōu)勢。只需看一眼這些函數(shù)的使用代碼,便不言自明了:
//C代碼
int x=234;
float y=789.89;
short z=332;
x=InvDataInt(x);
y=InvDataFloat(y);
z=InvDataShort(z);
//C#代碼
int x=234;
float y=789.89;
short z=332;
x=DataInv(x);
y=DataInv(y);
z=DataInv(z);
在C代碼中,對于不同的類型,需要使用不同命名的函數(shù)。而在C#代碼中,則只需使用DataInv這樣一個函數(shù)名。至于屆時選用那個版本的函數(shù),編譯器會
根據(jù)實際的類型自動匹配。C#運用函數(shù)重載這個特性,使得調(diào)用代碼可以采用統(tǒng)一的形式。即便是數(shù)據(jù)的類型有所變化,也無需對調(diào)用代碼做任何修改。(這在我
的開發(fā)過程中的得到了驗證,傳輸數(shù)據(jù)的成員類型曾經(jīng)發(fā)生變化,我也只是修改了數(shù)據(jù)結構的定義,便將問題搞定)。這一點,在C中是無法做到的。
歸結起來,C由于側重于底層,在數(shù)據(jù)轉換方便的靈活性,使得轉換代碼的構建更加容易。而C#則得益于函數(shù)重載,在轉換代碼使用方面,有獨到的優(yōu)勢。
迄今為止,三只小豬中,還只有兩只出現(xiàn)。下面就要第三只出場了。
作為C++的粉絲,我會自然而然地想到使用C++來實現(xiàn)這個轉換功能。于是便有了如下的代碼:
unsigned long InvData(unsigned long val, int n) {
unsigned long t=val, res=0;
for(; n >0; n--)
{
res = res << 8;
res |= (unsigned char)t;
t = t >> 8;
}
}
long InvData(long val) {
return (long)InvData((unsigned long)val, sizeof(val));
}
short InvData(short val) {
return (short)InvData((unsigned short)val, sizeof(val));
}
...
float InvData(float val) {
float val=InvData(*(unsigned long*)(&val), sizeof(val));
return *(float*)(&val);
}
這些代碼就好象是C和C#代碼的雜交后代。既有C的底層操作,也有C#的函數(shù)重載,兼有兩者的優(yōu)點。
不過,還能做得更好:
template<typename T>
T InvData(T val) {
T t=val, res=0;
int n=sizeof(T);
for(; n >0; n--)
{
res = res << 8;
res |= (unsigned char)t;
t = t >> 8;
}
return (T)res;
}
這樣,就把所有的整型都一網(wǎng)打盡了,僅用一個函數(shù)模板,便完成了原先諸多函數(shù)所做的工作。而float版本的函數(shù)則保持不變,作為InvData()的一個重載。按照C++的函數(shù)模板-重載規(guī)則,float版的函數(shù)重載將被優(yōu)先使用。
好了,三只小豬的故事講完了。前兩只小豬各有優(yōu)點,也各有缺點。而第三只小豬則雜合和前兩者的優(yōu)點,并且具有更大的進步。盡管第三只小豬存在各種各樣的缺陷,但畢竟它的眾多特性為我們帶來了很多效率和方便,這些還是應當值得肯定的。
附:第三只小豬的其他手段:
1、強制轉換成字符串數(shù)組
template<typename T>
T InvData1(T v) {
unsigned char* pVal1=(unsigned char*)(&v)
, *pVal2=pVal1+sizeof(T)-1, t;
while(pVal2-pVal1>1)
{
t=*pVal2;
*pVal2=*pVal1;
*pVal1=t;
pVal1++;
pVal2--;
}
return v;
}
2、使用標準庫,blog上有人留言建議的
template<typename T>
T InvData(T v) {
unsigned char* pVal=(unsigned char*)(&v);
size_t n=sizeof(T);
std::reverse(pVal, pVal+n, pVal);
}
3、使用traits
template<size_t n> struct SameSizeInt;
template<> struct SameSizeInt<1> { typedef unsigned char Type; };
template<> struct SameSizeInt<2> { typedef unsigned short Type; };
template<> struct SameSizeInt<4> { typedef unsigned long Type; };
template<> struct SameSizeInt<8> { typedef unsigned longlong Type; };
template<typename T>
T InvData(T v) {
size_t n=sizeof(T);
typedef SameSizeInt<sizeof(T)>::Type NewT;
NewT v1=*(NewT*)(&v), v2=0;
for(; n >0; n--)
{
v2= v2<< 8;
v2|= (unsigned char)v1;
v1 = v1 >> 8;
}
return *(T*)(&v2);
}
甚至可以使用tmp去掉其中的循環(huán)。在C++中,這類任務的實現(xiàn)方法,完全看程序員的想象力了。:)