編程時來點小叛逆 是不是超愛這10種壞習慣?

這些事兒我們都幹過:比如趁媽媽不注意趕緊偷吃一塊餅乾;比如開車過急轉彎不減速。當然,編程的時候我們也愛來點小叛逆。

我們對好的編程規則不屑一顧,打出來的代碼非常糟糕——可也就這麼對付過去了,並沒有編程之神用閃電懲罰我,我們的電腦也沒有爆炸。實際上,我們的代碼已編譯完成並交付使用,客戶也似乎很滿意。

那是因爲糟糕的代碼並不像摸電門或者摸老虎屁股那樣後果嚴重,大多數時候這些代碼都能用。編程規則往往只是指引或對編程風格的建議,並不會出現不遵守代碼就會完蛋的鐵律。當然,你的代碼可能會被同行嘲笑,甚至是“公開處刑“。但違反約定也是一種有點刺激的反叛行爲;這種叛逆行爲往往是產生優秀代碼的一種途徑,哪怕你不是故意爲之。

讓事情變得更復雜的是,有時打破規則纔是更好的選擇。這樣的代碼沒準兒會更清晰,甚至可以更快更簡潔。規則往往太過寬泛,熟練的程序員可以爲了改進代碼而打破常規。有時候用自己的方式編程是很有意義的,不過這一條可別跟你的老闆說。

下面是10條規則列表,其中也有被認爲是不可逾越的規則,但我們很多人經常會打破這些規則,而且結果還很不錯。

TOP1:粘貼複製

在學生時代,我們都知道抄襲是不對的。但在工作中,這方面的規則還很模糊。雖然有些代碼塊是不能盜用的——不要把專有代碼拷貝到你的堆棧中,尤其是這些代碼有標記版權信息。這種時候你應該編寫自己的版本,老闆付你薪水就是要做正事的。

但是當原始創作者想要共享代碼時,問題就變得複雜了。這些共享代碼也許放到了某個在線編程論壇上,也許它們是帶有許可證(BSD,MIT)的開放源代碼,允許使用一到三個函數。你使用這些共享代碼是沒有問題的,而且你上班是爲了解決問題,而不是重新發明輪子。

大多數情況下,複製代碼的優勢非常明顯,小心對待的話問題也不大。至少那些從靠譜的來源獲得的代碼已經被大致“檢查“過了。

問題的複雜之處在於,這些共享代碼是否存在一些未發現的錯誤,代碼的用途或底層數據是否存在一些特別的假設。也許你的代碼混入了空指針,而原始代碼從未檢查過。如果你能解決這些問題,那麼就可以理解爲你的老闆得到了兩位程序員共同努力的成果。這就是某種形式的結對編程,而且用不着什麼高大上的辦公桌。

TOP2:非函數式代碼

在過去十年間,函數範式愈加流行。喜歡用嵌套函數調用來構建程序的人們引用了很多研究成果。這些研究表明,與舊式的變量和循環相比,函數式編程代碼更安全,錯誤更少,而且可以隨程序員的喜好任意組合在一起。粉絲們十分追捧函數式編程,還會在代碼審查和拉取請求中詆譭非函數式方法。關於這種方法的優勢,他們的觀點其實並沒有錯。

但有時你需要的僅僅是一卷膠帶而已。精心設計並細心計劃的代碼需要花費很多時間,不僅需要花費時間想象,還需要構建和之後導航的時間。這些都增加了複雜性,並且會花費很多的時間與精力。開發漂亮的函數式代碼需要提前做計劃,還要確保所有數據都通過正確的途徑傳遞。有時找出並更改變量會簡單得多,可能再加個註釋說明一下就夠了。就算要在註釋中爲之後的程序員致以冗長而難懂的歉意,也比重新設計整個系統,把它扳回正軌上要省事得多。

不良的編程習慣第3位:非標準間距

軟件中的大多數空格都不會影響程序的性能。除少數使用間距指示代碼塊的語言(如Python)外,大多數空格對程序行爲的影響爲零。儘管如此,仍然有一些得了強迫症的程序員會數空格,並堅持認爲它們很重要。曾有這樣一位程序員以最嚴肅的口吻告訴我的老闆,說我正在寫“非標準代碼”,還說他一眼就看出來了。我的錯咯?因爲我沒在等號的兩側放置空格,違反了ESLint space-infix-ops規則。

有時候你只要操心那些更深層的內容就行了,誰管什麼空格的位置。也許你擔心數據庫過載,也許你擔心空指針可能會讓你的代碼崩潰。一套代碼中,幾乎所有的部分都比空格更重要,就算那些喜歡走形式的標準委員會寫出來一大堆規則來限制這些空格或製表符的位置,那又如何呢。

