

在軟體工程領域,有些 "老派" 的方法和理念,是經過時間檢驗的真理,值得我們重新審視和學習。
大多數大型軟體開發專案都會使用編碼規範,旨在規定編寫軟體的基本規則:程式碼應如何構建,以及應該使用和避免哪些語言特性,尤其是在程式碼的正確性會對裝置產生決定性影響的領域,如潛水艇、飛機、將宇航員送上同步軌道的航天器,以及距離居民區僅幾公里之外的核電站等設施執行的控制程式碼等。
在眾多編碼規範中,NASA 的編碼規則以其嚴苛性和有效性反覆被提起。近期,油管博主 ThePrime Time 釋出的解讀 NASA 安全編碼規則的影片,甚至短時間內引發了超百萬觀看。
特別是在 AI 程式設計和“氛圍程式設計”流行的當下,重新審視嚴謹、可驗證的程式設計規範,是對軟體工程本質的迴歸。
有聲音說,“老派的 NASA 編碼方式是最好的方式。”也有人評價,“在 C 語言中使用這些標準的編碼人員是真正的戰士。”


NASA 程式設計師在編寫航天裝置執行程式碼時都遵守一套嚴格的規則,這套編碼規則由 NASA 噴氣推進實驗室(JPL)首席科學家 Gerard J. Holzmann 所提出,名為《The Power of Ten – Rules for Developing Safety Critical Code1》(十倍力量:安全關鍵程式碼開發規則)。
其在開頭指出,“大多數現有的規範包含遠遠超過 100 條規則,而且有些規則的合理性存疑。有些規則,特別是那些試圖規定程式中空白使用方式的規則(提到了 Python),可能是出於個人偏好而制定的。其他一些規則則是為了防止同一組織早期編碼工作中出現的非常特定且不太可能發生的錯誤型別。毫不奇怪,現有編碼規範對開發人員實際編寫程式碼的行為影響甚微。許多規範最致命的方面是它們很少允許進行全面的基於工具的合規性檢查。基於工具的檢查很重要,因為對於大型應用程式編寫的數十萬行程式碼,手動審查通常是不可行的。”
ThePrime Time 對此表達了強烈地贊同,稱“確實有很多個人偏好被寫入了程式碼規範中。我認同目前提到的所有內容,程式碼就應該可靠。自動化和工具的使用應該杜絕個人偏好。”
NASA 的編碼規則主要針對 C 語言,力求最佳化更全面檢查用 C 語言編寫的關鍵應用程式可靠性的能力。原因是,“在包括 JPL 在內的許多組織中,關鍵程式碼都是用 C 語言編寫的。由於其悠久的歷史,這種語言有廣泛的工具支援,包括強大的原始碼分析器、邏輯模型提取器、度量工具、偵錯程式、測試支援工具,以及成熟穩定的編譯器選擇。因此,C 語言也是大多數已開發的編碼規範的目標。”
ThePrime Time 表示,“我知道現在有很多軟體開發人員,一聽到用 C 語言編寫安全關鍵程式碼,可能就會想‘怎麼又是這個’ 。你們可沒有像 ‘旅行者號’ (NASA 研製的太空探測器)那樣的專案,你們還不是頂尖開發者。”
此外,Holzmann 認為,“為了有效,規則集必須很小,並且必須足夠清晰,以便於理解和記憶。規則必須足夠具體,以便可以機械地進行檢查。當然,這麼小的規則集不可能涵蓋所有情況,但它可以為我們提供一個立足點,對軟體的可靠性和可驗證性產生可衡量的影響。”因此,他將 NASA 的編碼規則限制在十條。
這十條規則正在 NASA 噴氣推進實驗室用於關鍵任務軟體的編寫實驗,取得了令人鼓舞的成果。據 Holzmann 介紹,一開始,NASA 的開發人員對遵守如此嚴格的限制存在合理的牴觸情緒,但克服之後,他們常常發現,遵守這些規則確實有助於提高程式碼的清晰度、可分析性和安全性。這些規則減輕了開發人員和測試人員透過其他方式確定程式碼關鍵屬性(例如終止性、有界性、記憶體和棧的安全使用等)的負擔。“這些規則就像汽車上的安全帶,起初可能有點讓人不舒服,但過一段時間後,使用它們會成為習慣,不使用反而難以想象。”
ThePrime Time 最後對 NASA 編碼規則給出的整體評價是,“我喜歡這份文件,即便我並非完全認同其中所有的規則。我只是很驚訝,政府機構編寫的內容竟如此條理清晰。這是一份極其連貫的文件,似乎出自一位追求務實的人之手。”
不少與 NASA 工程師共事過的開發者們,都對這則 NASA 十大編碼規則的解讀影片深有感觸:“他們的編碼指南並不‘瘋狂’,反而實際上相當理智。我們沒有以這種方式程式設計才是瘋狂的”,並分享了許多個人的相關經歷。

