寫(xiě)代碼要小心地盤(pán) 陰溝里也能翻了船
筆者的同事陽(yáng)春君春節期間喝大了,樂(lè )極生悲摔傷了手。在做完手術(shù)后的復健階段,醫生讓他盤(pán)核桃,幫助恢復肌腱。
本文引用地址:http://dyxdggzs.com/article/201906/401791.htm可是,陽(yáng)春君的手既肥且小,普通大小的核桃抓一個(gè)綽綽有余,抓兩個(gè)就頗有些力不從心。而且,“盤(pán)”核桃這活兒大有技術(shù)含量,揉、捏、捻、擼、搓,各種手法,各有各的門(mén)道。所以,這只平時(shí)敲鍵盤(pán)、寫(xiě)代碼的手剛開(kāi)始“盤(pán)”核桃時(shí),動(dòng)作生硬,毫無(wú)章法。
玩家們盤(pán)上一段時(shí)間的核桃,就會(huì )變得既紅且圓潤??珊颂业搅怂氖掷?,過(guò)了一段時(shí)間,就好像受了十大酷刑一般,體無(wú)完膚,斑駁丑陋。讓人奇怪的是,雖然盤(pán)核桃接二連三地失敗,他的手竟而漸漸好了起來(lái),又能噼里啪啦地敲代碼了。
這段盤(pán)核桃的經(jīng)歷在陽(yáng)春君的身上打下了深深的烙印。他經(jīng)常把我拉過(guò)去,幫著(zhù)一起找運行出錯的代碼中的bug??粗?zhù)他那縱橫交錯的代碼,我經(jīng)常提醒他把代碼再重構一下,這樣方便找問(wèn)題。每每這時(shí),他就會(huì )盯著(zhù)屏幕上的代碼,對著(zhù)不知藏在何處的bug咬牙切齒地說(shuō):盤(pán)它!
1
萬(wàn)物皆可盤(pán),核桃如是,代碼亦然。
核桃自不必待言,渾然天成的疙疙瘩瘩,自然而言的天然丑,一副迫切求“盤(pán)”的樣子,求仁得仁,不盤(pán)它盤(pán)誰(shuí)?
代碼呢?基督說(shuō),在神面前,人生而不完美。人猿同祖同宗,不完美的程序“猿”寫(xiě)出來(lái)的代碼自然也不會(huì )盡善盡美了,故而同樣存在被“盤(pán)”的需求。
在被盤(pán)的過(guò)程中,核桃被千般揉捏,萬(wàn)般搓捻,終于洗盡鉛華,文藝氣息盡顯。代碼則被一次次地重構,抽筋換骨,改頭換面,最終臻于穩定可靠、好看好用。
盤(pán)核桃的過(guò)程是美妙的。十全老人曾作詩(shī)表達對盤(pán)核桃的喜愛(ài):掌上旋日月,時(shí)光欲倒流。周身氣血涌,何年是白頭。盤(pán)得如癡如醉,忘了歲月之憂(yōu)。
盤(pán)代碼的過(guò)程卻是苦樂(lè )夾雜的。倘若小心翼翼,技術(shù)精湛,代碼溫順如牛,在自己的手下一步步變得完美、和諧,碼農一樣可以樂(lè )而忘憂(yōu)??扇绻中拇笠?,或盤(pán)功很爛,搞得代碼猶如脫韁之馬,攜bug之威只是發(fā)出那冷笑,碼農就只能周身氣血涌,早日白了頭了。
有那么幾次,筆者就因為不小心,代碼沒(méi)有好好地盤(pán),在陰溝里翻了船。
2
這個(gè)世界上最遙遠的距離,不是相隔千山萬(wàn)里,而是你站在我的面前,我就是搞不懂你。
對于代碼,對于程序,我經(jīng)常生出這種“你為什么就是不懂我?!”的哀怨。
比如我曾經(jīng)寫(xiě)過(guò)下面這句代碼:
Ev_ia=Can_bms.ial+(uint16_t)Can_bms.iah<<8;
背景很簡(jiǎn)單,就是根據BMS(電池管理系統)這個(gè)CAN節點(diǎn)發(fā)來(lái)的電流高字節(iah)、低字節(ial)計算出當前電動(dòng)汽車(chē)的電流大小。計算方法為:高字節左移8位,再和低字節相加。
看,計算多么簡(jiǎn)單,簡(jiǎn)直是把心都剖開(kāi)了給你看。但是,歲月劍拔弩張,這世界并非你想的那樣。
調試過(guò)程中,錯誤在第一時(shí)間就跳了出來(lái),因為我發(fā)現,BMS送來(lái)的低字節電流為0x64,高字節電流為0時(shí),運算結果居然成了0x6400。
錯誤是顯然的,原因也是簡(jiǎn)單的。我立馬敏銳地意識到是“運算符的優(yōu)先級”問(wèn)題。查表一看,果然如此。
左移運算符(<<)的優(yōu)先級低于相加運算符(+),故而,在計算機的世界中,真正的運算過(guò)程為:
0x64+(uint16_t)0 = 0x64;0x64 << 8 = 0x6400。
而不是自己心中的想當然:0x64 + (0 << 8)=0x64。
細究起來(lái),這可以被認為是人-機之間存在誤會(huì ),而誤會(huì )無(wú)處不在。
宇宙黑暗森林中的各個(gè)文明之間充滿(mǎn)了猜疑,隨時(shí)準備發(fā)起黑暗打擊。地球文明中的超級大國美國和中國之間充滿(mǎn)了猜忌,貿易戰打得如火如荼。就是朝朝暮暮的情侶之間也各種誤會(huì ),隨時(shí)捕捉著(zhù)對方的不信任。
碼農和他鐘愛(ài)的程序之間吶,也被這宿命般的誤會(huì )搞得不能你儂我儂,地久天長(cháng)到那山無(wú)棱。
3
上面這個(gè)bug還算是有情可原,畢竟世間之事千般萬(wàn)種,被世事搞得體力虛弱、腦力孱弱的碼農們,很難在編程語(yǔ)言的語(yǔ)法上掌握地非常全面。
世道艱難,人生無(wú)常,程序猿過(guò)得很累,我們要原諒他。
但是還有一種本來(lái)不該出現的bug,它不僅出現了,竟然還長(cháng)時(shí)間地呆在那里,直到你把它捉走時(shí),才會(huì )發(fā)覺(jué)這種bug出現得多么不可理喻。
筆者就在一款產(chǎn)品的小批量試產(chǎn)階段發(fā)現了一個(gè)本不該出現、出現后也不該活過(guò)半天的bug。這只bug通過(guò)了功能確認,扛過(guò)了車(chē)廠(chǎng)的路試,活到了小批量試產(chǎn)階段。
這款產(chǎn)品中有個(gè)車(chē)速檢測功能,通過(guò)ABS發(fā)來(lái)的兩個(gè)字節的車(chē)速數據計算出當前車(chē)速,根據車(chē)速的變化自動(dòng)對車(chē)門(mén)上鎖。
兩個(gè)高低字節經(jīng)過(guò)移位、相加得到一個(gè)雙字節數據,似曾相識吧。沒(méi)錯,和上面那個(gè)計算電動(dòng)車(chē)當前消耗電流的方法一樣。
吃一塹長(cháng)一智的筆者肯定不會(huì )犯同樣的錯誤,這一次,灑家犯了新的錯誤。
我居然把存放一個(gè)中間車(chē)速的變量定義成了8位單字節類(lèi)型!把一個(gè)本該是16位雙字節類(lèi)型的數字塞到單字節里,你可以想象那是怎樣一種荒唐的bug。
uint16_t Speed_abs;
//Uint8_t Speed_abs;//錯誤就在這里?。?!
Speed_abs=can_ABS330.msg_data.sig.vehicle_speed_l+ (can_ABS330.msg_data.sig.vehicle_speed_h << 8);
if(Speed_abs <= 0x12c0){
Speed_quant = Speed_abs;
CheckSpeed();
}
出現這種錯誤,當然是不可原諒的,但是,人有失算,馬有失蹄,偶爾腦袋短路,似乎也無(wú)法全然避免。事情的詭異當然不在這里,它的神奇之處在于,這種低級別的bug居然一路通關(guān),活到了最后。
事后想來(lái),整件事都透露著(zhù)神奇和詭異。
人生充滿(mǎn)了陰差陽(yáng)錯,各種說(shuō)不清道不明的東西,會(huì )讓我們陷入莫名其妙的境地。這個(gè)神奇的bug活了這么久,將這種陰差陽(yáng)錯詮釋地淋漓盡致。
4
這段錯誤的代碼是在產(chǎn)品開(kāi)發(fā)的最后階段引入的。最后階段的主要工作是查缺補漏,盡可能地堵住各種可能的隱患。
本來(lái)的車(chē)速計算和判斷程序里沒(méi)有和最高有效值0x12c0的判斷,直接從ABS數據計算出當前車(chē)速,賦值給Speed_quant。不知怎么的,我覺(jué)得最好是在這里加上對最高有效值的判斷,于是就定義了一個(gè)中間變量Speed_abs,當它小于等于0x12c0時(shí),再把它賦值給Speed_quant。
沒(méi)用的好心,結果卻變成了壞事。本來(lái)取值區間在0-0x12c0之間的車(chē)速值,被硬生生地限制在了單字節的取值區間(0-0xff)里。想一下吧,有一個(gè)神力把三維的你給拍扁了,放進(jìn)了二維的畫(huà)里面,憋屈不憋屈?
本來(lái),這樣的bug不該活過(guò)半分鐘,結果,因為我們這個(gè)產(chǎn)品是在車(chē)廠(chǎng)的試驗車(chē)上進(jìn)行驗證,試驗車(chē)的油箱基本上空了,車(chē)廠(chǎng)工作人員也不允許我們開(kāi)起車(chē)來(lái)跑,于是,這個(gè)判斷車(chē)速、賦值車(chē)速、根據車(chē)速自動(dòng)閉鎖的程序分支一直沒(méi)有得到驗證。Bug就這樣活過(guò)了第一關(guān)。
盡管如此,在路試階段,由于這個(gè)bug的存在,加速閉鎖功能就失效了。那么,跑車(chē)的路試員怎么竟而沒(méi)有發(fā)現這么明顯的錯誤呢?
難道路試員每次開(kāi)車(chē)之前,都會(huì )先中控閉鎖一下?或者我們的合作伙伴給車(chē)廠(chǎng)送的樣件里燒錄的居然不是我給他們的最后一版程序?
實(shí)情如何,已無(wú)歷史可考了,這樁事件終于成了令我百思不得其解的懸案。只有它的詭異,時(shí)常浮現在我的心頭,讓我在敲代碼時(shí)更存了一分小心翼翼和戰戰兢兢。
戰戰兢兢,如履薄冰,如此,君子可不立于危地。
5
對于一個(gè)原始狀態(tài)的代碼,一位好的碼農會(huì )耐心地在之上精耕細作,仔細地捉蟲(chóng),耐心地施肥,小心地呵護,務(wù)求盡善盡美,這樣子,才能搞出健壯、耐看的果實(shí)來(lái),這樣子盤(pán)出來(lái)的代碼才能經(jīng)受住時(shí)間的考驗。
相反,有的碼農寫(xiě)了一段代碼后就把它扔在那里,就好像只管生不管養的無(wú)良父母一樣,急火火地又去寫(xiě)下一段代碼。沒(méi)有對代碼的耐心呵護,缺乏對它的修剪、完善,最終,搞出的程序總是既不穩定,又不耐看。
不愿意耐心找bug改代碼的人,實(shí)際上犯的是貪功冒進(jìn)的錯誤,總以為量變才能質(zhì)變,多寫(xiě)代碼才能把水平提升上去。其實(shí)這種認識非常片面,這種方式也很不可取,對自我能力的提升非常不利。
因為,只有在一次次的量變中有小的質(zhì)變,才能最終練成編碼神功。每次都馬馬虎虎,這樣的編碼經(jīng)歷就像掰玉米的狗熊一樣,一邊得到,一邊丟棄,到最后其實(shí)剩不下多少有用的東西。
不愿意下功夫盤(pán)代碼,也是一種懶惰。好逸惡勞是人之通病,所以好的程序猿總是難尋。
對待代碼,需要確立不求盡善盡美、但求問(wèn)心無(wú)愧的心態(tài)。要仔細地盤(pán),耐心地盤(pán)。
想必,我的經(jīng)歷也能給大家一些啟發(fā):代碼一定要耐心地盤(pán),陰溝里也能翻了船!
評論