令人欣喜的是,網上可以找到一些很好用的工具來自動重新格式化你的代碼,讓你的代碼遵守所有精心定義的linting規則。人類不應該在這種事情上浪費時間和腦細胞。如果這些規則這麼重要,我們就應該用工具來解決這些問題。

不良的編程習慣第4位:使用goto

禁止使用goto的規則可以追溯到許多結構化編程工具還沒有出現的時代。如果程序員想創建一個循環或跳轉到另一個例程,則需要鍵入GOTO,後跟一個行號。多年之後,編譯器團隊開始允許程序員使用字符串標籤來代替行號。當時這被認爲是一項熱門的新特性。

有的人把這樣做法的結果稱爲“意大利麪條式代碼”。以後沒人能讀懂你的代碼,沒人搞得清楚執行路徑。那會是一團混亂的線程,永遠纏結在一起。Edsger Dijkstra以題爲“我們認爲Goto聲明是有害的”的一篇文章禁止大家使用這個命令。

但是絕對分支並不是問題所在,問題在於它產生的那堆糾纏的結果。一般來說,精心設計的break或return能提供有關該位置的代碼執行情況的非常清晰的陳述。有時,將goto添加到一個case語句中所生成的東西,比級聯if-then-else塊的,結構更正確的列表理解起來更容易。

也有反例。蘋果SSL堆棧中的“goto fail”安全漏洞就是一個很好的例子。但是,如果我們謹慎地避免case語句和循環中出現的一些問題,我們就可以插入很好用的絕對跳轉,使代碼讀者更容易理解正在發生的事情。有時我們可以放一個break或return,不僅更簡潔,而且大家讀起來更愉快——當然那些討厭goto的人們除外

不良的編程習慣第5位:不聲明類型

熱愛類型化語言的人們有他們的理由。當我們爲每個變量的數據類型添加清晰的聲明時,我們會編寫更好,錯誤更少的代碼。花點時間來闡明類型,就可以幫助編譯器在代碼開始運行之前標記出愚蠢的錯誤。這可能會很痛苦,但也會有回報。這是一種編程的笨辦法,就是爲了避免錯誤。

時代變了。許多較新的編譯器已經足夠聰明瞭,它們可以在查看代碼時推斷出類型。它們可以在代碼中前後移動,最後確認變量應該是string或int,抑或是其他類型。而且,如果推斷出來的這些類型沒法對齊,則編譯器會給出錯誤標誌。它們不需要我們再類型化變量了。

換句話說,我們可以省略一些最簡單的聲明,然後就能輕鬆節省一些時間了。代碼變得更簡潔,代碼讀者也往往能猜出for循環中名爲i的變量是一個整數。

不良的編程習慣第6位:溜溜球代碼

程序員喜歡將其稱爲“yo-yo代碼”。首先,這些值將存儲爲字符串,然後將它們解析爲整數,接下來將它們轉換回字符串。這種方法效率極低。你幾乎能感受到一大堆額外負載讓CPU不堪重負的樣子。能快速編寫代碼的聰明程序員會調整自己的代碼架構,以最大程度地減少轉換。因爲他們安排好了計劃,他們的代碼也能跑得更快。

但不管你信不信,有時溜溜球代碼也是有意義的。有時,你要用到一個瘋狂的庫,它在自己的黑匣子裏會搞定一大堆智能操作。有時老闆花了很多錢,請好多天才做出來這麼一個庫。如果這個庫需要字符串形式的數據,那麼你就得給它字符串,哪怕你最近剛把數據轉換爲整數也得再轉回去。

當然,你可以重寫所有代碼以最大程度地減少轉換,但這會花費一些時間。有時,代碼多運行一分鐘、一小時、一天甚至一週也是可以接受的,因爲重寫代碼會花費更多時間。有時候,增加技術債務要比重新建立一筆技術債的成本更低些。

有時這種庫裏面不是專有代碼,而是你很久以前編寫的代碼。有時,轉換一次數據要比重寫該庫中的所有內容更省事。這種時候你就可以編寫悠悠球代碼了,不要怕,我們都遇到過這種事情。

不良的編程習慣第7位:編寫自己的數據結構

有一條標準規則是,程序員在大二學完數據結構課程後,再也不要編寫用於存儲數據的代碼了。已經有人編寫過了我們所需要的所有數據結構,並且他們的代碼經過了多年的測試和重新測試。這些結構與語言打包在一起,還可能是免費的。你自己寫的代碼只會是一堆錯誤。