“在學習 C 語言的時候,我的教授曾為衛星編寫 C 程式 / 程式碼。他把自己的方法教給了我們,這種方法要求我們在電腦上程式設計之前,先把所有內容都寫在紙上。這種方式迫使我們準確理解自己正在編寫的內容、記憶體分配等知識,還能編寫出更高效的程式碼,並掌握相關知識。我很慶幸自己是透過這種方式學習的,因為在面試時,我能輕鬆地在白板上編寫程式碼。”一名工程師說。
另一位與前 NASA 工程師共同開發過遊戲的程式設計師透露,“他的程式碼是我見過的最整潔、最易讀的。當時我還是一名初級程式設計師,僅僅透過和他一起編寫程式碼,我就學到了很多東西。我們使用的是 C++ 語言,但他的程式設計風格更像是帶有類的 C 語言。他的程式碼本身就很易於理解(具有自解釋性),不過他仍然對自己的程式碼進行了註釋(既有程式碼中的註釋,也有實際的文件說明)。”
還有一位自述“和 NASA 一位級別很高的程式設計師關係非常密切”的開發者表示,“我聽過很多故事,這些故事都能說明制定所有這些標準的合理性。客觀來講,從 Java 1.5 升級到 1.7 的成本,比從零開始重建任務控制中心(MCC)還要高。而重建任務控制中心是用 C 語言完成的,其中另一位首席工程師曾是 C++ 專家,他認定最初的 C 語言更可靠。”
同時有前 NASA 工程師出來現身說法道,“曾參與構建雲基礎設施,他們的指導原則可不是鬧著玩的,程式碼審查簡直是人間煉獄。‘嚴苛’這個詞用來形容再貼切不過了。不過,相比我之前在電信、金融科技領域的工作經歷,以及後來在其他科技公司的工作,我在 NASA 工作期間對可靠性方面的瞭解要多得多。”
對於這十條規則,Holzmann 已經宣告,“為了支援強大的審查,這些規則有些嚴格,甚至可以說嚴苛。但這種權衡是有道理的。在關鍵時候,尤其是開發安全關鍵程式碼時,多費些功夫,遵守更嚴格的限制是值得的。這樣我們就能更有力地證明關鍵軟體能按預期執行。”並且,每條規則之後都附了其被納入的簡短理由。
ThePrime Time 對這些編碼規則及理由一一進行了評價和分析,以下是經不改變原意的翻譯和編輯後整理出來的解讀內容。
將所有程式碼限制在非常簡單的控制流結構中,不要使用 goto 語句、setjmp 或 longjmp 結構以及直接或間接遞迴。
理由:更簡單的控制流意味著更強的驗證能力,並且通常能提高程式碼的清晰度。禁止遞迴可能是這裡最讓人意外的一點。不過,如果沒有遞迴,我們就能保證有一個無環的函式呼叫圖,這能被程式碼分析器利用,還能直接幫助證明所有本應有限的執行實際上都是有限的。(注意,這條規則並不要求所有函式都有一個單一的返回點——儘管這通常也會簡化控制流。不過,有很多情況下,提前返回錯誤是更簡單的解決方案。)
ThePrime Time: 我不知道間接遞迴是什麼意思。間接遞迴是指兩個函式相互呼叫嗎?在實際情況中,這種情況確實會發生,而且發生過很多次。比如有一個函式需要呼叫另一個函式去做某些事情並進行一些檢查,然後再透過某種不受你控制的方式返回結果。特別是在那些沒有非同步 / 等待(async/await)機制的語言裡,我猜你只能阻塞執行緒了,對吧?規則是說如果沒有非同步機制,就只能阻塞執行緒,然後在一個
while
迴圈裡處理,是這樣嗎?我得想想我自己使用間接遞迴的場景。實際上,我在處理套接字重連時就用到了間接遞迴。具體來說,當我開啟一個套接字(就像這樣,開啟這個套接字),重連機制會呼叫一個私有函式來重置狀態,然後呼叫reconnect
函式,reconnect
函式會呼叫connect
函式,當連線斷開時,connect
函式又會再次呼叫自己。從技術上講,這就是一種間接遞迴。我在想,也許我可以把它改成用while
迴圈加上等待機制,這樣會不會更簡單呢?現在真的讓我開始思考這個問題了,這可能只是一種替代方案。哎呀,我已經違反規則一了。不過間接遞迴確實是一種非常強大的無限問題解決機制,但我可以嘗試不用它,對吧?我完全不介意嘗試新的做法。理由很簡單,“更簡單的控制流意味著更強的驗證能力,並且通常能提高程式碼的清晰度。”我認同這一點,確實看到程式碼裡有類似這樣的邏輯時會覺得有點繞。比如在一個 close 函式中呼叫 reconnect,然後在 reconnect 中檢查是否已經啟動,如果沒有完成,就返回。這些語句的順序會導致問題,所以我可以理解為什麼會出現這種情況。我甚至可以說服自己接受這一點。
說實話,這是一個非常務實的觀點,解釋了為什麼不應該使用遞迴。而且說句公道話,我大學三四年級的時候(不對,其實是大二),老師讓我們用非遞迴的方式實現 AVL 樹。如果你不熟悉 AVL 樹,這是一種使用旋轉的自平衡二叉搜尋樹,有四種不同的旋轉:右旋、左旋、先右後左旋和先左後右旋。做起來其實很有趣,假設我們有一個二叉樹,像這樣:a(根節點),b 是左子節點,c 是右子節點。我們想重新組織成 b 作為根節點,a 作為左子節點,c 作為右子節點。如果你有一棵二叉樹,看起來像 “a b c”,你就把它重組為 “a c b”,然後進行旋轉。很簡單直接,對吧?老師說我們要實現這個程式,但不能用遞迴。這對我來說是一次很棒的學習經歷。
所有迴圈必須有固定的上限。檢查工具必須能夠輕易地靜態證明迴圈的預設迭代上限不會被突破。如果無法靜態證明迴圈的上限,就視為違反規則。
理由:沒有遞迴且存在迴圈上限可以防止程式碼失控。當然,這條規則不適用於那些本就不打算終止的迭代(例如在程序排程器中)。在這些特殊情況下,適用相反的規則:必須能靜態證明迭代不會終止。支援這條規則的一種方法是,給所有迭代次數可變的迴圈新增明確的上限(比如遍歷連結串列的程式碼)。當超過上限時,會觸發斷言失敗,包含失敗迭代的函式將返回錯誤。(關於斷言的使用,請參見規則 5)
ThePrime Time: 這是不是意味著不能使用像陣列的
forEach
這樣的方法呢?因為從技術上講,其上限是根據陣列動態變化的,沒有固定值。還是說不能使用while (true)
這種迴圈呢?這是一條有趣的規則。初始化後不要使用動態記憶體分配。
理由:這條規則在安全關鍵軟體中很常見,並且出現在大多數編碼規範裡。原因很簡單:像 malloc 這樣的記憶體分配器和垃圾回收器,其行為往往不可預測,可能會對效能產生重大影響。一類顯著的編碼錯誤也源於對記憶體分配和釋放例程的不當處理,比如忘記釋放記憶體、釋放後繼續使用記憶體、試圖分配超過實際可用的記憶體,以及越界訪問已分配的記憶體等等。強制所有應用程式在固定的、預先分配好的記憶體區域內執行,可以避免很多這類問題,也更容易驗證記憶體的使用情況。需要注意的是,在不使用堆記憶體分配的情況下,動態申請記憶體的唯一方法是使用棧記憶體。在沒有遞迴的情況下(規則 1),可以靜態推匯出棧記憶體使用的上限,從而可以證明應用程式將始終在其預先分配的記憶體範圍內執行。
ThePrime Time: 這麼一來,JavaScript 和 Go 語言可就有點麻煩了。不過說實在的,其實在 JavaScript 和 Go 裡也能做到這一點。顯然,在大多數情況下是可以的。但我敢肯定,只要呼叫類似
G Funk
(這裡可能是隨意提及的某個函式)這樣的函式,它背地裡肯定會分配一些你不知道的記憶體。當然,不是所有解釋型語言都這樣,這麼說不太準確,不是所有解釋型語言都有這個問題。我猜這條規則意味著不能使用閉包,對吧?因為閉包會涉及到記憶體分配。準確地說,你得使用記憶體池(Arena)進行記憶體分配。也就是說,不能隨意使用列表或字串嗎?也不是,其實可以使用列表和字串,只是意味著所有記憶體分配都必須在程式開始時完成。我猜這裡說的是堆記憶體,而不是棧記憶體。另外,我是這麼理解的,比如說你從伺服器獲取一系列響應資料,你得事先分配一塊足夠大的記憶體區域,用來儲存所有可能的響應資料,然後像使用環形緩衝區一樣迴圈利用這塊記憶體,這樣就不會有額外的記憶體分配操作了,所有可能用到的資料都已經預先分配好了。所以一開始你就應該擁有所需的所有記憶體,這意味著可以使用字串,只是得預先定義好字串佔用記憶體的大小。天吶,這得好好琢磨琢磨,確實很費腦筋,不過環形緩衝區的概念真的很有意思。
“在不使用堆記憶體分配的情況下,動態申請記憶體的唯一方式是使用棧記憶體。根據規則一,在沒有遞迴的情況下,可以靜態推匯出棧記憶體使用的上限,這樣就能證明應用程式始終在其預先分配的記憶體範圍內執行。”這聽起來有點瘋狂,但其實挺酷的,仔細想想還挺有道理。
我聽說在遊戲開發裡有這樣一種做法,如果我說錯了也請大家指正。在遊戲開發中,每個子團隊會有各自的資源預算,包括記憶體和 CPU 時間。在你負責的程式部分,你只能使用分配給你的那部分資源。一旦超出預算,就會有類似這樣的提示:“嘿,物理模擬團隊,你們用的時間太多了,能不能想想辦法?” 我覺得這聽起來挺不錯的。我知道有這麼回事,我舉的這個例子是希望普通的遊戲開發者也能理解。就好比今年兩家小的遊戲工作室因為資源超支沒拿到獎金,大致就是這麼個情況。寬泛來講,實際情況比在 Twitch 聊天裡說的要複雜一些,但差不多就是這樣。我只是以一個普通遊戲開發者的角度來解釋這個規則,我開發過一些小遊戲,但我也知道自己還算不上專業的遊戲開發者。
任何函式的長度都不應超過以標準參考格式列印在一張紙上的長度,即每行寫一條語句、每行寫一個宣告。通常情況下,這意味著每個函式的程式碼行數不應超過 60 行。
理由:每個函式都應該是程式碼中的一個邏輯單元,可以作為一個單元來理解和驗證。跨越計算機顯示器多個螢幕或列印時多頁的邏輯單元要難得多。過長的函式往往是程式碼結構不佳的表現。
ThePrime Time: 好的,這挺合理的。60 行程式碼的空間足夠你把事情弄清楚了。鮑勃大叔(Uncle Bob ,著名程式設計大師 Robert C. Martin)規定每個函式一般只能有三到五行程式碼,相比之下 60 行程式碼算很多了。不過這裡說的是列印在一張紙上,對吧?就是說程式碼列印在單張紙上,大概就是這個意思。注意,這其實也不算特別嚴格的硬性規定,但從能列印在紙上這個角度來說,它又算是個硬性規定。我覺得 60 行程式碼能表達很多內容,肯定有辦法打破這個規則,但我感覺自己通常能輕鬆寫出最多 60 行程式碼的函式,我覺得這一點都不難。沒錯,Ghost 標準庫有數千行程式碼,但我不會把 Ghost 標準庫當作史上最整潔、最出色的程式碼之一。老實說,我個人覺得 Ghost 標準庫讀起來真的很糟糕。
這條規則的理由是,每個函式都應該是程式碼中的一個邏輯單元,能夠作為一個整體被理解和驗證。如果一個邏輯單元跨越計算機顯示器的多個螢幕,或者打印出來有好多頁,理解起來就困難得多。過長的函式往往意味著程式碼結構不佳。我基本上同意這個觀點,我覺得實際上很少能見到超過 60 行程式碼的函式。而且一般來說,當你遇到這樣的函式時,要麼是因為功能本身非常複雜,由於行為的關聯性必須寫在一起;要麼這個函式寫得很糟糕。除非你寫 React 程式碼,我覺得我說的這些還是適用的。
程式碼的斷言密度平均每個函式至少應有兩個斷言。斷言用於檢查在實際執行中不應發生的異常情況。斷言必須始終無副作用,並且應定義為布林測試。當斷言失敗時,必須採取明確的恢復措施,例如,向執行失敗斷言的函式的呼叫者返回錯誤條件。任何靜態檢查工具能夠證明永遠不會失敗或永遠不會成立的斷言都違反此規則。(也就是說,不能透過新增無用的 “assert (true)” 語句來滿足該規則。)
理由:工業編碼工作的統計資料表明,單元測試通常每編寫 10 到 100 行程式碼就能發現至少一個缺陷。斷言密度越高,攔截缺陷的機率就越大。斷言的使用通常也被推薦作為強防禦性編碼策略的一部分。斷言可用於驗證函式的前置和後置條件、引數值、函式返回值以及迴圈不變式。由於斷言無副作用,因此在測試後,可以在對效能關鍵的程式碼中有選擇地停用它們。
ThePrime Time: 我很喜歡這條規則。我覺得它有很多優點,而且我覺得自己需要更多地實踐這條規則。我還得繼續堅持,因為就像我之前說的,我的程式碼庫裡已經有不少斷言了。我確實經常使用斷言,但目前我程式碼裡的斷言一旦觸發,程式就會直接崩潰。比如說,當程式內部生成的訊息與我預期的不一致時,我就會認為自己犯了嚴重錯誤,覺得整個程式都得“炸掉” ,結果整個程式就會受到影響。有一點很棒,如果你能想出某種模糊測試策略,就能測試你的程式。你可以往程式裡輸入一堆隨機資料,而程式裡應該有防禦性的語句,以確保不會觸發更多的斷言。這樣一來,你甚至可以減少很多針對模糊測試的特定單元測試,這是不是很神奇?
下面是一個典型的斷言使用示例:如果條件 C 成立,且斷言 p 大於等於為真,就返回錯誤。假設斷言定義如下:定義一個名為
c_assert
的斷言,用於除錯,當斷言失敗時,輸出檔案和行號等資訊,這裡設定為false
。在這個定義中,file
和line
由宏預處理器預先定義,用於輸出失敗斷言所在的檔名和行號。語法#e
會將斷言條件e
轉換為字串,作為錯誤訊息的一部分打印出來。在嵌入式程式程式碼中,通常沒有地方列印錯誤訊息,在這種情況下,對測試除錯的呼叫會變成空操作,斷言就變成了純粹的布林測試。這有助於從異常行為中恢復錯誤。我開始接觸這 “十條規則” 的契機是,有個來自 Tiger Beetle(一個專案)的人加入了我們。他們在 Tiger Beetle 專案中對斷言的使用非常嚴格。他可以往 Tiger Beetle 裡輸入任何資料,而程式始終能正常執行。他們每天在每次構建時,都會進行相當於 200 年查詢量的測試,程式不斷接受大量測試,而且執行得非常穩定。這真是個超酷的專案。
資料物件必須在儘可能小的作用域級別宣告。
理由:這條規則支援資料隱藏的基本原則。顯然,如果一個物件不在作用域內,其值就不能被引用或破壞。同樣,如果必須診斷一個物件的錯誤值,可能分配該值的語句越少,診斷問題就越容易。該規則不鼓勵將變數重複用於多個不相容的目的,這可能會使故障診斷複雜化。
ThePrime Time: 有人說這只是因為 C 語言沒有恰當的錯誤處理和語法特性,我強烈反對這種說法。實際上,斷言非常有用。比如說,Tiger Beetle 專案是用 Zig 語言編寫的,Zig 有恰當的錯誤處理工具,它有結果物件,而且其自身的錯誤處理結果物件能提供比標準錯誤更好的堆疊跟蹤資訊,這真的很酷。我記得在採訪時,他說當時 Tiger Beetle 專案裡有 8000 個斷言。
沒錯,斷言不是錯誤處理機制,實際上斷言不是用於處理錯誤的,它是一種不變式,可以說是硬性終止條件。我們來看看,這和在 Zig 語言裡直接返回錯誤有什麼不同呢?在 Zig 裡,你可以返回一個錯誤或者一個值,但這就是一種錯誤處理方式。Zig 裡不會硬性終止程式,它必須有恢復機制。我覺得這樣也挺好,無論是硬性終止,還是軟性終止並搭配某種錯誤恢復機制,都需要構建相應的錯誤恢復機制。
我其實並沒有完全理解規則六,除了感覺好像是說在使用變數的地方定義它,這樣作用域就是最小的,是這個意思嗎?聽起來好像是這樣,這裡是在說封裝的概念嗎?
非 void 函式的返回值必須由每個呼叫函式檢查,並且每個函式內部必須檢查引數的有效性。
理由:這可能是最常被違反的規則,因此作為一般規則有些可疑。從嚴格意義上講,這條規則意味著即使是 printf 語句和檔案關閉語句的返回值也必須被檢查。不過也有觀點認為,如果對錯誤的響應與對成功的響應沒有區別,那麼顯式檢查返回值就沒什麼意義。這通常是呼叫 printf 和 close 的情況。在這種情況下,可以接受將函式返回值顯式轉換為 (void)—— 這表明程式設計師是有意忽略返回值,而非不小心遺漏。在更可疑的情況下,應該有註釋解釋為什麼返回值無關緊要。不過,在大多數情況下,函式的返回值不應被忽略,尤其是在必須將錯誤返回值沿函式呼叫鏈向上傳播的情況下。標準庫因違反此規則而臭名昭著,並可能導致嚴重後果。例如,如果不小心執行 strlen (0),或者使用標準 C 字串庫執行 strcat (s1, s2, -1)—— 結果就很糟糕。遵循這條通用規則,我們可以確保例外情況必須有合理的解釋,並且機械檢查器會標記違規行為。通常,遵守這條規則比解釋為什麼不符合規則更容易。
ThePrime Time: 這實際上是我非常喜歡 Zig 的一個原因。我想 Rust 語言也有類似的情況,只不過當你忽略返回的結果或非同步操作的返回值時,Rust 只是給出警告。我喜歡這條規則,我覺得這是一條很棒的規則。這是一條普遍適用的好規則。要知道,程式設計的很大一部分就是學習這些技巧,避免自己給自己挖坑。
預處理器的使用必須僅限於包含標頭檔案和簡單的宏定義。不允許使用令牌貼上、可變引數列表(省略號)和遞迴宏呼叫。所有宏必須展開為完整的語法單元。條件編譯指令的使用通常也值得懷疑,但並非總是可以避免。這意味著,即使在大型軟體開發專案中,除了避免同一標頭檔案多次包含的標準樣板程式碼外,也很少有理由使用超過一兩個條件編譯指令。每次此類使用都應透過基於工具的檢查器標記,並在程式碼中說明理由。
理由:C 預處理器是一個強大的混淆工具,可能會破壞程式碼的清晰度,並使許多基於文字的檢查器感到困惑。即使手頭有正式的語言定義,不受限制的預處理器程式碼中的構造的效果也可能極其難以破譯。在 C 預處理器的新實現中,開發人員通常不得不求助於使用早期實現作為解釋 C 標準中複雜定義語言的裁判。對條件編譯持謹慎態度的理由同樣重要。請注意,僅使用十個條件編譯指令,就可能有多達 2 的 10 次方種可能的程式碼版本,每種版本都必須進行測試 —— 導致所需的測試工作量大幅增加。
ThePrime Time: 我認為對預處理宏保持謹慎肯定沒錯。我理解程式碼時遇到的困難,沒有比處理預處理宏更多的了。預處理宏真的是最難懂的部分之一。這條規則實際上會讓我們的開發工作變得更加複雜,我一點都不喜歡。有意思的是,總體來說我不喜歡預處理器。我能理解預處理器肯定會引發一堆問題,人們通常把它們叫做宏。一般來說,宏可能很有用,但它們通常也非常難以理解,理解起來特別費勁。謝天謝地 C 語言沒有宏(這裡表述有誤,C 語言有宏,作者可能想表達宏的複雜性讓人頭疼 )。宏是一種強大的工具,但就像所有強大的工具一樣,它們非常危險。預處理器是一個強大的混淆工具,會破壞程式碼的清晰度,讓許多基於文字的檢查器感到困惑。即使手頭有正式的語言定義,不受限制的預處理程式碼中的結構效果也極難解讀。在 C 預處理器的新實現中,開發人員常常不得不借助早期的實現來解讀 C 標準中的複雜定義語言。
對條件編譯保持謹慎的理由同樣重要。要知道,僅僅 10 個條件編譯指令就可能產生多達 2 的 10 次方種程式碼版本,每個版本都必須進行測試,這會大幅增加所需的測試工作量。我是說,這一點非常關鍵。一般來說,條件編譯就是一場噩夢,儘管有時又不得不使用它。我覺得這條規則務實的地方在於,它認識到雖然無法避免使用條件編譯,但條件編譯確實非常困難且麻煩。
沒錯,我猜 Rust 語言的 cargo 特性主要是因為 Rust 編譯速度慢才存在的。我認識的大多數人對 Rust 二進位制檔案不太感興趣,更多的是擔心編譯時間太慢。我敢肯定,最終 Rust 二進位制檔案非常重要,但就我所知,很多時候問題就出在編譯速度上。正是因為編譯慢,才引出了一系列問題,比如我總是會遇到這樣的情況,使用 clap 時忘記新增 feature derive,然後又得去新增;使用 request 時又忘記新增 request feature 之類的,情況越來越糟。你們看過 AutoSAR 的 C 程式碼嗎?我敢肯定那程式碼很糟糕。
指標的使用應受到限制。具體來說,允許的解引用級別不超過一級。指標解引用操作不得隱藏在宏定義或 typedef 宣告中。不允許使用函式指標。
理由:即使是經驗豐富的程式設計師也容易誤用指標。它們可能使程式中的資料流難以跟蹤或分析,尤其是對於基於工具的靜態分析器。同樣,函式指標可能會嚴重限制靜態分析器可以執行的檢查型別,只有在有充分理由使用它們的情況下才應使用,並且理想情況下應提供替代方法來幫助基於工具的檢查器確定控制流和函式呼叫層次結構。例如,如果使用函式指標,工具可能無法證明沒有遞迴,因此必須提供替代保證來彌補分析能力的損失。
ThePrime Time: 比如說,怎麼處理非同步相關的操作和中斷呢?我覺得他們可能不處理非同步操作,但我又確定他們肯定會處理。處理非同步操作肯定得使用某種互斥鎖,對吧?比如使用訊號量互斥鎖,然後還得涉及對記憶體的引用。非同步操作具有不確定性,所以不太好處理。其實也不是完全不確定,只是你得在一定程度上進行處理。想象一下,你有一個探測器,上面有個小攝像頭正在拍照。在某個時刻你拍了照,照片進行處理後儲存到記憶體中,處理完成後會有提示。然後某些程式碼需要被喚醒,這不也在一定程度上涉及到函式指標嗎?我猜得用互斥鎖,對吧?所以我猜你會有一段程式碼,比如說處理拍照的程式碼。你得在這裡進行一些操作,比如獲取訊號量,在 C 語言裡訊號量的值為 1,具體是用 lock unlock(鎖定解鎖 )還是 lock acquire(獲取鎖 )我記不清了。程式碼就停在這裡等待,當照片資料傳入後,程式碼開始處理,處理完之後再回到等待狀態。
我理解這個,不過這條規則感覺更難落實。
所有程式碼從開發的第一天起,就必須在編譯器最嚴格的設定下啟用所有編譯器警告進行編譯。所有程式碼必須在此設定下編譯且不發出任何警告。所有程式碼必須每天至少使用一個,但最好是多個最先進的靜態原始碼分析器進行檢查,並且應以零警告透過分析。
理由:如今市場上有幾種非常有效的靜態原始碼分析器,還有相當多的免費工具。任何軟體開發工作都沒有理由不使用這種現成的技術。即使對於非關鍵程式碼的開發,也應將其視為常規做法。零警告規則甚至適用於編譯器或靜態分析器給出錯誤警告的情況:如果編譯器或靜態分析器感到困惑,應重寫導致困惑的程式碼,使其更簡單有效。很多開發者一開始認為某個警告肯定是無效的,結果後來才意識到,由於一些不那麼明顯的原因,該警告實際上是合理的。早期的靜態分析器,比如 lint,大多會給出無效的提示資訊,這讓靜態分析器的名聲有些不好,但現在情況已經不同了。當今最好的靜態分析器速度快,並且會生成有針對性且準確的提示訊息。在任何一個嚴肅的軟體專案中,它們的使用都不應有商量餘地。
ThePrime Time: 說實話,我覺得這挺合理的。尤其是對於新手而言,如果你剛開始接觸軟體開發,就應該能夠做到這一點。公平地說,我不算專業的軟體開發人員,所以我能理解這一點。我覺得這真的很棒,規則 10 簡直太實用了。
不過,要把這條規則應用到很多專案中可能會很困難。比如說在 JavaScript 開發中,大家都知道 JavaScript 的 lint 工具體驗很差,大多數 ESLint 規則純粹是些主觀的好壞評判標準。比如在處理 Promise 時,使用
re
和reject
作為引數實際上是不好的做法,對吧?參考連結:
https://www.youtube.com/watch?v=JWKadu0ks20
宣告:本文為 InfoQ 整理,不代表平臺觀點,未經許可禁止轉載。
今日好文推薦
會議推薦
在 AI 大模型重塑軟體開發的時代,我們如何把握變革?如何突破技術邊界?4 月 10-12 日,QCon 全球軟體開發大會· 北京站 邀你共赴 3 天沉浸式學習之約,跳出「技術繭房」,探索前沿科技的無限可能。
本次大會將匯聚頂尖技術專家、創新實踐者,共同探討多行業 AI 落地應用,分享一手實踐經驗,深度參與 DeepSeek 主題圓桌,洞見未來趨勢。
