如何才能寫出沒有bug的程序?

可能很多人看到這個問題覺得我是新手,拿這種問題灌水,其實不然。即使所有程序員都知道不可能有「沒有bug」的程序,或者」沒有bug,那還叫程序嗎「。。。。但如果不致力於探索寫出沒有bug的程序,程序員永遠會陷入無盡的維護深淵,無法盡情地編碼自己的世界。有沒有人在這方面有些見解,可以暢所欲言!


拋個磚引個玉。其實學界幾十年探索的一大動力就是消除編程中的bug。已經被證明行之有效並開始運用的方法有:

  • 類型系統。可以在編譯期檢查出譬如42+"foo"之類的低級錯誤,另外提供了各種抽象機制(template/generics/...)來在類型安全的前提下保證代碼復用。動態語言如Python/Ruby/JavaScript等,沒有編譯期類型檢查,要通過注釋/文檔/單元測試等各種東西來保證工程代碼裡面的各種自定義的類遵守一些contract,比如支持某些方法/有某些數據成員等等。這是種非常naive和不安全的代碼復用方式,而且重構的時候非常傷神。
  • 更高級的類型系統。ML/Haskell等函數式語言里,你可以定義遞歸的類型,比如datatype "a binary_tree = empty | tree of "a*("a binary_tree)*("a binary_tree),處理許多遞歸的數據結構得心應手。C和C++沒有variant type,需要自己用指針模擬,要寫更多boilerplate code,增加了出bug幾率。另外,函數式語言的類型推導能在大多數情況下省卻在代碼里顯式聲明類型的必要,。(C++11的auto乾的漂亮!)
  • 垃圾回收。內存管理不當也是bug一大來源,垃圾回收在許多場合下能使程序員忘記內存管理的煩惱。
  • 形式驗證工具。比如Coq,Agda,以及新近的Idris語言。主要基於dependent types,這是一種特殊的類型系統,可以由值來決定類型,由此進一步拓寬了編譯器能排除的bug範圍。我尚未開始學習這一塊,不甚清楚,題主感興趣的話可以找找Certified Programming with Dependent Types一書看看,講Coq的經典教材,作者主頁上有draft可下載。另外這一塊基本上都是學界的人在用,比如對某某系統(一大半都是分散式)建個模,然後用Coq一證明,然後發篇POPL/PLDI啥的。。
  • 其他的程序靜態分析方法,比如抽象解釋等等。不行瞎掰不下去了,這裡還沒學。。題主自己去看Principles of Program Analysis吧。。

總結一下:有沒有什麼silver bullet能使程序員免除bug的煩擾:

  • 對於許多類型的bug,答案是yes。參見前面答案。有的方法目前比較昂貴(學習成本-&>人力成本),目前用在學界,以及一些對可靠性高要求的領域如航天。許多學界的方法也在成熟和進入應用,比如程序員用的IDE越來越吊,一部分原因就是應用了一些靜態分析技巧。所以未來學界會搞出更多的黑科技,發頂會,賣產品,然後越來越多類型的bug成為歷史。
  • 對於所有類型的bug,答案是no。一部分原因是計算機科學自身極限(搜停機問題有真相),一部分原因是面對複雜和高度變化的世界,程序員犯錯不可避免,如果是業務邏輯上的問題,現階段再牛掰的工具也白搭。當然說不定在這方面程序也能幫上忙,不過我們一般管那個叫人工智慧。

學識所限,如有細節錯誤,歡迎指正~


  • 詳細和無歧義的需求規格和業務邏輯

  • 合理的架構和模塊
  • 清晰明確的模塊間介面
  • 分而治之,降低複雜性
  • 不要複製代碼,儘可能抽取共用的部分,重複的代碼在修改時容易造成不一致
  • 當代碼有自解釋性,使用標識符清洗標明變數和方法的含義,少用字面量,只有一次調用的代碼段也值得抽取成函數
  • 為代碼書寫注釋,函數或方法要詳述介面的參數和返回值,什麼是未定義的行為。單行注釋或多或少,但在特殊的處理處一定要寫注釋,不僅要介紹怎麼干,特別要介紹為什麼這麼干
  • 處理邊界條件,處理非法的參數,考慮到各種邏輯分支
  • 編寫易讀的代碼,不過度使用技巧,難以理解的代碼很可能在修改中出錯

  • 編寫容易維護的代碼,使用面向對象方法和依賴注入,隔離可變性
  • 消除副作用,使用不可變數據(如函數式編程,以Java為例,使用final修飾不可變變數)
  • Fail-fast,使用assert來處理各種進入條件,退出條件等
  • 正確使用異常處理,捕捉能夠處理的異常
  • 不使用goto語句,儘可能減少switch語句,以多態取代switch
  • 限制函數的長度和圈複雜度
  • 單元測試,測試用例的數量應該和函數的圈複雜度相符,度量並提高測試覆蓋率
  • 進行代碼走查或評審,多一雙眼睛可能發現潛在的問題