但有的時候數據結構庫的速度有點緩慢。有時候我們被迫使用的標準結構並不適合我們自己的代碼。有時,庫會要求我們在使用它的結構之前重新配置數據。有時,這些庫帶有笨重的保護,還有一些諸如線程鎖定之類的特性,而我們的代碼並不需要它們。

發生這種情況時就該編寫我們自己的數據結構了。有時我們自己的結構會快很多,還可能讓我們的代碼更整潔,因爲我們不需要一大堆額外的代碼來重新精確地格式化數據。

不良的編程習慣第8位:老式循環

很久以前,創建C語言的某人想將所有抽象可能性封裝在一個簡單的構造中。這個構造開始時要做一些事情,每次循環都要做一些事情,所有事情都完成時還有一些方法來提示我們。當時,這似乎是一種擁有無限可能性的完美語法。

此一時彼一時。如今一些現代評論者只看到了其中的麻煩。發生的事情太多了。所有這些可能性既可能向善也可能作惡。這種構造讓閱讀和理解代碼變得非常困難。他們喜歡更加函數式的的範式,其中沒有循環,只有應用到列表的函數,還有映射到某些數據的計算模板。

有時無循環方法更簡潔,尤其是當我們只有一個簡單的函數和一個數組的時候。但還有些時候,老式的循環要簡單得多,因爲它可以做更多事情。例如,當你找到第一個匹配項後就立刻停止搜索,這樣的代碼就簡單得多。

此外,要對數據執行多項操作時,映射函數會要求更嚴格的編碼。假設你要對每個數字取絕對值,然後取平方根,最快的方案是先映射第一個函數,然後映射第二個函數,將數據循環兩次。

不良的編程習慣第9位:在中間打破循環

從有一天開始,一個規則制定小組宣佈每個循環都應該有一個“不變項”,就是一個在整個循環中都爲真的邏輯語句。當不變量不再爲真時,循環就結束了。這是處理複雜循環的好方法,但會帶來一些令人抓狂的約束,例如禁止我們在循環中間使用return或break。這條規則是禁止goto語句規則的子集。

這個理論很不錯,但它通常會導致代碼變得更復雜。考慮以下這種簡單的情況,其中會掃描一個數組,找出通過測試的一個條目:

while (i<a.length){
   ...
   if (test(a[i]) then return a[i];
   ...
}

喜歡循環不變項的人們寧願我們添加另一個布爾變量,將其稱爲notFound,然後這樣用它:

while ((notFound) && (i<a.length){
   ...
   if (test(a[i])) then notFound=false;
   ...
}

如果這個布爾名稱取得很合適,那就會是一段自我註釋得很好的代碼。它可以讓大家理解起來更容易。但這也增加了複雜性。這還意味着要分配另一個局部變量並阻塞一個寄存器,編譯器可能沒那麼聰明,沒法修復這個錯誤。

有時goto或jump會更簡潔。

不良的編程習慣第10位:重新定義運算符和函數

一些最有趣的語言會讓你繞一些大彎子,比如說重新定義看起來應該是常量的元素值。拿Python來說,至少在2.7版及更低版本中,它允許你鍵入TRUE=FALSE。這不會引發某種邏輯崩潰,也不會導致宇宙的終結;它只是交換了TRUE和FALSE的含義。你還可以使用C預處理器和其他一些語言來玩這種危險的遊戲。還有一些語言允許你重新定義加號之類的運算符。

有時候,在一大段代碼中重新定義一個或一些所謂常量,結果效率會更高。有時,老闆會希望代碼執行完全不同的操作。當然,你可以檢查代碼,逐一更改對應的部分,也可以乾脆重新定義現實來節省時間。別人會覺得你是天才。用不着重寫龐大的庫,只需翻轉一下即可。

這裏也許應該劃一條底線。無論這種做法多有意思,看起來多聰明,你都不應該在家裏做實驗。這太危險了——我是認真的。

相關鏈接

[1]ESLint space-infix-ops規則:https://eslint.org/docs/rules/space-infix-ops
[2]《我們認爲Goto聲明是有害的》:http://www.u.arizona.edu/~rubinson/copyright_violations/Go_To_Considered_Harmful.html
[3]“goto fail”安全漏洞:https://dwheeler.com/essays/apple-goto-fail.html

作者介紹:
Peter Wayner是InfoWorld的特約編輯,並撰寫了16本書,涉及各種主題,包括開源軟件、自動駕駛汽車、增強隱私的計算、數字交易和隱寫術等。

原文鏈接:
https://www.infoworld.com/article/2992566/10-bad-programming-habits-we-secretly-love.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章