欧美极品高清xxxxhd,国产日产欧美最新,无码AV国产东京热AV无码,国产精品人与动性XXX,国产传媒亚洲综合一区二区,四库影院永久国产精品,毛片免费免费高清视频,福利所导航夜趣136

標題: 編程的智慧—王垠 [打印本頁]

作者: 51黑論壇迷    時間: 2015-12-20 00:16
標題: 編程的智慧—王垠
文章有點長,不過耐心看完,還是有很大幫助的
編程是一種創造性的工作,是一門藝術。精通任何一門藝術,都需要很多的練習和領悟,所以這里提出的“智慧”,并不是號稱一天瘦十斤的減肥藥,它并不能代替你自己的勤奮。然而由于軟件行業喜歡標新立異,喜歡把簡單的事情搞復雜,我希望這些文字能給迷惑中的人們指出一些正確的方向,讓他們少走一些彎路,基本做到一分耕耘一分收獲。
反復推敲代碼
既然“天才是百分之一的靈感,百分之九十九的汗水”,那我先來談談這汗水的部分吧。有人問我,提高編程水平最有效的辦法是什么?我想了很久,終于發現最有效的辦法,其實是反反復復地修改和推敲代碼。
在IU的時候,由于Dan Friedman的嚴格教導,我們以寫出冗長復雜的代碼為恥。如果你代碼多寫了幾行,這老頑童就會大笑,說:“當年我解決這個問題,只寫了5行代碼,你回去再想想吧……” 當然,有時候他只是夸張一下,故意刺激你的,其實沒有人能只用5行代碼完成。然而這種提煉代碼,減少冗余的習慣,卻由此深入了我的骨髓。
有些人喜歡炫耀自己寫了多少多少萬行的代碼,仿佛代碼的數量是衡量編程水平的標準。然而,如果你總是匆匆寫出代碼,卻從來不回頭去推敲,修改和提煉,其實是不可能提高編程水平的。你會制造出越來越多平庸甚至糟糕的代碼。在這種意義上,很多人所謂的“工作經驗”,跟他代碼的質量,其實不一定成正比。如果有幾十年的工作經驗,卻從來不回頭去提煉和反思自己的代碼,那么他也許還不如一個只有一兩年經驗,卻喜歡反復推敲,仔細領悟的人。
有位文豪說得好:“看一個作家的水平,不是看他發表了多少文字,而要看他的廢紙簍里扔掉了多少! 我覺得同樣的理論適用于編程。好的程序員,他們刪掉的代碼,比留下來的還要多很多。如果你看見一個人寫了很多代碼,卻沒有刪掉多少,那他的代碼一定有很多垃圾。
就像文學作品一樣,代碼是不可能一蹴而就的。靈感似乎總是零零星星,陸陸續續到來的。任何人都不可能一筆呵成,就算再厲害的程序員,也需要經過一段時間,才能發現最簡單優雅的寫法。有時候你反復提煉一段代碼,覺得到了頂峰,沒法再改進了,可是過了幾個月再回頭來看,又發現好多可以改進和簡化的地方。這跟寫文章一模一樣,回頭看幾個月或者幾年前寫的東西,你總能發現一些改進。
所以如果反復提煉代碼已經不再有進展,那么你可以暫時把它放下。過幾個星期或者幾個月再回頭來看,也許就有煥然一新的靈感。這樣反反復復很多次之后,你就積累起了靈感和智慧,從而能夠在遇到新問題的時候直接朝正確,或者接近正確的方向前進。
寫優雅的代碼
人們都討厭“面條代碼”(spaghetti code),因為它就像面條一樣繞來繞去,沒法理清頭緒。那么優雅的代碼一般是什么形狀的呢?經過多年的觀察,我發現優雅的代碼,在形狀上有一些明顯的特征。
如果我們忽略具體的內容,從大體結構上來看,優雅的代碼看起來就像是一些整整齊齊,套在一起的盒子。如果跟整理房間做一個類比,就很容易理解。如果你把所有物品都丟在一個很大的抽屜里,那么它們就會全都混在一起。你就很難整理,很難迅速的找到需要的東西。但是如果你在抽屜里再放幾個小盒子,把物品分門別類放進去,那么它們就不會到處亂跑,你就可以比較容易的找到和管理它們。
優雅的代碼的另一個特征是,它的邏輯大體上看起來,是枝丫分明的樹狀結構(tree)。這是因為程序所做的幾乎一切事情,都是信息的傳遞和分支。你可以把代碼看成是一個電路,電流經過導線,分流或者匯合。如果你是這樣思考的,你的代碼里就會比較少出現只有一個分支的if語句,它看起來就會像這個樣子:
if (...) {  if (...) {    ...  } else {    ...  }} else if (...) {  ...} else {  ...}
注意到了嗎?在我的代碼里面,if語句幾乎總是有兩個分支。它們有可能嵌套,有多層的縮進,而且else分支里面有可能出現少量重復的代碼。然而這樣的結構,邏輯卻非常嚴密和清晰。在后面我會告訴你為什么if語句最好有兩個分支。
寫模塊化的代碼
有些人吵著鬧著要讓程序“模塊化”,結果他們的做法是把代碼分部到多個文件和目錄里面,然后把這些目錄或者文件叫做“module”。他們甚至把這些目錄分放在不同的VCS repo里面。結果這樣的作法并沒有帶來合作的流暢,而是帶來了許多的麻煩。這是因為他們其實并不理解什么叫做“模塊”,膚淺的把代碼切割開來,分放在不同的位置,其實非但不能達到模塊化的目的,而且制造了不必要的麻煩。
真正的模塊化,并不是文本意義上的,而是邏輯意義上的。一個模塊應該像一個電路芯片,它有定義良好的輸入和輸出。實際上一種很好的模塊化方法早已經存在,它的名字叫做“函數”。每一個函數都有明確的輸入(參數)和輸出(返回值),同一個文件里可以包含多個函數,所以你其實根本不需要把代碼分開在多個文件或者目錄里面,同樣可以完成代碼的模塊化。我可以把代碼全都寫在同一個文件里,卻仍然是非常模塊化的代碼。
想要達到很好的模塊化,你需要做到以下幾點:
寫可讀的代碼
有些人以為寫很多注釋就可以讓代碼更加可讀,然而卻發現事與愿違。注釋不但沒能讓代碼變得可讀,反而由于大量的注釋充斥在代碼中間,讓程序變得障眼難讀。而且代碼的邏輯一旦修改,就會有很多的注釋變得過時,需要更新。修改注釋是相當大的負擔,所以大量的注釋,反而成為了妨礙改進代碼的絆腳石。
實際上,真正優雅可讀的代碼,是幾乎不需要注釋的。如果你發現需要寫很多注釋,那么你的代碼肯定是含混晦澀,邏輯不清晰的。其實,程序語言相比自然語言,是更加強大而嚴謹的,它其實具有自然語言最主要的元素:主語,謂語,賓語,名詞,動詞,如果,那么,否則,是,不是,…… 所以如果你充分利用了程序語言的表達能力,你完全可以用程序本身來表達它到底在干什么,而不需要自然語言的輔助。
有少數的時候,你也許會為了繞過其他一些代碼的設計問題,采用一些違反直覺的作法。這時候你可以使用很短注釋,說明為什么要寫成那奇怪的樣子。這樣的情況應該少出現,否則這意味著整個代碼的設計都有問題。
如果沒能合理利用程序語言提供的優勢,你會發現程序還是很難懂,以至于需要寫注釋。所以我現在告訴你一些要點,也許可以幫助你大大減少寫注釋的必要:
有些人喜歡利用IDE的自動換行機制,編輯之后用一個熱鍵把整個代碼重新格式化一遍,IDE就會自動的把超過行寬限制的代碼自動折行。可是這種自動這行,往往沒有根據代碼的邏輯和讀者的理解來進行。你有可能得到這樣的代碼:
   if (someLongCondition1() && someLongCondition2() && someLongCondition3() &&      someLongCondition4()) {     ...   }
