2012年9月13日

程式中的 assert

斷言在程式的除錯上尤其重要,畢竟人的記性可說是惡名昭彰,在撰寫程式的時候往往容易犯下技術上微不足道卻致命的錯誤,以下將舉幾個例子來說服你多多使用斷言(assert)。



小心保護你的函數


假如今天你寫了一個階乘函數:

unsigned int
Factorial ( unsigned int number )
    {
    return ( number != 1 ) ? ( number * Factorial( number - 1 ) ) : 1
    }

這個規格是別人給你的,而你也知道這個函數無法處理很大的輸入,但規格書上並沒有要求任何例外處理,似乎是高層覺得天底下沒有笨蛋會給這個函數太大的數字,殊不知對這個函數來說12就已經是極限了,看樣子你只好自己動手來保護她:

unsigned int
Factorial ( unsigned int number )
    {
    assert( number <= 12 );
    return ( number != 1 ) ? ( number * Factorial( number - 1 ) ) : 1
    }

這下子你就Happy囉,萬一你沒下斷言而傳出錯的數字的話,說不定還會有人質疑你的函數哩,現在要是程式垮了,大家就可以來找笨蛋囉。

想了解更多可以參考Code Complete書中的防禦性程式設計

函式輸入的檢查還是驗證?


有天你寫了個用字串作為亂數種子算出亂數的函數:

int
Random( const char * seed )
    {
    if ( seed ) { puts("seed can not be NULL"); exit(1); }
    ...
    return r;
    }

亂數可是很難處理的呢,寫完函數後你為自己感到很驕傲,這時大家卻發現整個程式有50%的時間都耗在你的函數上,於是大家告訴你用if驗證太嚴格了,這個函數的輸入其實很固定,只要在編譯的時候檢查一下就行了,Realse版時並不需要真正的驗證,所以:

int
Random( const char * seed )
    {
    assert( seed );
    ...
    return r;
    }

你把函數改成用斷言檢察,在Debug版時找到了所有的問題,而在Realse版就拔掉檢察節省時間,最後你們的程式玩美無錯,還變得飛快!

這個用斷言檢察的方式旨在檢查契約,而非為輸入作防禦,關於程式契約的概念可以參考Eiffel語言中提倡的契約化程式設計的概念。

整數陣列同除以其開頭


把 [ 2, 4, 6, 8, 10 ] 變成 [ 1, 2, 3, 4, 5 ] 你相信會有人做錯嗎?

好吧,我就做錯過,作法是這樣的:

int numbers[ 5 ] = { 2, 4, 6, 8, 10 };

int idx;
for ( idx = 0; idx < 5; idx++ )
    {
    numbers[ idx ] /= numbers[ 0 ];
    }

有問題嗎?有,而且很嚴重,這個問題我花了很多時間才找出來,或許當初加個前置條件( pre-condition )事情就會輕鬆多了:

int idx;
for ( idx = 0; idx < 5; idx++ )
    {
    assert(  numbers[ 0 ]  != 1 || idx > 0 );
    numbers[ idx ] /= numbers[ 0 ];
    }

前置條件跟程式契約很像,不過她還有後置條件( post-condition )跟一些理論背書,詳請請看FLOLAC課程中講授的Hoare logic

最後來提供一個我個人平常用的斷言技巧,看似簡單卻能有條供更多訊息:

assert( condition && "error message" );

沒有留言:

張貼留言