正常情況下:bug不是人們有意寫的,bug其實是程序員犯的錯誤。

就跟電影中的穿幫鏡頭一樣,如何拍出沒穿幫的電影?拍的時候對畫面的考慮認真全面一點,還有剪輯的時候認真檢查修補一下兒。

就跟春晚等表演中的錯誤一樣,得多綵排,多檢查,多練習。

其實錯誤的原因有兩大類,一類是無知犯錯,一類的粗心犯錯。所以解決方法也有兩個部分,一個是增長知識,一個是細心認真。

其實我覺得存在「沒有bug「的程序,程序的bug其實指的就是程序非預期的行為或輸出,如果我們可以合理設計並清晰描述程序預期的行為和輸出,那麼我們完全可以構造出完全符合這種預期的程序。比如有興趣的話你可以讀一讀操作系統的形式化驗證:形式化驗證操作系統

這種思路就像是我們如何建設整個數學體系一樣:根據大家公認的公理加上大家認可的推理過程,來推理證明其它的命題。


對程序的性質進行形式化證明就行了。維基:http://en.wikipedia.org/wiki/Formal_verification 這樣開發出來的軟體就是 formally verified software。

不過因為需要用到很神(nan)奇(yong)的工具和方法所以在近期肯定成不了主流呢……


單元測試是我覺得最有效的方法。除了測試還是測試。其實很多程序,只要代碼結構好,思路清晰,都可以很好的避免bug。但你面對的是計算機,一切東西在效率為王的前提下都是扯淡,我們明明知道某些正確優美的結構,但是他在b系統下就是跑不快,所以優化一來,再好的代碼都變渣,剩下的唯有自動化和人肉測試才是你的朋友。


BUG,先分分類,不同的類型的Bug,不同的處理方式。盡量照顧到了,Bug就會少很多。

1.特殊需求。

特殊需求不算是bug,但是可能因為有些特殊的要求,導致代碼複雜度快速上升,而且重新架構之後,依然無法簡化代碼結構。這個就要和提出需求的人討論下,看看如何改進需求。在完成功能的情況下,降低代碼複雜度。

2.邏輯性Bug。

比如數值轉換錯誤,演算法錯誤,計算結果不對等等。這塊就是考驗開發人員自身功力,特別是理解需求和耐心以及細緻了。全看個人了。繞不過去,提升自身能力為上。

3.框架和框架應用Bug

有些框架本身自帶bug,被代碼觸發之後,是修正還是繞過去,完全看具體的情況了。

框架應用bug,因為對框架的某些技術細節不熟悉,胡亂用框架代碼導致的bug。或者代碼需求已經超出框架設計初衷了。要麼仔細學習文檔,要麼繞行。

4,外部環境變化引入的Bug

舉個例子來說:

網路伺服器的開發,一般的開發環境都是高網速的區域網中,實際部署之後,可能會遇到極低網速連接情況。可能會引發在高速網路開發環境中無法發現的Bug。

資料庫連接也是同樣的問題,高負荷生產資料庫和低負荷的開發伺服器的不同,會帶來一些bug

這樣的不可預料的偶發Bug,只能是記錄好關鍵日誌,以備後查。

其他的編譯器能檢查出來的Bug,就當不存在了。

對於研發人員來說,一個需求穩定的項目,邏輯性Bug是能夠完全消除的。

其他三者導致的Bug,則是不可控的。只能是努力減少,或者增強預防性代碼的編寫,多多多多多多多多多多記錄運行日誌,然後等小白鼠來觸發炸彈。

再根據日誌信息來還原現場,找出真兇,幹掉!


簡單的回答:使用契約式設計

契約式設計

