
來源:AI前線
整理:華衛(wèi)
在軟件工程領(lǐng)域,有些 “老派” 的方法和理念,是經(jīng)過時(shí)間檢驗(yàn)的真理,值得我們重新審視和學(xué)習(xí)。
大多數(shù)大型軟件開發(fā)項(xiàng)目都會(huì)使用編碼規(guī)范,旨在規(guī)定編寫軟件的基本規(guī)則:代碼應(yīng)如何構(gòu)建,以及應(yīng)該使用和避免哪些語言特性,尤其是在代碼的正確性會(huì)對(duì)設(shè)備產(chǎn)生決定性影響的領(lǐng)域,如潛水艇、飛機(jī)、將宇航員送上同步軌道的航天器,以及距離居民區(qū)僅幾公里之外的核電站等設(shè)施運(yùn)行的控制代碼等。
在眾多編碼規(guī)范中,NASA 的編碼規(guī)則以其嚴(yán)苛性和有效性反復(fù)被提起。近期,油管博主 ThePrime Time 發(fā)布的解讀 NASA 安全編碼規(guī)則的視頻,甚至短時(shí)間內(nèi)引發(fā)了超百萬觀看。
特別是在 AI 編程和“氛圍編程”流行的當(dāng)下,重新審視嚴(yán)謹(jǐn)、可驗(yàn)證的編程規(guī)范,是對(duì)軟件工程本質(zhì)的回歸。
有聲音說,“老派的 NASA 編碼方式是最好的方式。”也有人評(píng)價(jià),“在 C 語言中使用這些標(biāo)準(zhǔn)的編碼人員是真正的戰(zhàn)士?!?/p>
NASA 程序員在編寫航天設(shè)備運(yùn)行代碼時(shí)都遵守一套嚴(yán)格的規(guī)則,這套編碼規(guī)則由 NASA 噴氣推進(jìn)實(shí)驗(yàn)室(JPL)首席科學(xué)家 Gerard J. Holzmann 所提出,名為《The Power of Ten – Rules for Developing Safety Critical Code1》(十倍力量:安全關(guān)鍵代碼開發(fā)規(guī)則)。
其在開頭指出,“大多數(shù)現(xiàn)有的規(guī)范包含遠(yuǎn)遠(yuǎn)超過 100 條規(guī)則,而且有些規(guī)則的合理性存疑。有些規(guī)則,特別是那些試圖規(guī)定程序中空白使用方式的規(guī)則(提到了 Python),可能是出于個(gè)人偏好而制定的。其他一些規(guī)則則是為了防止同一組織早期編碼工作中出現(xiàn)的非常特定且不太可能發(fā)生的錯(cuò)誤類型。毫不奇怪,現(xiàn)有編碼規(guī)范對(duì)開發(fā)人員實(shí)際編寫代碼的行為影響甚微。許多規(guī)范最致命的方面是它們很少允許進(jìn)行全面的基于工具的合規(guī)性檢查。基于工具的檢查很重要,因?yàn)閷?duì)于大型應(yīng)用程序編寫的數(shù)十萬行代碼,手動(dòng)審查通常是不可行的?!?/p>
ThePrime Time 對(duì)此表達(dá)了強(qiáng)烈地贊同,稱“確實(shí)有很多個(gè)人偏好被寫入了代碼規(guī)范中。我認(rèn)同目前提到的所有內(nèi)容,代碼就應(yīng)該可靠。自動(dòng)化和工具的使用應(yīng)該杜絕個(gè)人偏好?!?/p>
NASA 的編碼規(guī)則主要針對(duì) C 語言,力求優(yōu)化更全面檢查用 C 語言編寫的關(guān)鍵應(yīng)用程序可靠性的能力。原因是,“在包括 JPL 在內(nèi)的許多組織中,關(guān)鍵代碼都是用 C 語言編寫的。由于其悠久的歷史,這種語言有廣泛的工具支持,包括強(qiáng)大的源代碼分析器、邏輯模型提取器、度量工具、調(diào)試器、測(cè)試支持工具,以及成熟穩(wěn)定的編譯器選擇。因此,C 語言也是大多數(shù)已開發(fā)的編碼規(guī)范的目標(biāo)?!?/p>
ThePrime Time 表示,“我知道現(xiàn)在有很多軟件開發(fā)人員,一聽到用 C 語言編寫安全關(guān)鍵代碼,可能就會(huì)想‘怎么又是這個(gè)’ 。你們可沒有像 ‘旅行者號(hào)’ (NASA 研制的太空探測(cè)器)那樣的項(xiàng)目,你們還不是頂尖開發(fā)者。”
此外,Holzmann 認(rèn)為,“為了有效,規(guī)則集必須很小,并且必須足夠清晰,以便于理解和記憶。規(guī)則必須足夠具體,以便可以機(jī)械地進(jìn)行檢查。當(dāng)然,這么小的規(guī)則集不可能涵蓋所有情況,但它可以為我們提供一個(gè)立足點(diǎn),對(duì)軟件的可靠性和可驗(yàn)證性產(chǎn)生可衡量的影響?!币虼耍麑?NASA 的編碼規(guī)則限制在十條。
這十條規(guī)則正在 NASA 噴氣推進(jìn)實(shí)驗(yàn)室用于關(guān)鍵任務(wù)軟件的編寫實(shí)驗(yàn),取得了令人鼓舞的成果。據(jù) Holzmann 介紹,一開始,NASA 的開發(fā)人員對(duì)遵守如此嚴(yán)格的限制存在合理的抵觸情緒,但克服之后,他們常常發(fā)現(xiàn),遵守這些規(guī)則確實(shí)有助于提高代碼的清晰度、可分析性和安全性。這些規(guī)則減輕了開發(fā)人員和測(cè)試人員通過其他方式確定代碼關(guān)鍵屬性(例如終止性、有界性、內(nèi)存和棧的安全使用等)的負(fù)擔(dān)?!斑@些規(guī)則就像汽車上的安全帶,起初可能有點(diǎn)讓人不舒服,但過一段時(shí)間后,使用它們會(huì)成為習(xí)慣,不使用反而難以想象?!?/p>
ThePrime Time 最后對(duì) NASA 編碼規(guī)則給出的整體評(píng)價(jià)是,“我喜歡這份文檔,即便我并非完全認(rèn)同其中所有的規(guī)則。我只是很驚訝,政府機(jī)構(gòu)編寫的內(nèi)容竟如此條理清晰。這是一份極其連貫的文檔,似乎出自一位追求務(wù)實(shí)的人之手?!?/p>
不少與 NASA 工程師共事過的開發(fā)者們,都對(duì)這則 NASA 十大編碼規(guī)則的解讀視頻深有感觸:“他們的編碼指南并不‘瘋狂’,反而實(shí)際上相當(dāng)理智。我們沒有以這種方式編程才是瘋狂的”,并分享了許多個(gè)人的相關(guān)經(jīng)歷。

