TEST

顯示具有 程式 標籤的文章。 顯示所有文章
顯示具有 程式 標籤的文章。 顯示所有文章

2010年11月16日 星期二

exception使用的時機

一直以來對於 exception 使用的時機不是很清楚,所以從來沒用過,想說反正也有 google約爾等有名大師都說不要用 exception ,所以一直以來都是使用 error code return (或紀錄 error code) 的方式。 但也有人說 exception 不是惡魔,且沒了 exception 就沒有 RAII 這方便的作法。但看了半天 RAII 是跟 exception 綁在一起的嗎?沒有 exception 一樣可以做 RAII ,是沒有 exception 就沒有做 RAII 的理由吧? exception 可以說是一種 goto ,對於程式流程跟可讀性來說,會造成人腦在run程式的複雜度。我也覺得非必要時候不用 exception 。那何時是非必要時?何時又是使用 exception 的時機呢? 這時就必須了解c+++中常用的處理錯誤方法:
  • assert
  • error code
  • exception
在此不闡述,就我自己的理解思考,使用 exception 時機有
  1. 用到 STL 或有拋出 exception 的程式,因你無法保證 100%不會有 exception。
  2. 簡化程式流程的時候,這在預設執行的各 method/function 都是 99.9%正確時可簡化,遇到不能考慮的錯誤時就是那 0.01%。
  3. 子程式不知怎麼處理此錯誤,可能父程式或父父程式以上(最高為使用者)知道如何處理時。
  4. 想回傳error code卻不行時。
避免濫用
  1. 不要在同一 scope throw, catch,這很明顯可以用 if else/switch 來解決,但是很多書上都這樣教。
  2. 父程式可考慮解決的錯誤可以使用 error code。
有時 exception 使用上沒有 error code 來的簡潔,比如說以下這段,你只是要秀錯誤訊息那用 exception 很方便但是要針對錯誤來處理呢?
while (IsSending)
{
 try
    {
        foo->Send(SendData);
        IsSending=false;
        foo->Save(SendData);
    }
    catch(Exception &e)
    {
        ErrorMsg = e.ClassName() + ":" + e.Message;
    }
}
還是使用 error code 的方式吧,我無法想像用exception來處理:
while (IsSending)
{
    ErrorCode error_code;
    error_code=foo->Send(SendData);
    switch(error_code)
    {
    case SEND_OK:
        IsSending=false;
        error_code=foo->Save(SendData);
        switch(error_code)
        {
        case SAVE_OK:
            break;
        case SAVE_NO_FILE:
            //Create a new file
            break;
        case SAVE_BE_LOCK:
            //UnLock or throw if can't be UnLock here
            break;
        }
        break;
    case SEND_NOT_FINISH:
        //Get left SendData  (then Send)
        break;
    case SEND_EMPTY:
        //WaitData or GetData (then Send )
        break;
    case SEND_BE_LOCK:
        //UnLock(then Send) or throw if can't be UnLock here
        break;
    }
}
好吧寫個參考,用exception來處理:
while (IsSending)
{
    try
    {
        foo->Send(SendData);
        IsSending=false;
    }
    catch(Exception &e)
    {
        ErrorCode error_code = e.Code;
        switch(error_code)
        {
        case SEND_NOT_FINISH:
            //Get left SendData  (then Send)
            break;
        case SEND_EMPTY:
            //WaitData or GetData (then Send )
            break;
        case SEND_BE_LOCK:
            //UnLock(then Send) or throw if can't be UnLock here
            break;
        }
    }
    
    try
    {
        foo->Save(SendData);
    }
    catch(Exception &e)
    {
        ErrorCode error_code = e.Code;
        switch(error_code)
        {
        case SAVE_NO_FILE:
            //Create a new file
            break;
        case SAVE_BE_LOCK:
            //UnLock or throw if can't be UnLock here
            break;
        }
    }
}
嗯有比較簡潔嗎? 或許可以改寫成以下方式:
while (IsSending)
{
    try
    {
        foo->Send(SendData);
        IsSending=false;
        foo->Save(SendData);
    }
    catch(Exception &e)
    {
        ErrorCode error_code = e.Code;
        switch(error_code)
        {
        case SEND_NOT_FINISH:
            //Get left SendData  (then Send)
            break;
        case SEND_EMPTY:
            //WaitData or GetData (then Send )
            break;
        case SEND_BE_LOCK:
            //UnLock(then Send) or throw if can't be UnLock here
            break;
            
        case SAVE_NO_FILE:
            IsSending=false;
            //Create a new file
            break;
        case SAVE_BE_LOCK:
            IsSending=false;
            //UnLock or throw if can't be UnLock here
            break;
        }
    }
}
雖然在程式上看來較簡潔,但是對於程式流程卻較複雜,隨著程式碼越多,跳到哪裡都不知道了... 其他不使用ex的原因還有影響效能,團隊編程顧慮等。 但使用error code (status code) 也有以下缺點:
  1. error code是可以被忽略的。
  2. 無法自動向上上層(父父程式)傳播。必須由上層接力在丟一次。
  3. 在程式流程中不能集中處理(某些情況是優點)。
  4. 必須有回傳值或紀錄值。