由于someLongCondition4()超過了行寬限制,被編輯器自動換到了下面一行。雖然這樣滿足了行寬限制,可是卻是相當任意的。它并不能幫助人理解這代碼的邏輯。這幾個boolean表達式,全都用&&連接,所以它們其實處于平等的地位。為了表達這一點,當這行太長需要折行的時候,你應該把每一個表達式都做成新的一行,就像這個樣子:
   if (someLongCondition1() &&        someLongCondition2() &&        someLongCondition3() &&        someLongCondition4()) {     ...   }
這樣每一個條件都對齊,里面的邏輯就很清楚了。再舉個例子:
   log.info("failed to find file {} for command {}, with exception {}", file, command,     exception);
這行因為太長,被自動折行成這個樣子。它就不如折成這個樣子:
   log.info("failed to find file {} for command {}, with exception {}",     file, command, exception);
把格式字符串單獨放在一行,而把它的參數放在另外一樣,這樣邏輯就更加清晰。
為了避免IDE把這些手動調整好的換行弄亂,很多IDE(比如IntelliJ)的自動格式化設定里都有“保留原來的換行符”的設定。如果你發現IDE的換行不符合邏輯,你可以修改這些設定,然后在某些地方保留你自己的手動換行。
說到這里,我必須警告你,這里所說的“不需注釋,讓代碼自己解釋自己”,并不是說要讓代碼看起來像某種自然語言。有個叫Chai的JavaScript測試工具,可以讓你這樣寫代碼:
expect(foo).to.be.a('string');expect(foo).to.equal('bar');expect(foo).to.have.length(3);expect(tea).to.have.property('flavors').with.length(3);
這種做法是極其錯誤的。程序語言本來就比自然語言簡單清晰,這種寫法讓它看起來像自然語言的樣子,反而變得復雜難懂了。
寫簡單的代碼
程序語言都喜歡標新立異,提供這樣那樣的“特性”,然而有些特性其實并不是什么好東西。很多特性都經不起時間的考驗,最后帶來的麻煩,比解決的問題還多。很多人盲目的追求“短小”和“精悍”,或者為了顯示自己頭腦聰明,學得快,所以喜歡利用語言里的一些特殊構造,寫出過于“聰明”,難以理解的代碼。
并不是語言提供什么,你就一定要把它用上的。實際上你只需要其中很小的一部分功能,就能寫出優秀的代碼。我一向反對“充分利用”程序語言里的所有特性。實際上,我心目中有一套最好的構造。不管語言提供了多么“神奇”的,“新”的特性,我基本都只用經過千錘百煉,我覺得值得信奈的那一套。
現在針對一些有問題的語言特性,我介紹一些我自己使用的代碼規范,并且講解一下為什么它們能讓代碼更簡單。
寫直觀的代碼
我寫代碼有一條重要的原則:如果有更加直接,更加清晰的寫法,就選擇它,即使它看起來更長,更笨,也一樣選擇它。比如,Unix命令行有一種“巧妙”的寫法是這樣:
command1 && command2 && command3
由于Shell語言的邏輯操作a && b具有“短路”的特性,如果a等于false,那么b就沒必要執行了。這就是為什么當command1成功,才會執行command2,當command2成功,才會執行command3。同樣,
command1 || command2 || command3
操作符||也有類似的特性。上面這個命令行,如果command1成功,那么command2和command3都不會被執行。如果command1失敗,command2成功,那么command3就不會被執行。
這比起用if語句來判斷失敗,似乎更加巧妙和簡潔,所以有人就借鑒了這種方式,在程序的代碼里也使用這種方式。比如他們可能會寫這樣的代碼:
if (action1() || action2() && action3()) {  ...}
你看得出來這代碼是想干什么嗎?action2和action3什么條件下執行,什么條件下不執行?也許稍微想一下,你知道它在干什么:“如果action1失敗了,執行action2,如果action2成功了,執行action3”。然而那種語義,并不是直接的“映射”在這代碼上面的。比如“失敗”這個詞,對應了代碼里的哪一個字呢?你找不出來,因為它包含在了||的語義里面,你需要知道||的短路特性,以及邏輯或的語義才能知道這里面在說“如果action1失敗……”。每一次看到這行代碼,你都需要思考一下,這樣積累起來的負荷,就會讓人很累。
其實,這種寫法是濫用了邏輯操作&&和||的短路特性。這兩個操作符可能不執行右邊的表達式,原因是為了機器的執行效率,而不是為了給人提供這種“巧妙”的用法。這兩個操作符的本意,只是作為邏輯操作,它們并不是拿來給你代替if語句的。也就是說,它們只是碰巧可以達到某些if語句的效果,但你不應該因此就用它來代替if語句。如果你這樣做了,就會讓代碼晦澀難懂。
上面的代碼寫成笨一點的辦法,就會清晰很多:
if (!action1()) {  if (action2()) {    action3();  }}
這里我很明顯的看出這代碼在說什么,想都不用想:如果action1()失敗了,那么執行action2(),如果action2()成功了,執行action3()。你發現這里面的一一對應關系嗎?if=如果,!=失敗,…… 你不需要利用邏輯學知識,就知道它在說什么。
寫無懈可擊的代碼
在之前一節里,我提到了自己寫的代碼里面很少出現只有一個分支的if語句。我寫出的if語句,大部分都有兩個分支,所以我的代碼很多看起來是這個樣子:
if (...) {  if (...) {    ...    return false;  } else {    return true;  }} else if (...) {  ...  return false;} else {  return true;}
使用這種方式,其實是為了無懈可擊的處理所有可能出現的情況,避免漏掉corner case。每個if語句都有兩個分支的理由是:如果if的條件成立,你做某件事情;但是如果if的條件不成立,你應該知道要做什么另外的事情。不管你的if有沒有else,你終究是逃不掉,必須得思考這個問題的。
很多人寫if語句喜歡省略else的分支,因為他們覺得有些else分支的代碼重復了。比如我的代碼里,兩個else分支都是return true。為了避免重復,他們省略掉那兩個else分支,只在最后使用一個return true。這樣,缺了else分支的if語句,控制流自動“掉下去”,到達最后的return true。他們的代碼看起來像這個樣子:
if (...) {  if (...) {    ...    return false;  } } else if (...) {  ...  return false;} return true;
這種寫法看似更加簡潔,避免了重復,然而卻很容易出現疏忽和漏洞。嵌套的if語句省略了一些else,依靠語句的“控制流”來處理else的情況,是很難正確的分析和推理的。如果你的if條件里使用了&&和||之類的邏輯運算,就更難看出是否涵蓋了所有的情況。
由于疏忽而漏掉的分支,全都會自動“掉下去”,最后返回意想不到的結果。即使你看一遍之后確信是正確的,每次讀這段代碼,你都不能確信它照顧了所有的情況,又得重新推理一遍。這簡潔的寫法,帶來的是反復的,沉重的頭腦開銷。這就是所謂“面條代碼”,因為程序的邏輯分支,不是像一棵枝葉分明的樹,而是像面條一樣繞來繞去。
另外一種省略else分支的情況是這樣:
String s = "";if (x < 5) {  s = "ok";}
寫這段代碼的人,腦子里喜歡使用一種“缺省值”的做法。s缺省為null,如果x<5,那么把它改變(mutate)成“ok”。這種寫法的缺點是,當x<5不成立的時候,你需要往上面看,才能知道s的值是什么。這還是你運氣好的時候,因為s就在上面不遠。很多人寫這種代碼的時候,s的初始值離判斷語句有一定的距離,中間還有可能插入一些其它的邏輯和賦值操作。這樣的代碼,把變量改來改去的,看得人眼花,就容易出錯。
現在比較一下我的寫法:
String s;if (x < 5) {  s = "ok";} else {  s = "";}
這種寫法貌似多打了一兩個字,然而它卻更加清晰。這是因為我們明確的指出了x<5不成立的時候,s的值是什么。它就擺在那里,它是""(空字符串)。注意,雖然我也使用了賦值操作,然而我并沒有“改變”s的值。s一開始的時候沒有值,被賦值之后就再也沒有變過。我的這種寫法,通常被叫做更加“函數式”,因為我只賦值一次。
如果我漏寫了else分支,Java編譯器是不會放過我的。它會抱怨:“在某個分支,s沒有被初始化!边@就強迫我清清楚楚的設定各種條件下s的值,不漏掉任何一種情況。
當然,由于這個情況比較簡單,你還可以把它寫成這樣:
String s = x < 5 ? "ok" : "";
對于更加復雜的情況,我建議還是寫成if語句為好。
正確處理錯誤
使用有兩個分支的if語句,只是我的代碼可以達到無懈可擊的其中一個原因。這樣寫if語句的思路,其實包含了使代碼可靠的一種通用思想:窮舉所有的情況,不漏掉任何一個。
程序的絕大部分功能,是進行信息處理。從一堆紛繁復雜,模棱兩可的信息中,排除掉絕大部分“干擾信息”,找到自己需要的那一個。正確地對所有的“可能性”進行推理,就是寫出無懈可擊代碼的核心思想。這一節我來講一講,如何把這種思想用在錯誤處理上。
錯誤處理是一個古老的問題,可是經過了幾十年,還是很多人沒搞明白。Unix的系統API手冊,一般都會告訴你可能出現的返回值和錯誤信息。比如,Linux的read系統調用手冊里面有如下內容:
RETURN VALUE On success, the number of bytes read is returned... On error, -1 is returned, and errno is set appropriately.ERRORSEAGAIN, EBADF, EFAULT, EINTR, EINVAL, ...
很多初學者,都會忘記檢查read的返回值是否為-1,覺得每次調用read都得檢查返回值真繁瑣,不檢查貌似也相安無事。這種想法其實是很危險的。如果函數的返回值告訴你,要么返回一個正數,表示讀到的數據長度,要么返回-1,那么你就必須要對這個-1作出相應的,有意義的處理。千萬不要以為你可以忽視這個特殊的返回值,因為它是一種“可能性”。代碼漏掉任何一種可能出現的情況,都可能產生意想不到的災難性結果。
對于Java來說,這相對方便一些。Java的函數如果出現問題,一般通過異常(exception)來表示。你可以把異常加上函數本來的返回值,看成是一個“union類型”。比如:
String foo() throws MyException {  ...}
這里MyException是一個錯誤返回。你可以認為這個函數返回一個union類型:{String, MyException}。任何調用foo的代碼,必須對MyException作出合理的處理,才有可能確保程序的正確運行。Union類型是一種相當先進的類型,目前只有極少數語言(比如Typed Racket)具有這種類型,我在這里提到它,只是為了方便解釋概念。掌握了概念之后,你其實可以在頭腦里實現一個union類型系統,這樣使用普通的語言也能寫出可靠的代碼。
由于Java的類型系統強制要求函數在類型里面聲明可能出現的異常,而且強制調用者處理可能出現的異常,所以基本上不可能出現由于疏忽而漏掉的情況。但有些Java程序員有一種惡習,使得這種安全機制幾乎完全失效。每當編譯器報錯,說“你沒有catch這個foo函數可能出現的異!睍r,有些人想都不想,直接把代碼改成這樣:
try {  foo();} catch (Exception e) {}
或者最多在里面放個log,或者干脆把自己的函數類型上加上throws Exception,這樣編譯器就不再抱怨。這些做法貌似很省事,然而都是錯誤的,你終究會為此付出代價。
如果你把異常catch了,忽略掉,那么你就不知道foo其實失敗了。這就像開車時看到路口寫著“前方施工,道路關閉”,還繼續往前開。這當然遲早會出問題,因為你根本不知道自己在干什么。
catch異常的時候,你不應該使用Exception這么寬泛的類型。你應該正好catch可能發生的那種異常A。使用寬泛的異常類型有很大的問題,因為它會不經意的catch住另外的異常(比如B)。你的代碼邏輯是基于判斷A是否出現,可你卻catch所有的異常(Exception類),所以當其它的異常B出現的時候,你的代碼就會出現莫名其妙的問題,因為你以為A出現了,而其實它沒有。這種bug,有時候甚至使用debugger都難以發現。
如果你在自己函數的類型加上throws Exception,那么你就不可避免的需要在調用它的地方處理這個異常,如果調用它的函數也寫著throws Exception,這毛病就傳得更遠。我的經驗是,盡量在異常出現的當時就作出處理。否則如果你把它返回給你的調用者,它也許根本不知道該怎么辦了。
另外,try { ... } catch里面,應該包含盡量少的代碼。比如,如果foo和bar都可能產生異常A,你的代碼應該盡可能寫成:
try {  foo();} catch (A e) {...}try {  bar();} catch (A e) {...}
而不是
try {  foo();  bar();} catch (A e) {...}
第一種寫法能明確的分辨是哪一個函數出了問題,而第二種寫法全都混在一起。明確的分辨是哪一個函數出了問題,有很多的好處。比如,如果你的catch代碼里面包含log,它可以提供給你更加精確的錯誤信息,這樣會大大地加速你的調試過程。
正確處理null指針
窮舉的思想是如此的有用,依據這個原理,我們可以推出一些基本原則,它們可以讓你無懈可擊的處理null指針。
首先你應該知道,許多語言(C,C++,Java,C#,……)的類型系統對于null的處理,其實是完全錯誤的。這個錯誤源自于Tony Hoare最早的設計,Hoare把這個錯誤稱為自己的“billion dollar mistake”,因為由于它所產生的財產和人力損失,遠遠超過十億美元。
這些語言的類型系統允許null出現在任何對象(指針)類型可以出現的地方,然而null其實根本不是一個合法的對象。它不是一個String,不是一個Integer,也不是一個自定義的類。null的類型本來應該是NULL,也就是null自己。根據這個基本觀點,我們推導出以下原則:
防止過度工程
人的腦子真是奇妙的東西。雖然大家都知道過度工程(over-engineering)不好,在實際的工程中卻經常不由自主的出現過度工程。我自己也犯過好多次這種錯誤,所以覺得有必要分析一下,過度工程出現的信號和兆頭,這樣可以在初期的時候就及時發現并且避免。
過度工程即將出現的一個重要信號,就是當你過度的思考“將來”,考慮一些還沒有發生的事情,還沒有出現的需求。比如,“如果我們將來有了上百萬行代碼,有了幾千號人,這樣的工具就支持不了了”,“將來我可能需要這個功能,所以我現在就把代碼寫來放在那里”,“將來很多人要擴充這片代碼,所以現在我們就讓它變得可重用”……
這就是為什么很多軟件項目如此復雜。實際上沒做多少事情,卻為了所謂的“將來”,加入了很多不必要的復雜性。眼前的問題還沒解決呢,就被“將來”給拖垮了。人們都不喜歡目光短淺的人,然而在現實的工程中,有時候你就是得看近一點,把手頭的問題先搞定了,再談以后擴展的問題。
另外一種過度工程的來源,是過度的關心“代碼重用”。很多人“可用”的代碼還沒寫出來呢,就在關心“重用”。為了讓代碼可以重用,最后被自己搞出來的各種框架捆住手腳,最后連可用的代碼就沒寫好。如果可用的代碼都寫不好,又何談重用呢?很多一開頭就考慮太多重用的工程,到后來被人完全拋棄,沒人用了,因為別人發現這些代碼太難懂了,自己從頭開始寫一個,反而省好多事。
過度地關心“測試”,也會引起過度工程。有些人為了測試,把本來很簡單的代碼改成“方便測試”的形式,結果引入很多復雜性,以至于本來一下就能寫對的代碼,最后復雜不堪,出現很多bug。
世界上有兩種“沒有bug”的代碼。一種是“沒有明顯的bug的代碼”,另一種是“明顯沒有bug的代碼”。第一種情況,由于代碼復雜不堪,加上很多測試,各種coverage,貌似測試都通過了,所以就認為代碼是正確的。第二種情況,由于代碼簡單直接,就算沒寫很多測試,你一眼看去就知道它不可能有bug。你喜歡哪一種“沒有bug”的代碼呢?
根據這些,我總結出來的防止過度工程的原則如下:








歡迎光臨 (http://www.raoushi.com/bbs/) Powered by Discuz! X3.1