“在學(xué)習(xí) C 語言的時(shí)候,我的教授曾為衛(wèi)星編寫 C 程序 / 代碼。他把自己的方法教給了我們,這種方法要求我們?cè)陔娔X上編程之前,先把所有內(nèi)容都寫在紙上。這種方式迫使我們準(zhǔn)確理解自己正在編寫的內(nèi)容、內(nèi)存分配等知識(shí),還能編寫出更高效的代碼,并掌握相關(guān)知識(shí)。我很慶幸自己是通過這種方式學(xué)習(xí)的,因?yàn)樵诿嬖嚂r(shí),我能輕松地在白板上編寫代碼?!币幻こ處熣f。
另一位與前 NASA 工程師共同開發(fā)過游戲的程序員透露,“他的代碼是我見過的最整潔、最易讀的。當(dāng)時(shí)我還是一名初級(jí)程序員,僅僅通過和他一起編寫代碼,我就學(xué)到了很多東西。我們使用的是 C++ 語言,但他的編程風(fēng)格更像是帶有類的 C 語言。他的代碼本身就很易于理解(具有自解釋性),不過他仍然對(duì)自己的代碼進(jìn)行了注釋(既有代碼中的注釋,也有實(shí)際的文檔說明)。”
還有一位自述“和 NASA 一位級(jí)別很高的程序員關(guān)系非常密切”的開發(fā)者表示,“我聽過很多故事,這些故事都能說明制定所有這些標(biāo)準(zhǔn)的合理性??陀^來講,從 Java 1.5 升級(jí)到 1.7 的成本,比從零開始重建任務(wù)控制中心(MCC)還要高。而重建任務(wù)控制中心是用 C 語言完成的,其中另一位首席工程師曾是 C++ 專家,他認(rèn)定最初的 C 語言更可靠?!?/p>
同時(shí)有前 NASA 工程師出來現(xiàn)身說法道,“曾參與構(gòu)建云基礎(chǔ)設(shè)施,他們的指導(dǎo)原則可不是鬧著玩的,代碼審查簡(jiǎn)直是人間煉獄?!畤?yán)苛’這個(gè)詞用來形容再貼切不過了。不過,相比我之前在電信、金融科技領(lǐng)域的工作經(jīng)歷,以及后來在其他科技公司的工作,我在 NASA 工作期間對(duì)可靠性方面的了解要多得多?!?/p>
“NASA 的編碼要求太瘋狂了”
對(duì)于這十條規(guī)則,Holzmann 已經(jīng)聲明,“為了支持強(qiáng)大的審查,這些規(guī)則有些嚴(yán)格,甚至可以說嚴(yán)苛。但這種權(quán)衡是有道理的。在關(guān)鍵時(shí)候,尤其是開發(fā)安全關(guān)鍵代碼時(shí),多費(fèi)些功夫,遵守更嚴(yán)格的限制是值得的。這樣我們就能更有力地證明關(guān)鍵軟件能按預(yù)期運(yùn)行?!辈⑶遥織l規(guī)則之后都附了其被納入的簡(jiǎn)短理由。
ThePrime Time 對(duì)這些編碼規(guī)則及理由一一進(jìn)行了評(píng)價(jià)和分析,以下是經(jīng)不改變?cè)獾姆g和編輯后整理出來的解讀內(nèi)容。
規(guī)則一:
將所有代碼限制在非常簡(jiǎn)單的控制流結(jié)構(gòu)中,不要使用 goto 語句、setjmp 或 longjmp 結(jié)構(gòu)以及直接或間接遞歸。
理由:更簡(jiǎn)單的控制流意味著更強(qiáng)的驗(yàn)證能力,并且通常能提高代碼的清晰度。禁止遞歸可能是這里最讓人意外的一點(diǎn)。不過,如果沒有遞歸,我們就能保證有一個(gè)無環(huán)的函數(shù)調(diào)用圖,這能被代碼分析器利用,還能直接幫助證明所有本應(yīng)有限的執(zhí)行實(shí)際上都是有限的。(注意,這條規(guī)則并不要求所有函數(shù)都有一個(gè)單一的返回點(diǎn)——盡管這通常也會(huì)簡(jiǎn)化控制流。不過,有很多情況下,提前返回錯(cuò)誤是更簡(jiǎn)單的解決方案。)
ThePrime Time:我不知道間接遞歸是什么意思。間接遞歸是指兩個(gè)函數(shù)相互調(diào)用嗎?在實(shí)際情況中,這種情況確實(shí)會(huì)發(fā)生,而且發(fā)生過很多次。比如有一個(gè)函數(shù)需要調(diào)用另一個(gè)函數(shù)去做某些事情并進(jìn)行一些檢查,然后再通過某種不受你控制的方式返回結(jié)果。特別是在那些沒有異步 / 等待(async/await)機(jī)制的語言里,我猜你只能阻塞線程了,對(duì)吧?規(guī)則是說如果沒有異步機(jī)制,就只能阻塞線程,然后在一個(gè)while
循環(huán)里處理,是這樣嗎?我得想想我自己使用間接遞歸的場(chǎng)景。實(shí)際上,我在處理套接字重連時(shí)就用到了間接遞歸。具體來說,當(dāng)我打開一個(gè)套接字(就像這樣,打開這個(gè)套接字),重連機(jī)制會(huì)調(diào)用一個(gè)私有函數(shù)來重置狀態(tài),然后調(diào)用reconnect
函數(shù),reconnect
函數(shù)會(huì)調(diào)用connect
函數(shù),當(dāng)連接斷開時(shí),connect
函數(shù)又會(huì)再次調(diào)用自己。從技術(shù)上講,這就是一種間接遞歸。我在想,也許我可以把它改成用while
循環(huán)加上等待機(jī)制,這樣會(huì)不會(huì)更簡(jiǎn)單呢?現(xiàn)在真的讓我開始思考這個(gè)問題了,這可能只是一種替代方案。
哎呀,我已經(jīng)違反規(guī)則一了。不過間接遞歸確實(shí)是一種非常強(qiáng)大的無限問題解決機(jī)制,但我可以嘗試不用它,對(duì)吧?我完全不介意嘗試新的做法。理由很簡(jiǎn)單,“更簡(jiǎn)單的控制流意味著更強(qiáng)的驗(yàn)證能力,并且通常能提高代碼的清晰度。”我認(rèn)同這一點(diǎn),確實(shí)看到代碼里有類似這樣的邏輯時(shí)會(huì)覺得有點(diǎn)繞。比如在一個(gè) close 函數(shù)中調(diào)用 reconnect,然后在 reconnect 中檢查是否已經(jīng)啟動(dòng),如果沒有完成,就返回。這些語句的順序會(huì)導(dǎo)致問題,所以我可以理解為什么會(huì)出現(xiàn)這種情況。我甚至可以說服自己接受這一點(diǎn)。
說實(shí)話,這是一個(gè)非常務(wù)實(shí)的觀點(diǎn),解釋了為什么不應(yīng)該使用遞歸。而且說句公道話,我大學(xué)三四年級(jí)的時(shí)候(不對(duì),其實(shí)是大二),老師讓我們用非遞歸的方式實(shí)現(xiàn) AVL 樹。如果你不熟悉 AVL 樹,這是一種使用旋轉(zhuǎn)的自平衡二叉搜索樹,有四種不同的旋轉(zhuǎn):右旋、左旋、先右后左旋和先左后右旋。做起來其實(shí)很有趣,假設(shè)我們有一個(gè)二叉樹,像這樣:a(根節(jié)點(diǎn)),b 是左子節(jié)點(diǎn),c 是右子節(jié)點(diǎn)。我們想重新組織成 b 作為根節(jié)點(diǎn),a 作為左子節(jié)點(diǎn),c 作為右子節(jié)點(diǎn)。如果你有一棵二叉樹,看起來像 “a b c”,你就把它重組為 “a c b”,然后進(jìn)行旋轉(zhuǎn)。很簡(jiǎn)單直接,對(duì)吧?老師說我們要實(shí)現(xiàn)這個(gè)程序,但不能用遞歸。這對(duì)我來說是一次很棒的學(xué)習(xí)經(jīng)歷。
規(guī)則二:
所有循環(huán)必須有固定的上限。檢查工具必須能夠輕易地靜態(tài)證明循環(huán)的預(yù)設(shè)迭代上限不會(huì)被突破。如果無法靜態(tài)證明循環(huán)的上限,就視為違反規(guī)則。
理由:沒有遞歸且存在循環(huán)上限可以防止代碼失控。當(dāng)然,這條規(guī)則不適用于那些本就不打算終止的迭代(例如在進(jìn)程調(diào)度器中)。在這些特殊情況下,適用相反的規(guī)則:必須能靜態(tài)證明迭代不會(huì)終止。支持這條規(guī)則的一種方法是,給所有迭代次數(shù)可變的循環(huán)添加明確的上限(比如遍歷鏈表的代碼)。當(dāng)超過上限時(shí),會(huì)觸發(fā)斷言失敗,包含失敗迭代的函數(shù)將返回錯(cuò)誤。(關(guān)于斷言的使用,請(qǐng)參見規(guī)則 5)
ThePrime Time:這是不是意味著不能使用像數(shù)組的forEach
這樣的方法呢?因?yàn)閺募夹g(shù)上講,其上限是根據(jù)數(shù)組動(dòng)態(tài)變化的,沒有固定值。還是說不能使用while (true)
這種循環(huán)呢?這是一條有趣的規(guī)則。
規(guī)則三:
初始化后不要使用動(dòng)態(tài)內(nèi)存分配。
理由:這條規(guī)則在安全關(guān)鍵軟件中很常見,并且出現(xiàn)在大多數(shù)編碼規(guī)范里。原因很簡(jiǎn)單:像 malloc 這樣的內(nèi)存分配器和垃圾回收器,其行為往往不可預(yù)測(cè),可能會(huì)對(duì)性能產(chǎn)生重大影響。一類顯著的編碼錯(cuò)誤也源于對(duì)內(nèi)存分配和釋放例程的不當(dāng)處理,比如忘記釋放內(nèi)存、釋放后繼續(xù)使用內(nèi)存、試圖分配超過實(shí)際可用的內(nèi)存,以及越界訪問已分配的內(nèi)存等等。強(qiáng)制所有應(yīng)用程序在固定的、預(yù)先分配好的內(nèi)存區(qū)域內(nèi)運(yùn)行,可以避免很多這類問題,也更容易驗(yàn)證內(nèi)存的使用情況。需要注意的是,在不使用堆內(nèi)存分配的情況下,動(dòng)態(tài)申請(qǐng)內(nèi)存的唯一方法是使用棧內(nèi)存。在沒有遞歸的情況下(規(guī)則 1),可以靜態(tài)推導(dǎo)出棧內(nèi)存使用的上限,從而可以證明應(yīng)用程序?qū)⑹冀K在其預(yù)先分配的內(nèi)存范圍內(nèi)運(yùn)行。
ThePrime Time:這么一來,JavaScript 和 Go 語言可就有點(diǎn)麻煩了。不過說實(shí)在的,其實(shí)在 JavaScript 和 Go 里也能做到這一點(diǎn)。顯然,在大多數(shù)情況下是可以的。但我敢肯定,只要調(diào)用類似G Funk
(這里可能是隨意提及的某個(gè)函數(shù))這樣的函數(shù),它背地里肯定會(huì)分配一些你不知道的內(nèi)存。當(dāng)然,不是所有解釋型語言都這樣,這么說不太準(zhǔn)確,不是所有解釋型語言都有這個(gè)問題。
我猜這條規(guī)則意味著不能使用閉包,對(duì)吧?因?yàn)殚]包會(huì)涉及到內(nèi)存分配。準(zhǔn)確地說,你得使用內(nèi)存池(Arena)進(jìn)行內(nèi)存分配。也就是說,不能隨意使用列表或字符串嗎?也不是,其實(shí)可以使用列表和字符串,只是意味著所有內(nèi)存分配都必須在程序開始時(shí)完成。我猜這里說的是堆內(nèi)存,而不是棧內(nèi)存。另外,我是這么理解的,比如說你從服務(wù)器獲取一系列響應(yīng)數(shù)據(jù),你得事先分配一塊足夠大的內(nèi)存區(qū)域,用來存儲(chǔ)所有可能的響應(yīng)數(shù)據(jù),然后像使用環(huán)形緩沖區(qū)一樣循環(huán)利用這塊內(nèi)存,這樣就不會(huì)有額外的內(nèi)存分配操作了,所有可能用到的數(shù)據(jù)都已經(jīng)預(yù)先分配好了。所以一開始你就應(yīng)該擁有所需的所有內(nèi)存,這意味著可以使用字符串,只是得預(yù)先定義好字符串占用內(nèi)存的大小。天吶,這得好好琢磨琢磨,確實(shí)很費(fèi)腦筋,不過環(huán)形緩沖區(qū)的概念真的很有意思。
“在不使用堆內(nèi)存分配的情況下,動(dòng)態(tài)申請(qǐng)內(nèi)存的唯一方式是使用棧內(nèi)存。根據(jù)規(guī)則一,在沒有遞歸的情況下,可以靜態(tài)推導(dǎo)出棧內(nèi)存使用的上限,這樣就能證明應(yīng)用程序始終在其預(yù)先分配的內(nèi)存范圍內(nèi)運(yùn)行?!边@聽起來有點(diǎn)瘋狂,但其實(shí)挺酷的,仔細(xì)想想還挺有道理。
我聽說在游戲開發(fā)里有這樣一種做法,如果我說錯(cuò)了也請(qǐng)大家指正。在游戲開發(fā)中,每個(gè)子團(tuán)隊(duì)會(huì)有各自的資源預(yù)算,包括內(nèi)存和 CPU 時(shí)間。在你負(fù)責(zé)的程序部分,你只能使用分配給你的那部分資源。一旦超出預(yù)算,就會(huì)有類似這樣的提示:“嘿,物理模擬團(tuán)隊(duì),你們用的時(shí)間太多了,能不能想想辦法?” 我覺得這聽起來挺不錯(cuò)的。我知道有這么回事,我舉的這個(gè)例子是希望普通的游戲開發(fā)者也能理解。就好比今年兩家小的游戲工作室因?yàn)橘Y源超支沒拿到獎(jiǎng)金,大致就是這么個(gè)情況。寬泛來講,實(shí)際情況比在 Twitch 聊天里說的要復(fù)雜一些,但差不多就是這樣。我只是以一個(gè)普通游戲開發(fā)者的角度來解釋這個(gè)規(guī)則,我開發(fā)過一些小游戲,但我也知道自己還算不上專業(yè)的游戲開發(fā)者。
規(guī)則四:
任何函數(shù)的長(zhǎng)度都不應(yīng)超過以標(biāo)準(zhǔn)參考格式打印在一張紙上的長(zhǎng)度,即每行寫一條語句、每行寫一個(gè)聲明。通常情況下,這意味著每個(gè)函數(shù)的代碼行數(shù)不應(yīng)超過 60 行。
理由:每個(gè)函數(shù)都應(yīng)該是代碼中的一個(gè)邏輯單元,可以作為一個(gè)單元來理解和驗(yàn)證??缭接?jì)算機(jī)顯示器多個(gè)屏幕或打印時(shí)多頁的邏輯單元要難得多。過長(zhǎng)的函數(shù)往往是代碼結(jié)構(gòu)不佳的表現(xiàn)。
ThePrime Time:好的,這挺合理的。60 行代碼的空間足夠你把事情弄清楚了。鮑勃大叔(Uncle Bob ,著名編程大師 Robert C. Martin)規(guī)定每個(gè)函數(shù)一般只能有三到五行代碼,相比之下 60 行代碼算很多了。不過這里說的是打印在一張紙上,對(duì)吧?就是說代碼打印在單張紙上,大概就是這個(gè)意思。注意,這其實(shí)也不算特別嚴(yán)格的硬性規(guī)定,但從能打印在紙上這個(gè)角度來說,它又算是個(gè)硬性規(guī)定。我覺得 60 行代碼能表達(dá)很多內(nèi)容,肯定有辦法打破這個(gè)規(guī)則,但我感覺自己通常能輕松寫出最多 60 行代碼的函數(shù),我覺得這一點(diǎn)都不難。沒錯(cuò),Ghost 標(biāo)準(zhǔn)庫有數(shù)千行代碼,但我不會(huì)把 Ghost 標(biāo)準(zhǔn)庫當(dāng)作史上最整潔、最出色的代碼之一。老實(shí)說,我個(gè)人覺得 Ghost 標(biāo)準(zhǔn)庫讀起來真的很糟糕。
這條規(guī)則的理由是,每個(gè)函數(shù)都應(yīng)該是代碼中的一個(gè)邏輯單元,能夠作為一個(gè)整體被理解和驗(yàn)證。如果一個(gè)邏輯單元跨越計(jì)算機(jī)顯示器的多個(gè)屏幕,或者打印出來有好多頁,理解起來就困難得多。過長(zhǎng)的函數(shù)往往意味著代碼結(jié)構(gòu)不佳。我基本上同意這個(gè)觀點(diǎn),我覺得實(shí)際上很少能見到超過 60 行代碼的函數(shù)。而且一般來說,當(dāng)你遇到這樣的函數(shù)時(shí),要么是因?yàn)楣δ鼙旧矸浅?fù)雜,由于行為的關(guān)聯(lián)性必須寫在一起;要么這個(gè)函數(shù)寫得很糟糕。除非你寫 React 代碼,我覺得我說的這些還是適用的。
規(guī)則五:
代碼的斷言密度平均每個(gè)函數(shù)至少應(yīng)有兩個(gè)斷言。斷言用于檢查在實(shí)際執(zhí)行中不應(yīng)發(fā)生的異常情況。斷言必須始終無副作用,并且應(yīng)定義為布爾測(cè)試。當(dāng)斷言失敗時(shí),必須采取明確的恢復(fù)措施,例如,向執(zhí)行失敗斷言的函數(shù)的調(diào)用者返回錯(cuò)誤條件。任何靜態(tài)檢查工具能夠證明永遠(yuǎn)不會(huì)失敗或永遠(yuǎn)不會(huì)成立的斷言都違反此規(guī)則。(也就是說,不能通過添加無用的 “assert (true)” 語句來滿足該規(guī)則。)
理由:工業(yè)編碼工作的統(tǒng)計(jì)數(shù)據(jù)表明,單元測(cè)試通常每編寫 10 到 100 行代碼就能發(fā)現(xiàn)至少一個(gè)缺陷。斷言密度越高,攔截缺陷的幾率就越大。斷言的使用通常也被推薦作為強(qiáng)防御性編碼策略的一部分。斷言可用于驗(yàn)證函數(shù)的前置和后置條件、參數(shù)值、函數(shù)返回值以及循環(huán)不變式。由于斷言無副作用,因此在測(cè)試后,可以在對(duì)性能關(guān)鍵的代碼中有選擇地禁用它們。
ThePrime Time:我很喜歡這條規(guī)則。我覺得它有很多優(yōu)點(diǎn),而且我覺得自己需要更多地實(shí)踐這條規(guī)則。我還得繼續(xù)堅(jiān)持,因?yàn)榫拖裎抑罢f的,我的代碼庫里已經(jīng)有不少斷言了。我確實(shí)經(jīng)常使用斷言,但目前我代碼里的斷言一旦觸發(fā),程序就會(huì)直接崩潰。比如說,當(dāng)程序內(nèi)部生成的消息與我預(yù)期的不一致時(shí),我就會(huì)認(rèn)為自己犯了嚴(yán)重錯(cuò)誤,覺得整個(gè)程序都得“炸掉” ,結(jié)果整個(gè)程序就會(huì)受到影響。有一點(diǎn)很棒,如果你能想出某種模糊測(cè)試策略,就能測(cè)試你的程序。你可以往程序里輸入一堆隨機(jī)數(shù)據(jù),而程序里應(yīng)該有防御性的語句,以確保不會(huì)觸發(fā)更多的斷言。這樣一來,你甚至可以減少很多針對(duì)模糊測(cè)試的特定單元測(cè)試,這是不是很神奇?
下面是一個(gè)典型的斷言使用示例:如果條件 C 成立,且斷言 p 大于等于為真,就返回錯(cuò)誤。假設(shè)斷言定義如下:定義一個(gè)名為c_assert
的斷言,用于調(diào)試,當(dāng)斷言失敗時(shí),輸出文件和行號(hào)等信息,這里設(shè)置為false
。在這個(gè)定義中,file
和line
由宏預(yù)處理器預(yù)先定義,用于輸出失敗斷言所在的文件名和行號(hào)。語法#e
會(huì)將斷言條件e
轉(zhuǎn)換為字符串,作為錯(cuò)誤消息的一部分打印出來。在嵌入式程序代碼中,通常沒有地方打印錯(cuò)誤消息,在這種情況下,對(duì)測(cè)試調(diào)試的調(diào)用會(huì)變成空操作,斷言就變成了純粹的布爾測(cè)試。這有助于從異常行為中恢復(fù)錯(cuò)誤。
我開始接觸這 “十條規(guī)則” 的契機(jī)是,有個(gè)來自 Tiger Beetle(一個(gè)項(xiàng)目)的人加入了我們。他們?cè)?Tiger Beetle 項(xiàng)目中對(duì)斷言的使用非常嚴(yán)格。他可以往 Tiger Beetle 里輸入任何數(shù)據(jù),而程序始終能正常運(yùn)行。他們每天在每次構(gòu)建時(shí),都會(huì)進(jìn)行相當(dāng)于 200 年查詢量的測(cè)試,程序不斷接受大量測(cè)試,而且運(yùn)行得非常穩(wěn)定。這真是個(gè)超酷的項(xiàng)目。
規(guī)則六:
數(shù)據(jù)對(duì)象必須在盡可能小的作用域級(jí)別聲明。
理由:這條規(guī)則支持?jǐn)?shù)據(jù)隱藏的基本原則。顯然,如果一個(gè)對(duì)象不在作用域內(nèi),其值就不能被引用或破壞。同樣,如果必須診斷一個(gè)對(duì)象的錯(cuò)誤值,可能分配該值的語句越少,診斷問題就越容易。該規(guī)則不鼓勵(lì)將變量重復(fù)用于多個(gè)不兼容的目的,這可能會(huì)使故障診斷復(fù)雜化。
ThePrime Time:有人說這只是因?yàn)?C 語言沒有恰當(dāng)?shù)腻e(cuò)誤處理和語法特性,我強(qiáng)烈反對(duì)這種說法。實(shí)際上,斷言非常有用。比如說,Tiger Beetle 項(xiàng)目是用 Zig 語言編寫的,Zig 有恰當(dāng)?shù)腻e(cuò)誤處理工具,它有結(jié)果對(duì)象,而且其自身的錯(cuò)誤處理結(jié)果對(duì)象能提供比標(biāo)準(zhǔn)錯(cuò)誤更好的堆棧跟蹤信息,這真的很酷。我記得在采訪時(shí),他說當(dāng)時(shí) Tiger Beetle 項(xiàng)目里有 8000 個(gè)斷言。
沒錯(cuò),斷言不是錯(cuò)誤處理機(jī)制,實(shí)際上斷言不是用于處理錯(cuò)誤的,它是一種不變式,可以說是硬性終止條件。我們來看看,這和在 Zig 語言里直接返回錯(cuò)誤有什么不同呢?在 Zig 里,你可以返回一個(gè)錯(cuò)誤或者一個(gè)值,但這就是一種錯(cuò)誤處理方式。Zig 里不會(huì)硬性終止程序,它必須有恢復(fù)機(jī)制。我覺得這樣也挺好,無論是硬性終止,還是軟性終止并搭配某種錯(cuò)誤恢復(fù)機(jī)制,都需要構(gòu)建相應(yīng)的錯(cuò)誤恢復(fù)機(jī)制。
我其實(shí)并沒有完全理解規(guī)則六,除了感覺好像是說在使用變量的地方定義它,這樣作用域就是最小的,是這個(gè)意思嗎?聽起來好像是這樣,這里是在說封裝的概念嗎?
規(guī)則七:
非 void 函數(shù)的返回值必須由每個(gè)調(diào)用函數(shù)檢查,并且每個(gè)函數(shù)內(nèi)部必須檢查參數(shù)的有效性。
理由:這可能是最常被違反的規(guī)則,因此作為一般規(guī)則有些可疑。從嚴(yán)格意義上講,這條規(guī)則意味著即使是 printf 語句和文件關(guān)閉語句的返回值也必須被檢查。不過也有觀點(diǎn)認(rèn)為,如果對(duì)錯(cuò)誤的響應(yīng)與對(duì)成功的響應(yīng)沒有區(qū)別,那么顯式檢查返回值就沒什么意義。這通常是調(diào)用 printf 和 close 的情況。在這種情況下,可以接受將函數(shù)返回值顯式轉(zhuǎn)換為 (void)—— 這表明程序員是有意忽略返回值,而非不小心遺漏。在更可疑的情況下,應(yīng)該有注釋解釋為什么返回值無關(guān)緊要。不過,在大多數(shù)情況下,函數(shù)的返回值不應(yīng)被忽略,尤其是在必須將錯(cuò)誤返回值沿函數(shù)調(diào)用鏈向上傳播的情況下。標(biāo)準(zhǔn)庫因違反此規(guī)則而臭名昭著,并可能導(dǎo)致嚴(yán)重后果。例如,如果不小心執(zhí)行 strlen (0),或者使用標(biāo)準(zhǔn) C 字符串庫執(zhí)行 strcat (s1, s2, -1)—— 結(jié)果就很糟糕。遵循這條通用規(guī)則,我們可以確保例外情況必須有合理的解釋,并且機(jī)械檢查器會(huì)標(biāo)記違規(guī)行為。通常,遵守這條規(guī)則比解釋為什么不符合規(guī)則更容易。
ThePrime Time:這實(shí)際上是我非常喜歡 Zig 的一個(gè)原因。我想 Rust 語言也有類似的情況,只不過當(dāng)你忽略返回的結(jié)果或異步操作的返回值時(shí),Rust 只是給出警告。我喜歡這條規(guī)則,我覺得這是一條很棒的規(guī)則。這是一條普遍適用的好規(guī)則。要知道,編程的很大一部分就是學(xué)習(xí)這些技巧,避免自己給自己挖坑。
規(guī)則八:
預(yù)處理器的使用必須僅限于包含頭文件和簡(jiǎn)單的宏定義。不允許使用令牌粘貼、可變參數(shù)列表(省略號(hào))和遞歸宏調(diào)用。所有宏必須展開為完整的語法單元。條件編譯指令的使用通常也值得懷疑,但并非總是可以避免。這意味著,即使在大型軟件開發(fā)項(xiàng)目中,除了避免同一頭文件多次包含的標(biāo)準(zhǔn)樣板代碼外,也很少有理由使用超過一兩個(gè)條件編譯指令。每次此類使用都應(yīng)通過基于工具的檢查器標(biāo)記,并在代碼中說明理由。
理由:C 預(yù)處理器是一個(gè)強(qiáng)大的混淆工具,可能會(huì)破壞代碼的清晰度,并使許多基于文本的檢查器感到困惑。即使手頭有正式的語言定義,不受限制的預(yù)處理器代碼中的構(gòu)造的效果也可能極其難以破譯。在 C 預(yù)處理器的新實(shí)現(xiàn)中,開發(fā)人員通常不得不求助于使用早期實(shí)現(xiàn)作為解釋 C 標(biāo)準(zhǔn)中復(fù)雜定義語言的裁判。對(duì)條件編譯持謹(jǐn)慎態(tài)度的理由同樣重要。請(qǐng)注意,僅使用十個(gè)條件編譯指令,就可能有多達(dá) 2 的 10 次方種可能的代碼版本,每種版本都必須進(jìn)行測(cè)試 —— 導(dǎo)致所需的測(cè)試工作量大幅增加。
ThePrime Time:我認(rèn)為對(duì)預(yù)處理宏保持謹(jǐn)慎肯定沒錯(cuò)。我理解代碼時(shí)遇到的困難,沒有比處理預(yù)處理宏更多的了。預(yù)處理宏真的是最難懂的部分之一。這條規(guī)則實(shí)際上會(huì)讓我們的開發(fā)工作變得更加復(fù)雜,我一點(diǎn)都不喜歡。有意思的是,總體來說我不喜歡預(yù)處理器。我能理解預(yù)處理器肯定會(huì)引發(fā)一堆問題,人們通常把它們叫做宏。一般來說,宏可能很有用,但它們通常也非常難以理解,理解起來特別費(fèi)勁。謝天謝地 C 語言沒有宏(這里表述有誤,C 語言有宏,作者可能想表達(dá)宏的復(fù)雜性讓人頭疼 )。宏是一種強(qiáng)大的工具,但就像所有強(qiáng)大的工具一樣,它們非常危險(xiǎn)。預(yù)處理器是一個(gè)強(qiáng)大的混淆工具,會(huì)破壞代碼的清晰度,讓許多基于文本的檢查器感到困惑。即使手頭有正式的語言定義,不受限制的預(yù)處理代碼中的結(jié)構(gòu)效果也極難解讀。在 C 預(yù)處理器的新實(shí)現(xiàn)中,開發(fā)人員常常不得不借助早期的實(shí)現(xiàn)來解讀 C 標(biāo)準(zhǔn)中的復(fù)雜定義語言。
對(duì)條件編譯保持謹(jǐn)慎的理由同樣重要。要知道,僅僅 10 個(gè)條件編譯指令就可能產(chǎn)生多達(dá) 2 的 10 次方種代碼版本,每個(gè)版本都必須進(jìn)行測(cè)試,這會(huì)大幅增加所需的測(cè)試工作量。我是說,這一點(diǎn)非常關(guān)鍵。一般來說,條件編譯就是一場(chǎng)噩夢(mèng),盡管有時(shí)又不得不使用它。我覺得這條規(guī)則務(wù)實(shí)的地方在于,它認(rèn)識(shí)到雖然無法避免使用條件編譯,但條件編譯確實(shí)非常困難且麻煩。
沒錯(cuò),我猜 Rust 語言的 cargo 特性主要是因?yàn)?Rust 編譯速度慢才存在的。我認(rèn)識(shí)的大多數(shù)人對(duì) Rust 二進(jìn)制文件不太感興趣,更多的是擔(dān)心編譯時(shí)間太慢。我敢肯定,最終 Rust 二進(jìn)制文件非常重要,但就我所知,很多時(shí)候問題就出在編譯速度上。正是因?yàn)榫幾g慢,才引出了一系列問題,比如我總是會(huì)遇到這樣的情況,使用 clap 時(shí)忘記添加 feature derive,然后又得去添加;使用 request 時(shí)又忘記添加 request feature 之類的,情況越來越糟。你們看過 AutoSAR 的 C 代碼嗎?我敢肯定那代碼很糟糕。
規(guī)則九:
指針的使用應(yīng)受到限制。具體來說,允許的解引用級(jí)別不超過一級(jí)。指針解引用操作不得隱藏在宏定義或 typedef 聲明中。不允許使用函數(shù)指針。
理由:即使是經(jīng)驗(yàn)豐富的程序員也容易誤用指針。它們可能使程序中的數(shù)據(jù)流難以跟蹤或分析,尤其是對(duì)于基于工具的靜態(tài)分析器。同樣,函數(shù)指針可能會(huì)嚴(yán)重限制靜態(tài)分析器可以執(zhí)行的檢查類型,只有在有充分理由使用它們的情況下才應(yīng)使用,并且理想情況下應(yīng)提供替代方法來幫助基于工具的檢查器確定控制流和函數(shù)調(diào)用層次結(jié)構(gòu)。例如,如果使用函數(shù)指針,工具可能無法證明沒有遞歸,因此必須提供替代保證來彌補(bǔ)分析能力的損失。
ThePrime Time:比如說,怎么處理異步相關(guān)的操作和中斷呢?我覺得他們可能不處理異步操作,但我又確定他們肯定會(huì)處理。處理異步操作肯定得使用某種互斥鎖,對(duì)吧?比如使用信號(hào)量互斥鎖,然后還得涉及對(duì)內(nèi)存的引用。異步操作具有不確定性,所以不太好處理。其實(shí)也不是完全不確定,只是你得在一定程度上進(jìn)行處理。想象一下,你有一個(gè)探測(cè)器,上面有個(gè)小攝像頭正在拍照。在某個(gè)時(shí)刻你拍了照,照片進(jìn)行處理后存儲(chǔ)到內(nèi)存中,處理完成后會(huì)有提示。然后某些代碼需要被喚醒,這不也在一定程度上涉及到函數(shù)指針嗎?我猜得用互斥鎖,對(duì)吧?所以我猜你會(huì)有一段代碼,比如說處理拍照的代碼。你得在這里進(jìn)行一些操作,比如獲取信號(hào)量,在 C 語言里信號(hào)量的值為 1,具體是用 lock unlock(鎖定解鎖 )還是 lock acquire(獲取鎖 )我記不清了。代碼就停在這里等待,當(dāng)照片數(shù)據(jù)傳入后,代碼開始處理,處理完之后再回到等待狀態(tài)。
我理解這個(gè),不過這條規(guī)則感覺更難落實(shí)。
規(guī)則十:
所有代碼從開發(fā)的第一天起,就必須在編譯器最嚴(yán)格的設(shè)置下啟用所有編譯器警告進(jìn)行編譯。所有代碼必須在此設(shè)置下編譯且不發(fā)出任何警告。所有代碼必須每天至少使用一個(gè),但最好是多個(gè)最先進(jìn)的靜態(tài)源代碼分析器進(jìn)行檢查,并且應(yīng)以零警告通過分析。
理由:如今市場(chǎng)上有幾種非常有效的靜態(tài)源代碼分析器,還有相當(dāng)多的免費(fèi)工具。任何軟件開發(fā)工作都沒有理由不使用這種現(xiàn)成的技術(shù)。即使對(duì)于非關(guān)鍵代碼的開發(fā),也應(yīng)將其視為常規(guī)做法。零警告規(guī)則甚至適用于編譯器或靜態(tài)分析器給出錯(cuò)誤警告的情況:如果編譯器或靜態(tài)分析器感到困惑,應(yīng)重寫導(dǎo)致困惑的代碼,使其更簡(jiǎn)單有效。很多開發(fā)者一開始認(rèn)為某個(gè)警告肯定是無效的,結(jié)果后來才意識(shí)到,由于一些不那么明顯的原因,該警告實(shí)際上是合理的。早期的靜態(tài)分析器,比如 lint,大多會(huì)給出無效的提示信息,這讓靜態(tài)分析器的名聲有些不好,但現(xiàn)在情況已經(jīng)不同了。當(dāng)今最好的靜態(tài)分析器速度快,并且會(huì)生成有針對(duì)性且準(zhǔn)確的提示消息。在任何一個(gè)嚴(yán)肅的軟件項(xiàng)目中,它們的使用都不應(yīng)有商量余地。
ThePrime Time:說實(shí)話,我覺得這挺合理的。尤其是對(duì)于新手而言,如果你剛開始接觸軟件開發(fā),就應(yīng)該能夠做到這一點(diǎn)。公平地說,我不算專業(yè)的軟件開發(fā)人員,所以我能理解這一點(diǎn)。我覺得這真的很棒,規(guī)則 10 簡(jiǎn)直太實(shí)用了。
不過,要把這條規(guī)則應(yīng)用到很多項(xiàng)目中可能會(huì)很困難。比如說在 JavaScript 開發(fā)中,大家都知道 JavaScript 的 lint 工具體驗(yàn)很差,大多數(shù) ESLint 規(guī)則純粹是些主觀的好壞評(píng)判標(biāo)準(zhǔn)。比如在處理 Promise 時(shí),使用re
和reject
作為參數(shù)實(shí)際上是不好的做法,對(duì)吧?
https://www.youtube.com/watch?v=JWKadu0ks20
聲明:本文為 InfoQ 整理,不代表平臺(tái)觀點(diǎn),未經(jīng)許可禁止轉(zhuǎn)載。
閱讀最新前沿科技趨勢(shì)報(bào)告,請(qǐng)?jiān)L問歐米伽研究所的“未來知識(shí)庫”
https://wx.zsxq.com/group/454854145828

未來知識(shí)庫是“ 歐米伽 未來研究所”建立的在線知識(shí)庫平臺(tái),收藏的資料范圍包括人工智能、腦科學(xué)、互聯(lián)網(wǎng)、超級(jí)智能,數(shù)智大腦、能源、軍事、經(jīng)濟(jì)、人類風(fēng)險(xiǎn)等等領(lǐng)域的前沿進(jìn)展與未來趨勢(shì)。目前擁有超過8000篇重要資料。每周更新不少于100篇世界范圍最新研究資料。 歡迎掃描二維碼或訪問https://wx.zsxq.com/group/454854145828進(jìn)入。
截止到2月28日 ”未來知識(shí)庫”精選的100部前沿科技趨勢(shì)報(bào)告
熱門跟貼