反過來說你不想要這些缺點也是使用 exception 的時機。

Next: c++ checked exceptions (Exception specifications)?
根據[C++ coding Standards] 書中第 75 項 Avoid exception specifications , 且在 A Pragmatic Look at Exception Specifications 中的建議 "Never write an exception specification." 所以就不研究了,進階可看 Questions About Exception Specifications

[Reference]

2010年6月17日 星期四

ASSERT的使用時機

ASSERT全大寫表示它是全域的。它是一種假設,當假設不成立時,程式就得全部中止。且只作用在Debug時,為的就是讓你在Debug時可以更快地寫程式、找出錯誤。Release時會不會假設就不成立了?有可能,但通常寫程式Debug時就會出現ASSERT出錯的情況,在此時就會快速的發現錯誤,如果Relese版本才出錯,就是考慮的不夠嚴謹,有些ASSERT沒有設定,或是某種情況Relese版才出現。此時更需要Unit Test配合。

寫程式需不需要到處都先假設?

被呼叫端的函式比如fun(obj * o);就必須成立一假設ASSERT(NULL!=o);
為何不return error code或throw exception,要考慮的是上層有沒有檢查就呼叫。通常是會檢查的,就沒有返回錯誤碼或丟例外的必要,但並不能保證。

如果上層要檢查那上上層要不要檢查?在每一層都必須檢查是無謂的。

通常一個軟體設計會分成三個模組,也就是所謂的MVC,是指模組而言。在程式邏輯可分成Controller、UI、Library三層,是有點類似的。Controller呼叫UI和Library,而UI傳送使用者event給Controller。在大型的軟體,可能Controller上還會有一個總體的Controller,Library可能還會呼叫別的Library。
實際的程式可能是C1->C2->L1->L2->L3,UI可能由C1整合(C1<->UI),

設有一程式輸入名子就可以知道工作時數來取得Payment(薪水、股票)
UI(input)<->C1->C2->
fun1(string name)->fun2(int work_time)->fun3(Payment * pay);

fun1就必須有ASSERT(!name.IsEmpty());
fun2就必須有ASSERT(work_time>0);
fun3就必須有ASSERT(NULL!=pay);

在此情況*下L1,L2,L3都必須ASSERT,而C1或C2就必須檢查避免傳遞無效名子。Controller會去使用UI跟Library,被呼叫端UI跟Library就必須要假設是被正常使用。

但Controller不能有ASSERT,因為它不能有任何的假設情況,必須把會發生的都考慮進去,遇到不正常的時候就阻擋。而被呼叫端都必須要確認情況是成立的才能繼續執行。煩惱就交給上層,下層就只要假設就好了。

C++ 編程規範70提到error有三種:

  • Precondition(前條件)
  • Postcondition(後條件)
  • Invariant(不變性)

書中講得很清楚,在此不重述。只有不變性比較抽象,不變性就是滿足一個有效狀態(valid states)物件的條件。比如說一個有帳號、密碼的物件,帳號和密碼必須是成對且正確對應的,其中一個有錯就是error、無效狀態。這很明顯不該使用ASSERT來保證*。

書中說到Postcondition也可用ASSERT來確認:

"假設你呼叫某個API函式,其文件說它總是返回正值,但你懷疑該函式有臭蟲,那麼你可以在呼叫該函式後使用assert驗證其結果。"

這我認為是多餘的,不是很建議。當此API函式出錯有兩種解法:

  • API可以修改:修改API錯誤,但不管Debug還是Release時還是要再確認。
  • API不可修改:此時也不能用ASSERT中止,且Release版本也不希望出現這個錯誤,當然是由上層想辦法檢查繞過來解決。

不管Debug還是Release時,你還是得再一次確認結果是你所預期的,此時可搭配Unit Test來確認。要不然就是上層自行檢查來避開,沒有必要用ASSERT。Postcondition真的有需要ASSERT時,我覺得只有在驗算的時候,比如設計類似Excel的軟體,寫了兩個演算法來計算同樣的數學式,一個是用來驗算,而且只有Debug版本會算兩次比較慢,不相等的話就一定是其中一個有錯。

總結ASSERT的使用時機:

  • Precondition
  • 當你寫的程式是被使用的UI或Library,請多多利用ASSERT來防範吧。

*這只是假命題,fun1也可以接受空名子再處理,fun2可以用unsign int限制,fun3也可以傳reference防null物件,所以實際上用到ASSERT的情況不多。
*不要用ASSERT報告執行期錯誤[C++ 編程規範68]。