Bertrand Meyer思考過相同的問題,於是他發明了Eiffel語言(Eiffel (programming language))。在這種語言里,他首次提出了Design by Contract——契約式設計的思想,這也是軟體工程界對『無bug程序』最激進的嘗試。契約式設計要求程序給出並遵守三種契約:

  • 函數的先驗條件,即對函數參數和調用函數時的上下文的約束。例如,對於Mage(法師)類的RecoverMana(回藍)方法,其參數mana(回復的法力值)必須大於0;
  • 函數的後驗條件,即函數的返回值和退出函數時的上下文的約束。例如RecoverMana方法必須保證在退出時的Mana成員大於進入時的值。
  • 不變式(Invariant):在函數進入和退出時應保持不變的約定。例如,Mana成員的值永遠不應該大過MaxMana,也不會小於0。

有了這些契約,就可通過特定的工具來找出程序中違法契約的地方——也就是潛在的bug:

  • 通過編譯時的靜態分析器可以對契約進行靜態檢查,因此一些靜態地違反契約的地方就會被找到(例如 mage.RecoverMana(-3) );
  • 運行時檢查:契約代碼會在運行時起到檢查的作用,找到動態的違反契約的地方。運行時檢查是會影響程序的性能的,因此可以僅在調試時開啟,在發布時關閉。

另外,

  • 通過契約可以生成更有意義的單元測試,不會測試不符合契約的參數;
  • 契約可以用於豐富程序的文檔,通過自動生成文檔的工具可以將契約寫入文檔中。

那麼,理論上,只要你編寫了完美的契約,並達成了完美符合契約的實現,你的程序就是沒有bug的——而這並非是不可能的事情。編寫契約雖然是一件費時費力的事情,但它為編寫無bug程序提供了一條可靠的路徑。

語言和類庫的支持

