我一直以來(lái)對(duì)于exception的態(tài)度都是很明確的。首先exception是好的,否則就不會(huì)有絕大多數(shù)的語(yǔ)言都支持他了。其次,error code也沒(méi)什么問(wèn)題,只是需要一個(gè)前提——你的語(yǔ)言得跟Haskell一樣有monad和comonad。你看Haskell就沒(méi)有exception,大家也寫(xiě)的很開(kāi)心。為什么呢?因?yàn)橹灰逊祷貛rror code結(jié)果的函數(shù)給做成一個(gè)monad/comonad,那么就可以用CPS變換把它變成exception了。所以說(shuō)CPS作為跟goto同樣基本的控制流語(yǔ)句真是當(dāng)之無(wú)愧呀,只是CPS是type rich的,goto是type poor的。
其實(shí)很多人對(duì)于exception的恐懼心理在于你不知道一個(gè)函數(shù)會(huì)拋什么exception出來(lái),然后程序一crash你就傻逼了。對(duì)于server來(lái)講情況還好,出了問(wèn)題只要?dú)⒌艨焖僦貑⒕托辛耍缃駴](méi)個(gè)replication和fault tolerance還有臉說(shuō)你在寫(xiě)后端(所以不知道那些做web的人究竟在反對(duì)什么)?這主要的問(wèn)題還是在于client。只要client上面的東西還沒(méi)保存,那你一crash數(shù)據(jù)就完蛋了是不是——當(dāng)然這只是你的想象啦,其實(shí)根本不是這樣子的。
我們的程序拋了一個(gè)access violation出來(lái),和拋了其它exception出來(lái),究竟有什么區(qū)別呢?access violation是一個(gè)很奇妙的東西,一旦拋了出來(lái)就告訴你你的程序沒(méi)救了,繼續(xù)執(zhí)行下去說(shuō)不定還會(huì)有破壞作用。特別是對(duì)于C/C++/Delphi這類(lèi)語(yǔ)言來(lái)說(shuō),你不小心把錯(cuò)誤的東西寫(xiě)進(jìn)了什么亂七八糟的指針里面去,那會(huì)兒什么事情都沒(méi)發(fā)生,結(jié)果程序跑著跑著就錯(cuò)了。因?yàn)槟隳莻€(gè)算錯(cuò)了得到的野指針,說(shuō)不定是隔壁的不知道什么object的成員變量,說(shuō)不定是heap里面的數(shù)據(jù)結(jié)構(gòu),或者說(shuō)別的什么東西,就這么給你寫(xiě)了。如果你寫(xiě)了別的object的成員變量那封裝肯定就不管用了,這個(gè)類(lèi)的不變量就給你破壞了。既然你的成員函數(shù)都是基于不變量來(lái)寫(xiě)的,那這個(gè)時(shí)候出錯(cuò)時(shí)必須的。如果你寫(xiě)到了heap的數(shù)據(jù)結(jié)構(gòu)那就更加呵呵呵了,說(shuō)不定下次一new就崩了,而且你還不知道為什么。
出了access violation以外的exception基本是沒(méi)什么危害的,最嚴(yán)重的大概也就是網(wǎng)線(xiàn)被拔了,另一塊不是裝OS的硬盤(pán)突然壞了什么的這種反正你也沒(méi)辦法但是好歹還可以處理的事情。如果這些exception是你自己拋出來(lái)的那就更可靠了——那都是計(jì)劃內(nèi)的。只要程序未來(lái)不會(huì)進(jìn)入access violation的狀態(tài),那證明你現(xiàn)在所能拿到的所有變量,還有指針指向的memory,基本上都還是靠譜的。出了你救不了的錯(cuò)誤,至少你還可以吧數(shù)據(jù)安全的保存下來(lái),然后讓自己重啟——就跟word一樣。但是你有可能會(huì)說(shuō),拿出了access violation怎么就不能保存數(shù)據(jù)了呢?因?yàn)檫@個(gè)時(shí)候內(nèi)存都?xì)Я耍覆欢惚4鏀?shù)據(jù)的代碼new點(diǎn)東西然后掛了,這基本上是沒(méi)準(zhǔn)的。
所以無(wú)論你喜歡exception還是喜歡error code,你所希望達(dá)到的效果本質(zhì)上就是避免程序未來(lái)會(huì)進(jìn)入access violation的狀態(tài)。想做到這一點(diǎn),方法也是很簡(jiǎn)單粗暴的——只要你在函數(shù)里面把運(yùn)行前該對(duì)函數(shù)做的檢查都查一遍就好了。這個(gè)無(wú)論你用exception還是用error code,寫(xiě)起來(lái)都是一樣的。區(qū)別在于調(diào)用你的函數(shù)的那個(gè)人會(huì)怎么樣。那么我來(lái)舉個(gè)例子,譬如說(shuō)你覺(jué)得STL的map實(shí)在是太傻比了,于是你自己寫(xiě)了一個(gè),然后有了一個(gè)這樣子的函數(shù):
// exception版本
Symbol* SymbolMap::Lookup(const wstring& name);
// error code版本
int SymbolMap::Lookup(const wstring& name, Symbol*& result);
// 其實(shí)COM就是你們最喜歡的error code風(fēng)格了,寫(xiě)起來(lái)應(yīng)該很開(kāi)心才對(duì)呀,你們的雙重標(biāo)準(zhǔn)真嚴(yán)重
HRESULT ISymbolMap::Lookup(BSTR name, ISymbol** result);
于是拿到了Lookup函數(shù)之后,我們就要開(kāi)始來(lái)完成一個(gè)任務(wù)了,譬如說(shuō)拿兩個(gè)key得到兩個(gè)symbol然后組合出一個(gè)新的symbol。函數(shù)的錯(cuò)誤處理邏輯是這樣的,如果key失敗了,因?yàn)闃I(yè)務(wù)的原因,我們要告訴函數(shù)外面說(shuō)key不存在的。調(diào)用了一個(gè)ComposeSymbol的函數(shù)丟出什么IndexOutOfRangeException顯然是不合理的。但是合并的那一步,因?yàn)闃I(yè)務(wù)都在同一個(gè)領(lǐng)域內(nèi),所以suppose里面的異常外面是可以接受的。如果出現(xiàn)了計(jì)劃外的異常,那我們是處理不了的,只能丟給上面了,外面的代碼對(duì)于不認(rèn)識(shí)的異常只需要報(bào)告任務(wù)失敗了就可以了。于是我們的函數(shù)就會(huì)這么寫(xiě):
Symbol* ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map)
{
Symbol* sa=0;
Symbol* sb=0;
try
{
sa=map->Lookup(a);
sa=map->Lookup(b);
}
catch(const IndexOutOfRangeException& ex)
{
throw SymbolKeyException(ex.GetIndex());
}
return CreatePairSymbol(sa, sb);
}
看起來(lái)還挺不錯(cuò)。現(xiàn)在我們可以開(kāi)始考慮error code的版本了。于是我們需要思考幾個(gè)問(wèn)題。首先第一個(gè)就是Lookup失敗的時(shí)候要怎么報(bào)告?直接報(bào)告key的內(nèi)容是不可能的,因?yàn)閑rror code是個(gè)int。
題外話(huà),error code當(dāng)然可以是別的什么東西,如果需要返回豐富內(nèi)容的錯(cuò)誤的話(huà),那怎樣都得是一個(gè)指針了,這個(gè)時(shí)候你們就會(huì)面臨下面的問(wèn)題——這已經(jīng)他媽不滿(mǎn)足誰(shuí)構(gòu)造誰(shuí)釋放的原則了呀,而且我這個(gè)指針究竟直接返回出去外面理不理呢,如果只要有一個(gè)環(huán)節(jié)不理了,那內(nèi)存豈不是泄露了?如果我要求把錯(cuò)誤返回在參數(shù)里面的話(huà),我每次調(diào)用函數(shù)都要?jiǎng)?chuàng)建出那么個(gè)結(jié)構(gòu)來(lái)保存異常,不僅有if的復(fù)雜度,還有創(chuàng)建空間的復(fù)雜度,整個(gè)代碼都變成了屎。所以還是老老實(shí)實(shí)用int吧……
那我們要如何把key的信息給編碼在一個(gè)int里面呢?因?yàn)閗ey要么是來(lái)自于a,要么是來(lái)自于b,所以其實(shí)我們就需要兩個(gè)code了。那Lookup的其他錯(cuò)誤怎么辦呢?CreatePairSymbol的錯(cuò)誤怎么辦呢?萬(wàn)一Lookup除了ERROR_KEY_NOT_FOUND以外,或者是CreatePairSymbol的錯(cuò)誤剛好跟a或者b的code重合了怎么辦?對(duì)于這個(gè)問(wèn)題,我只能說(shuō):
要不你們team的人先開(kāi)會(huì)討論一下最后記錄在文檔里面?zhèn)洳橐悦夂竺娴娜丝戳松笛哿恕?/font>
好了,現(xiàn)在假設(shè)說(shuō)會(huì)議取得了圓滿(mǎn)成功,會(huì)議雙方加深了互相的理解,促進(jìn)了溝通,最后還寫(xiě)了一個(gè)白皮書(shū)出來(lái),有效的落實(shí)了對(duì)a和b的code的指導(dǎo),于是我們終于可以寫(xiě)出下面的代碼了:
#define SUCCESS 0 // global error code for success
#define ERROR_COMPOSE_SYMBOL_WRONG_A 1
#define ERROR_COMPOSE_SYMBOL_WRONG_B 2
int ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map, Symbol*& result)
{
int code=SUCCESS;
Symbol* sa=0;
Symbol* sb=0;
switch(code=map->Lookup(a, sa))
{
case SUCCESS:
break;
case ERROR_SYMBOL_MAP_KEY_NOT_FOUND:
return ERROR_COMPOSE_SYMBOL_WRONG_A;
default:
return code;
}
switch(code=map->Lookup(b, sb))
{
case SUCCESS:
break;
case ERROR_SYMBOL_MAP_KEY_NOT_FOUND:
return ERROR_COMPOSE_SYMBOL_WRONG_B;
default:
return code;
}
return CreatePairSymbol(sa, sb, result);
}
啊,好像太長(zhǎng),干脆我還是不負(fù)責(zé)任一點(diǎn)吧,反正代碼寫(xiě)的好也漲不了工資,干脆不認(rèn)識(shí)的錯(cuò)誤都返回ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR好了,于是就可以把代碼變成下面這樣……都到這份上了不要叫自己程序員了,叫程序狗吧……
#define SUCCESS 0 // global error code for success
#define ERROR_COMPOSE_SYMBOL_WRONG_A 1
#define ERROR_COMPOSE_SYMBOL_WRONG_B 2
#define ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR 3
int ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map, Symbol*& result)
{
Symbol* sa=0;
Symbol* sb=0;
if(map->Lookup(a, sa)!=SUCCESS)
return ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR;
if(map->Lookup(b, sb)!=SUCCESS)
return ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR;
if(CreatePairSymbol(sa, sb, result)!=SUCCESS)
return ERROR_COMPOSE_SYMBOL_UNKNOWN_ERROR;
return SUCCESS;
}
當(dāng)然,如果大家都一樣不負(fù)責(zé)任的話(huà),還是exception完爆error code:
Symbol* ComposeSymbol(const wstring& a, const wstring& b, SymbolMap* map)
{
return CreatePairSymbol(map->Lookup(a), map->Lookup(b));
}
大部分人人只會(huì)用在當(dāng)前條件下最容易寫(xiě)的方法來(lái)設(shè)計(jì)軟件,而不是先設(shè)計(jì)出軟件然后再看看怎樣寫(xiě)比較容易,這就是為什么我說(shuō),只要你一個(gè)月給程序員還給不到一狗半,還是老老實(shí)實(shí)在政策上落實(shí)exception吧。至少exception寫(xiě)起來(lái)還不會(huì)讓人那么心煩,可以把程序?qū)懙脠?jiān)固一點(diǎn)。
好了,單線(xiàn)程下面至少你還可以爭(zhēng)吵說(shuō)究竟exception好還是error code好,但是到了異步程序里面就完全不一樣了。現(xiàn)在的異步程序都很多,譬如說(shuō)有良心的手機(jī)app啦,譬如說(shuō)javascript啦,metro程序等等。一個(gè)try根本沒(méi)辦法跨線(xiàn)程使用所以一個(gè)這樣子的函數(shù)(下面開(kāi)始用C#,C++11的future/promise我用的還不熟):
class Normal
{
public string Do(string args);
}
最后就會(huì)變成這樣:
class Async
{
// before .NET 4.0
IAsyncResult BeginDo(string args, Action<IAsyncResult> continuation);
string EndDo(IAsyncResult ar);
// after .NET 4.0
Task<string> DoAsync(string args);
}
當(dāng)你使用BeginDo的時(shí)候,你可以在continuation里面調(diào)用EndDo,然后得到一個(gè)string,或者得到一個(gè)exception。但是因?yàn)镋ndDo的exception不是在BeginDo里面throw出來(lái)的,所以無(wú)論你EndDo返回string也好,返回Tuple<string, Exception>也好,對(duì)于BeginDo和EndDo的實(shí)現(xiàn)來(lái)說(shuō)其實(shí)都一樣,沒(méi)有上文所說(shuō)的exception和error code的區(qū)別。
不過(guò).NET從BeginDo/EndDo到DoAsync經(jīng)歷了一個(gè)巨大的進(jìn)步。雖然形式上都一樣,但是由于C#并不像Haskell那樣可以完美的操作函數(shù),C#還是面向?qū)ο笞龅酶茫谑侨绻覀儼蒚ask<T>看成下面的樣子,那其實(shí)兩種寫(xiě)法是沒(méi)有區(qū)別的:
class Task<T>
{
public IAsyncResult BeginRun(Action<IAsyncResult> continuation);
public T EndRun(IAsyncResult ar);
}
不過(guò)如果還是用BeginRun/EndRun這種方法來(lái)調(diào)用的話(huà),使用起來(lái)還是很不方便,而且也很難把更多的Task組合在一起。所以最后.NET給出的Task是下面這個(gè)樣子的(Comonad!):
class Task<T>
{
public Task<U> ContinueWith<U>(Func<Task<T>, U> continuation);
}
盡管真實(shí)的Task<T>要比上面那個(gè)復(fù)雜得多,但是總的來(lái)說(shuō)其實(shí)就是圍繞著基本簡(jiǎn)單的函數(shù)建立起來(lái)的一大堆helper function。到這里C#終于把CPS變換在異步處理上的應(yīng)用的這一部分給抽象出來(lái)了。在看CPS的效果之前,我們先來(lái)看一個(gè)同步函數(shù):
void button1_Clicked(object sender, EventArgs e)
{
// 假設(shè)我們有string Http.Download(string url);
try
{
string a = Http.Download(url1);
string b = Http.Download(url2);
textBox1.Text=a+b;
}
catch(Exception ex)
{
textBox1.Text=ex.Message;
}
}
這段代碼顯然是一個(gè)GUI里面的代碼。我們?nèi)绻谝粋€(gè)GUI程序里面這么寫(xiě),就會(huì)把程序?qū)懙酶鶴Q一樣卡了。所以實(shí)際上這么做是不對(duì)的。不過(guò)為了表達(dá)程序需要做的所有事情,就有了這么一個(gè)同步的版本。那么我們嘗試吧這個(gè)東西修改成異步的把!
void button2_Clicked(object sender, EventArgs e)
{
// 假設(shè)我們有Task<string> Http.DownloadAsync(string url);
// 需要MethodInvoker是因?yàn)椋瑢?duì)textBox1.Text的修改只能在GUI線(xiàn)程里面做
Http.DownloadAsync(url1).ContinueWith(ta=>new MethodInvoker(()=>
{
try
{
// 這個(gè)時(shí)候ta已經(jīng)運(yùn)行完了,所以對(duì)ta.Result的取值不會(huì)造成GUI線(xiàn)程等待IO。
// 而且如果DownloadAsync內(nèi)部出了錯(cuò),異常會(huì)在這里拋出來(lái)。
string a=ta.Result;
Http.DownloadAsync(url2).ContinueWith(tb=>new MethodInvoker(()=>
{
try
{
string b=tb.Result;
textBox1.Text=a+b;
}
catch(Exception ex)
{
textBox1.Text=ex.Message;
}
})));
}
catch(Exception ex)
{
textBox1.Text=ex.Message;
}
})));
}
我們發(fā)現(xiàn),異步操作發(fā)生的異常,把優(yōu)越的exception拉低到了丑陋的error code的同一個(gè)情況上面——我們需要不斷地對(duì)每一個(gè)操作重復(fù)同樣的錯(cuò)誤處理過(guò)程!而且在這種地方我們連“不負(fù)責(zé)任”的選項(xiàng)都沒(méi)有了,如果你不try-catch(或者不檢查error code),那到時(shí)候程序就會(huì)發(fā)生一些莫名其妙的問(wèn)題,在GUI那一層你什么事情都不知道,整個(gè)程序就變成了傻逼。
現(xiàn)在可以開(kāi)始解釋一下什么是CPS變換了。CPS變換就是把所有g(shù)(f(x))都給改寫(xiě)成f(x, r=>g(r))的過(guò)程。通俗一點(diǎn)講,CPS變換就是幫你把那個(gè)同步的button1_Click給改寫(xiě)成異步的button2_Click的這個(gè)過(guò)程。盡管這么說(shuō)可能不太嚴(yán)謹(jǐn),因?yàn)閎utton1_Click跟button2_Click所做的事情是不一樣的,一個(gè)會(huì)讓GUI卡成qq,另一個(gè)不會(huì)。但是我們討論CPS變換的時(shí)候,我們討論的是對(duì)代碼結(jié)構(gòu)的變換,而不是別的什么東西。
現(xiàn)在就是激動(dòng)人心的一步了。既然CPS可以把返回值變換成lambda表達(dá)式,那反過(guò)來(lái)我們也可以把所有的以這種形式存在的lambda表達(dá)式都改寫(xiě)成返回值嘛。現(xiàn)在我們滾回去看一看button2_Click,會(huì)發(fā)現(xiàn)這個(gè)程序其實(shí)充滿(mǎn)了下面的pattern:
// lambda的參數(shù)名字故意起了跟前面的變量一樣的名字(previousTask)因?yàn)槠鋵?shí)他們就是同一個(gè)東西
previousTask.ContinueWith(previousTask=>new MethodInvoker(()=>
{
try
{
continuation(previousTask.Result);
}
catch(Exception ex)
{
textBox1.Text=ex.Message;
}
})));
我們可以“發(fā)明”一個(gè)語(yǔ)法來(lái)代表這個(gè)過(guò)程。C#用的是await關(guān)鍵字,那我們也來(lái)用await關(guān)鍵字。假設(shè)說(shuō)上面的代碼永遠(yuǎn)等價(jià)于下面的這個(gè)代碼:
try
{
var result=await previousTask;
continuation(result);
}
catch(Exception ex)
{
textBox1.Text=ex.Message;
}
兩段代碼的關(guān)系就跟i++;和i=i+1;一樣是可以互相替換的,只是不同的寫(xiě)法而已。那我們就可以用相同的方法來(lái)把button2_Click給替換成下面的button3_Click了:
void button3_Click(object sender, EventArgs e)
{
try
{
var a=await Http.DownloadAsync(url1);
try
{
var b=await Http.DownloadAsync(url2);
textBox1.Text=a+b;
}
catch(Exception ex)
{
textBox1.Text=ex.Message;
}
}
catch(Exception ex)
{
textBox1.Text=ex.Message;
}
}
聰明的讀者立刻就想到了,兩個(gè)try其實(shí)是重復(fù)的,那為什么不把他們合并成一個(gè)呢!當(dāng)然我想告訴大家的是,異常是在不同的線(xiàn)程里面拋出來(lái)的,只是我們用CPS變換把代碼“改寫(xiě)”成這種形式而已。理論上兩個(gè)try是不能合并的。但是!我們的C#編譯器君是很聰明的。正所謂語(yǔ)言的抽象高級(jí)了一點(diǎn),那么編譯器對(duì)你的代碼也就理解得更多了一點(diǎn)。如果編譯器發(fā)現(xiàn)你在try里面寫(xiě)了兩個(gè)await,馬上就明白了過(guò)來(lái)他需要幫你復(fù)制catch的部分——或者說(shuō)他可以幫你自動(dòng)的復(fù)制catch的部分,那情況就完全不同了,最后就可以寫(xiě)成:
// C#要求函數(shù)前面要加一個(gè)async來(lái)允許你在函數(shù)內(nèi)使用await
// 當(dāng)然同時(shí)你的函數(shù)也就返回Task而不是void了
// 不過(guò)沒(méi)關(guān)系,C#的event也可以接受一個(gè)標(biāo)記了async的函數(shù),盡管返回值不一樣
// 設(shè)計(jì)語(yǔ)言這種事情就是牽一發(fā)而動(dòng)全身呀,加個(gè)await連event都要改
async void button4_Click(object sender, EventArgs e)
{
try
{
string a=await Http.DownloadAsync(url1);
string b=await Http.DownloadAsync(url2);
textBox1.Text=a+b;
}
catch(Exception ex)
{
textBox1.Text=ex.Message;
}
}
把兩個(gè)await換成回調(diào)已經(jīng)讓我們寫(xiě)的夠辛苦了,那么如果我們把a(bǔ)wait寫(xiě)在了循環(huán)里面,事情就不那么簡(jiǎn)單了。CPS需要把循環(huán)翻譯成遞歸,那你就得把lambda表達(dá)時(shí)拿出來(lái)寫(xiě)成一個(gè)普通的函數(shù)——這樣他就可以有名字了——然后才能遞歸(寫(xiě)出一個(gè)用于CPS的Y-combinator是一件很困難的事情,盡管并沒(méi)有比Y-combinator本身困難多少)。這個(gè)例子就復(fù)雜到爆炸了,我在這里就不演示了。
總而言之,C#因?yàn)橛辛薈PS變換(await),就可以把button4_Click幫你寫(xiě)成button3_Click然后再幫你寫(xiě)成button2_Click,最后把整個(gè)函數(shù)變成異步和回調(diào)的形式(真正的做法要更聰明一點(diǎn),大家可以反編譯去看)在異步回調(diào)的寫(xiě)法里面,exception和error code其實(shí)是一樣的。但是CPS+exception和CPS+error code就跟單線(xiàn)程下面的exception和error code一樣,有著重大的區(qū)別。這就是為什么文章一開(kāi)始會(huì)說(shuō),我只會(huì)在帶CPS變換的語(yǔ)言(Haskell/F#/etc)里面使用error code。
在這類(lèi)語(yǔ)言里面利用相同的技巧,就可以不是異步的東西也用CPS包裝起來(lái),譬如說(shuō)monadic parser combinator。至于你要選擇monad還是comonad,基本上就是取決于你要自動(dòng)提供錯(cuò)誤處理還是要手動(dòng)提供錯(cuò)誤處理。像上面的Task.ContinueWith,是要求你手動(dòng)提供錯(cuò)誤處理的(因?yàn)槟鉩atch了之后可以干別的事情,Task無(wú)法自動(dòng)替你選擇最好的措施),所以他就把Task.ContinueWith寫(xiě)成了comonad的那個(gè)樣子。
寫(xiě)到這里,不禁要同情寫(xiě)前端的那幫javascript和自以為可以寫(xiě)后端的node.js愛(ài)好者們,你們因?yàn)樾⌒〉膃val的問(wèn)題,不用老趙的windjs(windjs給javascript加上了await但是它不是一個(gè)altjs所以得顯式調(diào)用eval),是一個(gè)多大的損失……
posted on 2013-06-09 23:01
陳梓瀚(vczh) 閱讀(13994)
評(píng)論(8) 編輯 收藏 引用 所屬分類(lèi):
啟示