1. [RUN! PC] 2008 十一月號

    IMG_0208

    YA! 第四篇!! :D 還是一樣要先感謝一下編輯賞光,讓我有點空間寫些不一樣的東西。

     

    基本的執行緒相關的程式設計跟函式庫,講的差不多了,其實這些也沒什麼好寫的。接下來打算寫一些應用的模式,來談談有那些方法,那些設計方式才能夠有效的發揮多執行緒的優點。看了 .NET Framework 4.0 / Visual Studio 2010 的 ROADMAP,有一大部份的重點擺在平行處理,INTEL年底也要發表四核 + HT 的 CPU ( WINDOWS 會認為有八個處理器 ),軟硬體都備齊了,剩下的就是程式設計師的巧思了。

     

    其實之前貼過幾篇類似主題的文章,只是這次把它統合起來介紹一下。生產線模式,如果簡化後就是 [生產者消費者] 的模式,而把它徹底一點的應用,則是上回提到 [Stream Pipeline] ..

    這篇也是第一次在雜誌上嘗試說明比較偏設計概念的文章,實作比較少,很怕不合讀者的口味... 應該不會貼了就沒續篇了吧? :P 有買雜誌的記得讀者回函填一下,哈哈,也算是點鼓勵。這次範例程式也是 Console application (我不會寫太炫的程式 :P ),需要的可以點 [這裡] 下載!

    2008/11/04 RUN! PC 專欄文章 .NET RUN! PC 作品集 多執行緒 技術隨筆

  2. 該如何學好 "寫程式" #5. 善用 TRACE / ASSERT

    哈哈,這篇拖的夠久了 :P 上篇扯太多,寫到一半寫不完就留到這篇了。寫出可靠的程式,這是軟體工程師的基本要求。上篇提到了 TRACE / ASSERT 的應用,來複習一下: TRACE: 原本是 C 的除錯用巨集,目的是用適合的方式輸出除錯用的訊息,用來跟一般的訊息輸出有所區別。因為用的是不同的方式輸出,可以很容易的統一關掉。隨著工具的進步,輸出的方式也越來越適合除錯,比如輸出到開發工具的除錯視窗,或是輸出成記錄檔等等。 ASSERT: 也是除錯用巨集,它接受一個 bool 參數,輸入值為 TRUE 時一切正常,就像沒呼叫一樣,輸入 FALSE 則會中斷程式,或是輸出顯目的警告訊息。目的在於確保程式的每個步驟情況都如預料般的順利。 這兩個東西從 C 的巨集,衍生出各種語言及環境都有各自的版本。它的目的很簡單,就是 [Writing Solid Code] 裡提到的: 用同一套程式碼,同時維護兩個版本 (RELEASE / DEBUG),讓錯誤自動跑出來   雖然這本書提到了不少技巧,正確的應用 TRACE / ASSERT 是最基本的。但是那些細節並不是主要的重點。重點是你在寫 CODE 時有時時刻刻記得要盡量減少 BUG 嗎? 你有正確的擬出對策嗎? 來看看上回最後一段範例程式:  

    加上 ASSERT 的算分程式碼[copy code]
    public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
    {
        int totalScore = 0;
        int itemCount = quiz_question.SelectNodes("item").Count;
        Trace.Assert(quiz_question != null);
        Trace.Assert(paper_question != null);
        Trace.Assert(paper_question.SelectNodes("item").Count == quiz_question.SelectNodes("item").Count);
        //
        //  如果都沒作答, 此題放棄
        //
        if (paper_question.SelectNodes("item[@checked='true']").Count == 0)
        {
            return 0;
        }
        //
        //  題目的配分
        //
        int quiz_score = int.Parse(quiz_question.GetAttribute("score"));
        //
        //  答對一個選項的分數
        //
        int item_score = quiz_score / itemCount;
        for (int itemPos = 0; itemPos < itemCount; itemPos++)
        {
            XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;
            XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;
            //
            //  算成積
            //
            if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))
            {
                totalScore += item_score;
            }
            else
            {
                totalScore -= item_score;
            }
        }
        Trace.Assert(totalScore >= (0 - quiz_score));
        Trace.Assert(totalScore <= quiz_score);
        return totalScore;
    }
      各位仔細看一下加上 ASSERT 的地方。大家寫程式,通常都是腦袋裡想著 "我要處理什麼問題" ,很少人會去想錯誤處理的部份。沒錯,這部份的確是吃力不討好,以此例來說,光是傳進來的參數就有可能狀況百出了。正常的流程都寫不完了,誰還有力氣去把這些錯誤都擋下來? 不過最容易出錯的地方也在這裡。我常在跟其它工程師說,正確的資料 (參數) 傳進來,本來就應該有正確的答案傳出去。難的是錯誤的資料傳進來,你還得回應 "正確" 的錯誤訊息回去,這才真的是個挑戰。這時 ASSERT 的效果就出來了。你可以把 ASSERT 想像成 "宣告" 的子句。以 line 5 ~ 7 行為例:
    確保傳入參數是正確的[copy code]
    Trace.Assert(quiz_question != null);
    Trace.Assert(paper_question != null);
    Trace.Assert(paper_question.SelectNodes("item").Count == quiz_question.SelectNodes("item").Count);
    
      這三行看在我眼裡,意思就是: "這兩個參數不能是 NULL,而且兩個 XML ELEMENT 都要有一樣數量的子節點 (Element),否則就不惜代價警告我" 同樣的,在程式的中間,還有傳回值之前,也都可以用同樣的方式來替你的程式 "把關"。再來看看算完成績後,要把值傳回去之前的 CODE:
    確保傳回值的範圍正確的程式碼[copy code]
    Trace.Assert(totalScore >= (0 - quiz_score));
    Trace.Assert(totalScore <= quiz_score);
    return totalScore;
    
      這兩行的意思就是: "不管成績怎麼算,每張答案卷最後的總分一定介於 0 ~ 滿分之間。一樣,有例外的話就不惜代價警告我"   聽起來蠻狠的,不惜代價...,不過使用 ASSERT 的話就真的是這樣。通常碰到 ASSERT 後,程式不是進 DEBUGGER 就是直接關掉了。不過請大家注意一下,並不是到處加上 ASSERT 你的程式就沒問題了。要搞清楚加上它的目的是什麼。它要抓的是你程式的 BUG,不是執行期的錯誤 (比如 USER 輸入錯誤的值,或是必填的資料沒填等等)。執行期的錯誤,你還是得乖乖的寫程式,不能用 ASSERT 替代。 舉例來說,如果最後算出來的分數是負的,則會觸動 return 前的 ASSERT。有些有點經驗又有點兩光的 PROGRAMMER 可能會自己顯示一些錯誤訊息。但是這跟本不干 USER 的事啊! 會出現這種情況,錯的一定是 "程式" 本身,也就是你看到 ASSERT 警告後就該來改程式抓 BUG 了。加上 ASSERT 的目的就是在你的程式到處布下眼線,任何一個地方偵測到不對勁,馬上通知你來處理。 當你有心把程式寫好時,你才會覺的這樣作是必要的,而不是累贅。你眼線布的越多,BUG就越難藏在你的程式裡。相對的,如果傳進來的參數就不對了,那應該怎麼辦? 這時就要小心分清楚你要抓的是 BUG 還是做錯誤處理了。如果參數是 USER 直接輸入的,那收到 NULL 或是錯誤的值本來就有可能 (吃芝蔴那有不掉燒餅的...),你需要的是老老實實寫好錯誤處理的流程。但是如果你的 API 早已嚴格定義不接受 NULL,卻還是有白目的工程師硬把 NULL 傳給你的 API,那這時就是 BUG 了,應該用 ASSERT 抓出來,然後找到冤大頭叫他改程式。 不過這樣的 CODE 可不能交到 USER 手上。想像一下如果你正在用 WORD 打文件,結果碰到一個小 BUG,ASSERT 就跳出警告訊息要中止程式,你連存檔都來不及,大概會抓狂吧。這時就是一份程式碼兩種版本的作法發威的地方了。交給 USER 的程式,就應該是切到 RELEASE MODE (或是關掉 ASSERT / TRACE) 編譯的版本。這時所有的 TRACE / ASSERT 好像完全消失一樣,程式就如同一般情況運作。 當 USER 回報一些很難抓到的 BUG 時,這時就可以打開 ASSERT 或是改用 DEBUG BUILD 的版本,再讓 USER 去重現 BUG,這時如果你都有老老實實加上 ASSERT 的話,BINGO,問題在那就一目了然。看看是那一道 ASSERT 指令被觸發,就知道是什麼問題了。抓 BUG 最麻煩的就是找出錯在那裡,而善用 ASSERT 就可以讓 BUG 自己跳出來告訴你出了什麼問題,只要你養成好習慣。   再舉一個應用例。看到 Steve Maguire 先生舉這個例子,真是想拍手叫好。他舉了他們在開發 EXCEL 時的例子。EXCEL就是要替試算表作一堆運算,當年還在 DOS 時代,CPU怎樣都不夠快,RAM怎樣都不夠多,程式設計師無不絞盡腦汁,要榨出所有的運算能力,最佳化做到無所不用其極的地步。不過這種東西是錯不得的啊,少算了一塊錢還得了? 碰到這種問題你該怎麼辦? 通常,我們都會先有個安全的版本,算的不快,但是因為邏輯簡單,比較不容易出錯。這種版本寫出來後才開始想盡辦法,去改善程式讓速度加快。馬先生 (ㄜ... 是馬奎爾先生... ) 就充份應用了 ASSERT,隨時都要把 BUG 逼出來的精神,真的把 "驗算" 的方法應用上來。它的作法很簡單,同一張試算表,用兩份不同的程式碼各計算一次,最後再來比對一下結果 (驗算)。只要兩者得到的答案不一樣,那就是出問題了! 當然也有可能是安全的版本寫錯了,不過你至少多了個機會抓到問題,因為不一樣的話,一定 "至少" 有一邊是錯的!   沒有這樣的前題的話,各位看到可能都會在心裡想: "有沒有搞錯,程式都寫不完了,還要寫兩種演算法來驗算?? 老闆又不會多給我一點薪水..." 沒錯,這的確是成本較高的方法,每套系統應該都有關鑑的地方,只要有絕對不能失誤的地方,就值得用這種作法。速度的問題怎麼辦? 很簡單。你只要在 DEBUG MODE 才啟用這 "驗算" 的機制,測試人員輸入各種數值做黑箱測試,如果每次測試的過程中發現驗算錯誤,則 "黑箱" 測試就能幫助你抓到只有 "白箱" 測試才有可能抓到的 BUG !   我寫的這個範例程式 (算成績) 其實也準備了兩個版本。上一篇貼的是基本的作法,結果比較可靠。而為了效率我也寫了另一份程式碼,用的是位元運算,希望藉著位元運算,一次就把多選題的答案給算出來。開發的過程中就用了 ASSERT + 驗算的技巧,它不會加快我寫程式的速度,但是它可以加速我找到 BUG 跟解決 BUG 的時間!   有沒有覺的這跟單元測試其實很像? 沒錯。單元測試就是一樣的觀念演變出來的作法,所以你用的單元測試 FRAMEWORK 也延用一樣的 ASSERT 使用慣例。你會發現其實之間的觀念都是相通的,只不過單元測試更進一步的把它系統化了,由原本四處藏在程式碼中的 ASSERT,抽出來成為一個一個獨立的 TEST CASE,由原本被動的執行時期檢查,演變為主動執行所有測試的 UNIT TEST。我覺的 Kent 在 XP (extreme programming) 裡舉了一個例子來說明單元測試,比喻的很貼切,我覺的也一樣能拿來比喻 ASSERT:   "車子裝了煞車,是要讓車子能開的更快!"   聽起來好像很蠢? 煞車明明是讓車子停下來的... 其實不然。想像一下如果你的車子沒煞車,你敢開多快? 了不起就是撞到不會怎麼樣的速度,或是油門放開就停下來的程度而以。有了煞車讓你有信心,碰到危險時你隨時能把車子停下來,你才敢把車子開上高速公路...   很有道理的比喻,ASSERT 跟 UNIT TEST 大部份人都覺的是 "煞車",是拖慢你速度用的,但是也因為有這些 "煞車",你才能放心的衝更快。當你有充份運用 ASSERT 的話,你就能很放心的寫程式,沒有後顧之憂。其實類似的關念,Steve Maguire 的書還有提到很多,只不過它的範例都是用 C 寫的 (還不是 C++ ...),看起來會吃力一點。範例程式可能對現今大部份的人都用不到,但是裡面的觀念跟作法還是很有參考價值的,手上還有這本書的人不妨拿起來翻一翻。   講到這裡,花了兩篇才講完第一個部份,主要的重點就是用 TRACE / ASSERT 來說明,要讓你的程式夠穩定,第一個要改進的就是你寫程式的想法,觀念及態度。各位不妨以這兩篇的例子,自己回想看看,你做到那幾項:
    1. 你寫程式有考慮到這些問題嗎?
    2. 如果你寫程式有用這些方法,有多少你曾解過的棘手 BUG 會變的迎刃而解的?
    3. 加上 ASSERT 之後,你是否對你程式更有信心了?
    4. 你是否更認同單元測試的必要了?
      想法跟觀念有了改變,才有可能開發出優良的軟體。你開始認同這樣的想法了嗎? 恭喜你,你已經跨出第一步了。不過光是 BUG FREE 還不足以成為優秀的軟體工程師,這只是必要條件之一而以。除了把程式寫的 "可靠" 之外,接下來的挑戰是如何把程式寫的 "漂亮" ? 下回要開始來探討如何構思你程式碼的結構。什麼樣的結構,什麼樣的方式去分析你的問題,才寫的出架構漂亮的程式? 別急,請期待續篇 :D     -- 註: 範例程式很多 CODE 被我跳過去了,有興趣的人可以抓回去研究看看... 請點 [這裡] 下載。

    2008/11/03 系列文章: 如何學好寫程式 .NET C# 作品集 專欄 技術隨筆 有的沒的 物件導向

  3. 也是 "生產者 & 消費者" ...

    IMG_3467 (Canon PowerShot G9)

    哈哈,貼一下家裡魚缸的照片... 家裡養的孔雀魚一直生就算了,無意間丟進來的一隻蝸牛,沒兩個月竟然也生了一堆,現在算算大概有四十隻吧 @_@,照片裡紅紅的都是...

    不過有了蝸牛 (消費者),把水裡的魚大便跟水藻都吃的乾乾淨淨的也不錯啦,以前每週要換一次水,現在偷懶撐久一點都無所謂了 :D

    2008/11/01 有的沒的

  4. 關不掉的 Vista UAC !?

    不知道是更新了啥 PATCH,還是那次沒正常關機,我公司 VISTA 的 UAC 突然莫名奇妙的被打開了。怪的是控制台裡看到的還是關掉的,不管怎麼改狀態也不會改變 (一直都是關的) ...。

    直覺告訴我一定是控制台的 AP 那邊出問題,設定值寫不進去造成的...,於是我就開使找其它可以修改 UAC 設定的方法...,最後找到這個,還真的成功了 :D,看來沒機會動用 ProcessMonitor 追追看問題了..

    找到的方法是: msconfig.exe

    在開始 --> 執行裡輸入 msconfig.exe 後,可以看到這一項:

    image

     

    看來是直接修改 registry, 果然有效,直接執行後 REBOOT 就一切正常了 -_-, 如果有人也碰過一樣的問題可以試看看!

    2008/10/31 Tips

  5. 該如何學好 "寫程式" #4. 你的程式夠 "可靠" 嗎?

    撐了很久,續篇來了。這次要進階一點,直接從 software engineer (軟體工程師) 的階段開始。   所謂的軟體工程師,我對它的定義是在這個領域已經算是資深人員了。programmer 該作的是把程式寫好,要挑正確的方式及技術寫好你的程式 (如之前幾篇介紹的演算法及資料結構等等)。而軟體工程師呢? 之前介紹的那些已經不夠了,你該好好的安排你的 code 及工具,要能把你的 solution (如會用到的演算法及資料結構),跟你手上能運用的資源 (如程式語言、開發工具及函式庫) 作最佳化的搭配及整合。因此我認為在這階段的重點有幾個:

    1. 先成為一個好的 programmer (廢話)
    2. 程式要有足夠的可靠性 (穩定、沒有BUG、易讀、對於未知問題的防禦能力)
    3. 要有足夠的系統知識 (比如作業系統/API/系統服務/記憶體管理等等 OS 提供的環境及功能)
    4. 程式要有好的結構 (正確/優秀的類別設計、好維護、有足夠的擴充及應變能力)
    5. 要有解決未知問題或是未知 BUG 的能力,有自行學習新知的能力。
    這些能力,跟 programmer 需要俱備的剛好是另一個角度的要求。某種程度上是各自發展的,不會互相衝突。有心的 programmer 應該要及早作好準備。如果 programmer 是要把程式寫對,那 software engineer 就是要把程式寫好,用專業的方式來寫,而不是用業餘的方式。 什麼叫作 "專業" 的程式? 我舉幾個例子,你的程式防呆嗎? 你的程式面對未知的問題或狀況的免疫力夠不夠強? 面對問題時你的程式有沒有比其它人的程式還容易抓出 BUG ? 你有能力有系統的找出未知的問題嗎? 還是只能看著程式碼發呆? 面對上面的問題有沒有有效的預防措施? 設計階段可以怎樣預防? ... etc 實在太多了。不過這些看起來又是教條,實際上這幾點會影響的到底是什麼? 後面幾篇就一項一項來看吧!   [程式要有足夠的可靠性] 老實說,我很怕光是這一段,就會拖到好幾篇了 ... @_@,我會盡量挑出重點來寫。開始之前先問一下,不知道有多少人看過馬奎爾 (Steve Maguire) 寫的這本書? 有的話記得留個回應 :D "完美程式設計指南" (Writing Solid Code) 這本書真是經典。不過它真的也很 "經典",是 1993 年就出版的書。以講程式設計來說,這個年代的書真的可以扔了,裡面的範例現在沒幾個人用的到了。不過它提到的作法真的是很實際,只是書上的範例大半都過時了,下面碰到的例子我都會用 C# 重新表達一次作者的理念。在這個主題我就舉幾個例子,各位讀者可以自己回顧一下你的程式碼,到底藏了多少地雷在裡面?   [要讓問題浮現出來: 善用 DEBUG / RELEASE 模式] 專不專業就看這裡了。如果你想當個稱職的軟體工程師,除了讓程式跑的快之外,第一點就是要降低 BUG 數。如果你面對 BUG 的態度是 "找到再改就好",或是 BUG 一堆你也沒方法去預防,也沒辦法降低 BUG 出現的頻率,那麼你跟半路出家的人差別在那? 大家都知道 Visual Studio 正上方就有個切換 Release / Debug 模式的選單吧? 你確切瞭解它是幹嘛的嗎? 先從一個簡單的範例開始吧! 我工作上常碰到線上測驗之類的應用軟體開發,因此線上考試算分是個很常用的功能。因此我把這個重責大任交給工程師來處理。先來看看我要求工程師寫什麼 CODE ? 我用 XML 定義了一份考卷 (QUIZ.xml,含正確答案),也定義了答案卷的格式 (PAPER-XXXX.xml),程式很簡單,就是拿到題目跟答案卷後,要算出正確的總分。 不難吧? 先看看 XML 檔長啥樣子:
    試卷 (QUIZ.xml)[copy code]
    <?xml version="1.0" encoding="utf-8" ?>
    <quiz>
      <question score="20">
        <body>那一隻熊最勵害?</body>
        <item correct="false">白熊</item>
        <item correct="false">黑熊</item>
        <item correct="false">棕熊</item>
        <item correct="true">灰熊</item>
      </question>
     
      <question score="40">
        <body>誰發現萬有引力?</body>
        <item correct="false">鼠頓</item>
        <item correct="true">牛頓</item>
        <item correct="false">虎頓</item>
        <item correct="false">兔頓</item>
      </question>
     
      <question score="40">
        <body>下列那些東西是可以吃的?</body>
        <item correct="false">東瓜</item>
        <item correct="true">西瓜</item>
        <item correct="true">南瓜</item>
        <item correct="false">北瓜</item>
      </question>
    </quiz>
        再來代表答案卷的檔案 (PAPER-PERFECT.xml),這份看來是天才寫的,每一題都答對了... @_@
    答案卷 (都是正確答案,PAPER-PERFECT.xml)[copy code]
    <?xml version="1.0" encoding="utf-8" ?>
    <quiz>
      <question>
        <item checked="false" />
        <item checked="false" />
        <item checked="false" />
        <item checked="true" />
      </question>
      <question>
        <item checked="false" />
        <item checked="true" />
        <item checked="false" />
        <item checked="false" />
      </question>
      <question>
        <item checked="false" />
        <item checked="true" />
        <item checked="true" />
        <item checked="false" />
      </question>
    </quiz>
        而我交待的算分規則也很簡單,就一般考試的計算方式: 每題有自己的配分,以複選題來算,答對幾個選項就照比例給分,答錯會倒扣。新人工程師果然好用耐操,不一會就交給我這份 Library 的程式碼:
    第一版計分程式[copy code]
    public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)
    {
        int questionCount = quizDoc.SelectNodes("/quiz/question").Count;
        int totalScore = 0;
        for (int questionPos = 0; questionPos < questionCount; questionPos++)
        {
            XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
            XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
            totalScore += ComputeQuestionScore(quiz_question, paper_question);
        }
        return totalScore;
    }
    public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
    {
        int totalScore = 0;
        int itemCount = quiz_question.SelectNodes("item").Count;
        //
        //  題目的配分
        //
        int quiz_score = int.Parse(quiz_question.GetAttribute("score"));
        //
        //  答對一個選項的分數
        //
        int item_score = quiz_score / itemCount;
        for (int itemPos = 0; itemPos < itemCount; itemPos++)
        {
            XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;
            XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;
            //
            //  算成積
            //
            if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))
            {
                totalScore += item_score;
            }
            else
            {
                totalScore -= item_score;
            }
        }
        return totalScore;
    }
      很中規中舉的程式,把天才寫的答案卷 (paper-perfect.xml) 套進去算,也真的拿到滿分,於是工程師就很高興的把程式 shelve 給我... 各位回頭想想上面的問題。這段程式以作業的標準來說勉強及格了。但是以實際系統運作的角度來說有那些缺陷? 原則上程式只要是人寫的都會有 BUG,不過我也是人,沒辦法一眼看穿所有程式的問題... 只能事事抱著懷疑的態度,試一試再說。我不是天才,所以寫不出滿分的答案,我另外準備了一份答案卷 (PAPER-NORMAL1.xml):
    只答對第一題的答案卷 (PAPER-NORMAL1.xml)[copy code]
    <?xml version="1.0" encoding="utf-8" ?>
    <quiz>
      <question>
        <item checked="false" />
        <item checked="false" />
        <item checked="false" />
        <item checked="true" />
      </question>
      <question>
        <item checked="false" />
        <item checked="false" />
        <item checked="false" />
        <item checked="false" />
      </question>
      <question>
        <item checked="false" />
        <item checked="false" />
        <item checked="false" />
        <item checked="false" />
      </question>
    </quiz>
      見鬼了,算出來是 40 分... 蠢才也是有尊嚴的,不用平白無故送我 20 分吧... @_@,我把 BUG 丟回去給工程師,最後他抓出 BUG 在那裡了,第二題第三題我完全沒作答,應該視為放棄才對,結果程式也照規則給我算分... 運氣好多賺了 20 分.. 工程師又改了一版給我,這次加上了放棄此題的判斷 (第八行):
    修正後的程式 #2: 放棄的話不算分[copy code]
    public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
    {
        int totalScore = 0;
        int itemCount = quiz_question.SelectNodes("item").Count;
        //
        //  如果都沒作答, 此題放棄
        //
        if (paper_question.SelectNodes("item[@checked='true']").Count == 0) return 0;
        //
        //  題目的配分
        //
        int quiz_score = int.Parse(quiz_question.GetAttribute("score"));
     
      有了上一次經驗,直覺告訴我我還得再測一測,搞不好還有其它 BUG ... 這次找了丁丁來考試,丁丁果真是個人才,交了一份全都錯的答案卷給我,前兩題放棄,第三題全選錯 (PAPER-NATIVE.xml):
    丁丁的答案卷: 倒扣 (PAPER-NATIVE.xml)[copy code]
    <?xml version="1.0" encoding="utf-8" ?>
    <quiz>
      <question>
        <item checked="false" />
        <item checked="false" />
        <item checked="false" />
        <item checked="false" />
      </question>
      <question>
        <item checked="false" />
        <item checked="false" />
        <item checked="false" />
        <item checked="false" />
      </question>
      <question>
        <item checked="true" />
        <item checked="false" />
        <item checked="false" />
        <item checked="true" />
      </question>
    </quiz>
      果然有柯南的地方就有密室殺人事件... @_@,又被我抓到一個問題。這次得到的總分是 -40,那有人扣到負的? 工程師又被我叫來唸了一頓,這次改了這版程式給我 (第十一行,最低是0分):
     
    修正後的程式 #3: 倒扣到0分為止[copy code]
    public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)
    {
        int questionCount = quizDoc.SelectNodes("/quiz/question").Count;
        int totalScore = 0;
        for (int questionPos = 0; questionPos < questionCount; questionPos++)
        {
            XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
            XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
            totalScore += ComputeQuestionScore(quiz_question, paper_question);
        }
        return Math.Max(0, totalScore);
    }
        金融業最重視的就是錢了,銀行的程式連一毛錢都不能算錯,而在線上考試的系統也一樣,連一分都不能算錯。只是當你的老闆這樣要求你的時後,你是謹記在心,還是照一般方式寫程式嗎? 還是你有什麼有效的措施可以預防這些問題? 這時才是顯示你專業的地方啊... 套句鄉民的慣用語:
      "閃開! 讓專業的來..."   哈哈,來看看鄉民... 不,專家該怎麼解決這種問題。怕程式錯就加上一堆檢查就好了。上面舉的例子真的只是 BUG 而以,其它還有更多不可預測的問題,像是題目跟答案卷跟本搭不起來,或是沒有答案卷等等鳥問題都有可能發生。那怎辦? 可憐的工程師被我訓了一頓,只好摸摸鼻子加了一堆令人哭笑不得的 check code, 像這樣:
    多了一堆 CHECK 及印出 DEBUG MESSAGE 的程式碼[copy code]
    public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
    {
        int totalScore = 0;
        int itemCount = quiz_question.SelectNodes("item").Count;
        if (quiz_question == null)
        {
            throw new Exception("沒有題目卷");
        }
        if (paper_question == null)
        {
            throw new Exception("沒有答案卷");
        }
        //
        //  如果都沒作答, 此題放棄
        //
        if (paper_question.SelectNodes("item[@checked='true']").Count == 0)
        {
            Console.WriteLine("偵測到沒作答的答案,此題放棄");
            return 0;
        }
        //
        //  確認題目跟答案的選項數目一致
        //
        if (paper_question.SelectNodes("item").Count != quiz_question.SelectNodes("item").Count)
        {
            throw new Exception("此題的選項跟題目定義不符合");
        }
        老實說這範例我也寫不下去了,加這麼多 check 是好事,不過事情都有黑暗面,我覺的不妥的地方有幾個:
    1. 可讀性變差 太多的 check / debug code, 完全把正常流程的 code 淹沒了,一眼看去看不出什麼邏輯...
    2. 效能變差 對我來說,有些問題是輸入造成的 (如沒有給答案卷),有些是鳥程式自己沒寫好 (如前面的例子)。並不是所有的 check 都需要寫在程式裡。
    3. 花在寫 check 程式的時間太多 沒錯,寫個程式五分鐘就搞定,寫 check 要多花廿分鐘...
    即使這樣,我還是贊成要這樣做。只是要做的聰明一點,要消掉上面的疑慮,還要達成一樣的效果。不需要什麼新技術,十幾年前馬奎爾這本 "Write Solid Code" 就講的很清楚了,要同時維護 RELEASE / DEBUG 兩種版本的程式! 在 C 的年代,只靠兩個巨集就解決了,分別是 TRACE 跟 ASSERT。一個就相當於 printf,可以印出 MESSAGE,另一個 ASSERT 則什麼都不做,只要你傳給它當參數是 TRUE 的話。否則就會印出錯誤訊息同時中止程式。而這兩個巨集都有個特點,就是只在 DEBUG MODE 發生作用,如果是在 RELEASE MODE,則一點用都沒有,就像你沒寫這段 CODE 一樣。 細節我就不多說了,這本書講的很清楚,我直接來用。老實說這種應用太經典了,經典到每種程式語言跟開發工具都有支援,連 Microsoft 在 JavaScript 都有實作,甚至跟 debugger 也有整合,不過不曉得有多少人知道? 在 .NET 當然也有 (System.Diagnoistics)。來看看我改版過的 code:
    套用 TRACE / ASSERT 的程式碼[copy code]
    public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)
    {
        Trace.Assert(quizDoc != null);
        Trace.Assert(paperDoc != null);
        Trace.Assert(quizDoc.SelectNodes("/quiz/question").Count == paperDoc.SelectNodes("/quiz/question").Count);
        int questionCount = quizDoc.SelectNodes("/quiz/question").Count;
        int totalScore = 0;
        for (int questionPos = 0; questionPos < questionCount; questionPos++)
        {
            XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
            XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
            totalScore += ComputeQuestionScore(quiz_question, paper_question);
        }
        totalScore = Math.Max(0, totalScore);
        Trace.Assert(totalScore >= 0);
        return totalScore;
    }
    public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
    {
        int totalScore = 0;
        int itemCount = quiz_question.SelectNodes("item").Count;
        //if (quiz_question == null)
        //{
        //    throw new Exception("沒有題目卷");
        //}
        //if (paper_question == null)
        //{
        //    throw new Exception("沒有答案卷");
        //}
        ////
        ////  確認題目跟答案的選項數目一致
        ////
        //if (paper_question.SelectNodes("item").Count != quiz_question.SelectNodes("item").Count)
        //{
        //    throw new Exception("此題的選項跟題目定義不符合");
        //}
        Trace.Assert(quiz_question != null);
        Trace.Assert(paper_question != null);
        Trace.Assert(paper_question.SelectNodes("item").Count == quiz_question.SelectNodes("item").Count);
        //
        //  如果都沒作答, 此題放棄
        //
        if (paper_question.SelectNodes("item[@checked='true']").Count == 0)
        {
            //Console.WriteLine("偵測到沒作答的答案,此題放棄");
            Trace.WriteLine("偵測到沒作答的答案,此題放棄");
            return 0;
        }
        //
        //  題目的配分
        //
        int quiz_score = int.Parse(quiz_question.GetAttribute("score"));
        //
        //  答對一個選項的分數
        //
        int item_score = quiz_score / itemCount;
        for (int itemPos = 0; itemPos < itemCount; itemPos++)
        {
            XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;
            XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;
            //
            //  算成積
            //
            if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))
            {
                totalScore += item_score;
            }
            else
            {
                totalScore -= item_score;
            }
        }
        Trace.Assert(totalScore >= (0 - quiz_score));
        Trace.Assert(totalScore <= quiz_score);
        return totalScore;
    }
      我特地把之前加的亂七八糟的 check code 用註解留下來,各位可以看看用 TRACE / ASSERT 前後的差別有多少。ASSERT是其中的精華。你可以到處都加上 ASSERT ,來說明你對於程式執行到某個地方的 "假設"。舉例來說,你 "假設" 呼叫你 FUNC 的人一定會傳 quizDoc 跟 paperDoc 給你,你又不想為了它寫一堆 IF ....,你就可以簡單的加上這一行 ASSERT( quizDoc != null), 代表只有 quizDoc 不是 NULL 時才是 "正常" 的。
    那真的不正常的話會怎樣? 我特地拿掉倒扣扣到 0 分為止的 check, 用新版的 code 執行看看。 image 在 .NET 裡 ASSERT 觸動後就是這個樣子。那 TRACE 呢? 我們進 DEBUG MODE 來看看: image TRACE Message 直接被收到 Visual Studio 的 Output 視窗內。不過在 .NET 環境下,這兩者的行為已經跟書上講的廿年前作法有很多不同了。這些機制仍然可以開關,不過已經不是靠 DEBUG / RELEASE MODE 來切換,而是在 .NET configuration 裡用設定檔的方式來切換。       ------------------------------------ 果然寫到一半寫不完 @_@,先做個小結。這些技巧都是一般人寫程式不會注意的,然而這些才是你寫的程式品質有沒有比別人好的關鍵,要讓你的程式可靠,做好預防措施是很重要的。你沒有辦法在所有地方都派警衛防守,但是你至少可以張貼警告標示,ASSERT 就是這樣的東西。下一篇會更進一步的以這例子為延申,ASSERT 還有更強大的應用。也許有人看到這裡會想說: "怎麼跟單元測試有點像? 我們直接用 UnitTest 就好了啊" 沒錯,單元測試其實就是從最基本的 Trace / Assert 衍生出來的,一直到這幾年才成為顯學。後續幾篇也會再對這些議題做討論,敬請期待 :D

    2008/10/20 系列文章: 如何學好寫程式 .NET C# Tips 作品集 專欄 技術隨筆 有的沒的