契約式設計並非僅僅是學院派提出的概念。自這種思想提出後,很多語言都在語法層面上相繼實現了對契約式設計的支持,包括Clojure、Perl6等。更主流的語言則傾向於通過庫來實現相應的支持,例如微軟在.NET Framework 4.0里加入了Code Contracts,Java則可以通過 jContractor(http://jcontractor.sourceforge.net/)等庫來實現。


追求寫出一次性寫出永遠零 bug 的程序是不可能的,也毫無意義。但是追求寫出不令自己死得很慘的 bug 還是有可能,並且意義重大。顯然,發現 bug 的時機決定你是否會死得很慘,而 bug 出現在上線之前還是上線之後這一時機也至關重要。

所以我還是談談如何讓 bug 在上線前暴露的一些淺見吧。

  1. 不僅要提供一段可正確運行的程序,還要提供一個證明該程序「正確性」的方案。這並不是說非得要你寫一個 Test Case。實際上你是通過用滑鼠點點點,還是用肉眼一個一個看都沒關係,關鍵是這麼做能證明你的程序是正確的。
  2. 接上條,盡量別讓你的程序依賴不可控條件。例如,寫多線程程序。如果多線程方案可用單線程代替的話,就用單線程吧。(甚至以性能為代價也未嘗不可,其實多線程寫不好性能可能還不如單線程呢。)同樣的,複雜的高並發資料庫同步事務也會如此。
  3. 如果你真的寫了有N個競爭資源的多線程程序,或高並發資料庫事務,就給自己上個專註+200%的 buff 吧。當然,這事一定記得要告訴 QA 同事。另外,發版上線的時候,留意運維同事的郵件。(其實就是慘~)
  4. 如果你的程序行為中有隨機行為,可以提供一個固定隨機數種子的方法,並且把這個方法告訴 QA 同事,以方便 bug 重現。
  5. 如果只有黑盒測試,儘可能讓 QA 同事了解完整的邏輯,以便100%的代碼至少跑過一遍。另外,如果你在你的程序中有什麼彩蛋(如當且僅當4月1日彈出個框說」愚人節「快樂)之類的,一定要讓 QA 知道有這回事。
  6. 最後,有種東西叫做測試驅動開發。


將bug所代表的東西重新命名,例如命名為buk,這樣bug就消失了


給我嚴格形式化的需求文檔,我給你一個沒有Bug的程序。

PS:有Bug也不會是我程序的Bug,只可能是環境或者編譯器的。


我是在學校圖書館學會編程的。

我花了很長時間看了很多關於C語言編程的書,卻始終沒有寫過一行代碼。

我一直在懷疑這些書中寫的東西是不是真的。真的只要幾行代碼就可以在屏幕畫出各種幾何形狀?真的可以寫一些代碼就能編出一個能跟人下棋的程序?

我當然沒有那麼愚昧,真的在懷疑這些事情本身。我只是隱約覺得,如果編程這件事這麼神奇,也許將會改變我的人生。而那段時間我對自己很絕望,不相信自己還會有美好的未來。所以我懷疑的其實是自己,我真的可以寫程序完成這些神奇的事情嗎?所以我看了很多編程的書,卻始終沒有勇氣到電腦上真正去寫一行代碼。

最後,當我花了一塊錢,在學校的計算機房買了一個小時的上機時間決定寫下自己人生的第一個程序的時候,感覺自己像是去朝聖,內心中的感受大約和藏傳佛教的信徒磕了幾千里的等身長頭最終看到布達拉宮一樣。

為了這一刻,我把一個簡單的程序在草稿紙上反覆寫了無數遍,而在那台386電腦上輸入了這段小小的程序後,我又認真的檢查了每一個字元。

當我按下回車鍵,運行程序的時候,彷彿在觸摸某種神跡。

程序順利運行,完美地呈現了這段代碼最初的期望,那一刻,我淚流滿面,渾身顫抖,我覺得自己進入了一個新的世界。

那是我寫的第一個程序,那是我寫過的唯一一個沒有BUG的程序。


在沒有發現Bug之前,這個程序是沒有Bug的。。


這裡空白太小。

不妨把你的github地址貼出來,我們談點具體的?


bug的確無法做到沒有,但是要盡量做到沒有致命bug,一些IDE如visual studio等會通過語法檢查和拼寫檢查來消除語法和拼寫bug.但是邏輯bug只能靠自己發現。


測試驅動。

寫任何方法之前先寫測試代碼。

任何代碼改動之後先運行測試代碼。

需求變更之後,先改測試代碼。


程序要按需求實現,即使程序100%符合需求。可需求本身有Bug呢?

我要讀寫都O(1)!100%可用性!100%數據一致性!支持無限個用戶並發的資料庫!這是需求!


需求改的少,bug就少。

最常見的情況是,程序寫了大半,需求變了,然後開始各種hack,然後就出現各種bug。

但需求總是要變的,我做了十年了,還沒見過不變的需求。產品經理經常要做一件事情,就是用一句話描述你的產品。我們程序員也應當這樣,用一句話描述 你正在做的模塊。這可以告訴我們什麼是最重要的,最基礎的功能,這些功能是最穩定,最少改變的,應當最早做。與核心功能越遠的,就越不重要,需求變化的可能就越大,甚至被取消,這樣的就應當越往後做。

有的功能可能是技術難點,但它不一定是項目重點,設計和編程應當圍繞核心功能,而不是那些困難但不那麼重要的方面。

開發的順序很重要,先做核心,不斷迭代,需求也會相應進化,雖然它在變,但傷筋動骨改動會很少,很多時候是需求追著設計跑。也很好玩。

如果需求已經不變了,程序還是很多bug,就需要加強基本功訓練了,實在不行,就提高自己的測試技巧吧。


大師如是說:「任何一個程序,無論它多麼小,總存在著錯誤。」

初學者不相信大師的話,「如果一個程序小得只執行一個簡單的功能,那麼會怎樣?」他問。

「這樣一個程序將沒有意義,」大師說,「但假設這樣一個程序存在的話,操作系統最後將失

效。產生一個錯誤。」

但初學者不滿足。「如果操作系統不失效,那麼會怎樣?」他問。

「沒有不失效的操作系統,」大師說,「但假設這樣一個操作系統存在的話,硬體最後將失效,

產生一個錯誤。」

初學者仍不滿足。「如果硬體不失效,那麼會怎樣?」他問。

大師長嘆一聲。「沒有不失效的硬體,」他說,「但假設這樣的硬體存在的話,用戶就會想讓

這個程序做一件不同的事,這件事也是一個錯誤!」

沒有錯誤的程序是一則謬論,世間難尋。假設存在著一個沒有任何錯誤的程序,那麼這個世

界將會不復存在

----《編程之禪》第四篇(金)第二節


bug,俗稱缺陷,即不完美。

你見過這世界上有完美的東西嗎?

沒有!

那為什麼要追求一個完美的代碼呢?


print("hello world")

return

就是人類已知的最早的絕無bug的程序


推薦閱讀:

為什麼OJ上有些人提交的代碼運行那麼快? 有人總結這些技巧么
如何解決 C++ 代碼不能打開提示有一個錯誤的問題?
為什麼在 C++ 中,人們經常寫全命名空間,在其他語言卻不呢?
既然編譯器可以判斷一個函數是否適合 inline,那還有必要自己加 inline 關鍵字嗎?
Windows 下最佳的 C++ 開發的 IDE 是什麼?

TAG:程序員 | 軟體工程 | Java | C | C# |