11/1/2008 9:10:19 PM

MSDN Magazine (NOV 2008) 好文推薦: Engineers Who Write

543 | MSDN | 技術隨筆

MSDN Magazine (Nov 2008) 有一篇文章: "Engineers Who Write",覺的寫的還不錯,就把 LINK 貼上來。這篇文章的 abstract 是這段:

http://msdn.microsoft.com/en-us/magazine/dd153757.aspx

 

Writing is good exercise for developers and software engineers. Richard Ward explains how writing can help you focus your thoughts and communicate more precisely.

 

內文就不多說了,大意是說為什麼多寫寫文章對工程師很有幫助,看了覺的很有道理,其實裡面的原因也是我開始寫 BLOG 的原因。我最近比較常寫篇幅長一點的內容,一方面我想的東西都比較古怪,太短的篇幅講不完,另一方面內容多一點比較能訓練組織能力...。

技術出身的人常有個盲點,你想到很棒的點子,但是別人完全搞不懂你在想什麼...。另一種例子是你想做的東西,別人也聽不懂,或是聽懂了但是抓不到精髓,最後只能自己默默的把它完成。多寫寫文章有助於改善這種狀況,算是個技術人很值得投資的第二專長 :D



11/1/2008 1:58:00 AM

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

543

IMG_3467 (Canon PowerShot G9)

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

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



10/20/2008 1:53:00 AM

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

543 | C# | TROUBLE SHOOTING | 小技巧 | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章]

撐了很久,續篇來了。這次要進階一點,直接從 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>
   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <quiz>
   3:    <question score="20">
   4:      <body>那一隻熊最勵害?</body>
   5:      <item correct="false">白熊</item>
   6:      <item correct="false">黑熊</item>
   7:      <item correct="false">棕熊</item>
   8:      <item correct="true">灰熊</item>
   9:    </question>
  10:   
  11:    <question score="40">
  12:      <body>誰發現萬有引力?</body>
  13:      <item correct="false">鼠頓</item>
  14:      <item correct="true">牛頓</item>
  15:      <item correct="false">虎頓</item>
  16:      <item correct="false">兔頓</item>
  17:    </question>
  18:   
  19:    <question score="40">
  20:      <body>下列那些東西是可以吃的?</body>
  21:      <item correct="false">東瓜</item>
  22:      <item correct="true">西瓜</item>
  23:      <item correct="true">南瓜</item>
  24:      <item correct="false">北瓜</item>
  25:    </question>
  26:  </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>
   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <quiz>
   3:    <question>
   4:      <item checked="false" />
   5:      <item checked="false" />
   6:      <item checked="false" />
   7:      <item checked="true" />
   8:    </question>
   9:    <question>
  10:      <item checked="false" />
  11:      <item checked="true" />
  12:      <item checked="false" />
  13:      <item checked="false" />
  14:    </question>
  15:    <question>
  16:      <item checked="false" />
  17:      <item checked="true" />
  18:      <item checked="true" />
  19:      <item checked="false" />
  20:    </question>
  21:  </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;        }
   1:  public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)
   2:  {
   3:      int questionCount = quizDoc.SelectNodes("/quiz/question").Count;
   4:      int totalScore = 0;
   5:      for (int questionPos = 0; questionPos < questionCount; questionPos++)
   6:      {
   7:          XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
   8:          XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
   9:          totalScore += ComputeQuestionScore(quiz_question, paper_question);
  10:      }
  11:      return totalScore;
  12:  }
  13:  public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
  14:  {
  15:      int totalScore = 0;
  16:      int itemCount = quiz_question.SelectNodes("item").Count;
  17:      //
  18:      //  題目的配分
  19:      //
  20:      int quiz_score = int.Parse(quiz_question.GetAttribute("score"));
  21:      //
  22:      //  答對一個選項的分數
  23:      //
  24:      int item_score = quiz_score / itemCount;
  25:      for (int itemPos = 0; itemPos < itemCount; itemPos++)
  26:      {
  27:          XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;
  28:          XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;
  29:          //
  30:          //  算成積
  31:          //
  32:          if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))
  33:          {
  34:              totalScore += item_score;
  35:          }
  36:          else
  37:          {
  38:              totalScore -= item_score;
  39:          }
  40:      }
  41:      return totalScore;
  42:  }

 

很中規中舉的程式,把天才寫的答案卷 (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>
   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <quiz>
   3:    <question>
   4:      <item checked="false" />
   5:      <item checked="false" />
   6:      <item checked="false" />
   7:      <item checked="true" />
   8:    </question>
   9:    <question>
  10:      <item checked="false" />
  11:      <item checked="false" />
  12:      <item checked="false" />
  13:      <item checked="false" />
  14:    </question>
  15:    <question>
  16:      <item checked="false" />
  17:      <item checked="false" />
  18:      <item checked="false" />
  19:      <item checked="false" />
  20:    </question>
  21:  </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"));
   1:  public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
   2:  {
   3:      int totalScore = 0;
   4:      int itemCount = quiz_question.SelectNodes("item").Count;
   5:      //
   6:      //  如果都沒作答, 此題放棄
   7:      //
   8:      if (paper_question.SelectNodes("item[@checked='true']").Count == 0) return 0;
   9:      //
  10:      //  題目的配分
  11:      //
  12:      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>
   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <quiz>
   3:    <question>
   4:      <item checked="false" />
   5:      <item checked="false" />
   6:      <item checked="false" />
   7:      <item checked="false" />
   8:    </question>
   9:    <question>
  10:      <item checked="false" />
  11:      <item checked="false" />
  12:      <item checked="false" />
  13:      <item checked="false" />
  14:    </question>
  15:    <question>
  16:      <item checked="true" />
  17:      <item checked="false" />
  18:      <item checked="false" />
  19:      <item checked="true" />
  20:    </question>
  21:  </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);        }
   1:  public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)
   2:  {
   3:      int questionCount = quizDoc.SelectNodes("/quiz/question").Count;
   4:      int totalScore = 0;
   5:      for (int questionPos = 0; questionPos < questionCount; questionPos++)
   6:      {
   7:          XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
   8:          XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
   9:          totalScore += ComputeQuestionScore(quiz_question, paper_question);
  10:      }
  11:      return Math.Max(0, totalScore);
  12:  }

 

金融業最重視的就是錢了,銀行的程式連一毛錢都不能算錯,而在線上考試的系統也一樣,連一分都不能算錯。只是當你的老闆這樣要求你的時後,你是謹記在心,還是照一般方式寫程式嗎? 還是你有什麼有效的措施可以預防這些問題? 這時才是顯示你專業的地方啊... 套句鄉民的慣用語:

 

"閃開! 讓專業的來..."

 

哈哈,來看看鄉民... 不,專家該怎麼解決這種問題。怕程式錯就加上一堆檢查就好了。上面舉的例子真的只是 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("此題的選項跟題目定義不符合");            }
   1:  public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
   2:  {
   3:      int totalScore = 0;
   4:      int itemCount = quiz_question.SelectNodes("item").Count;
   5:      if (quiz_question == null)
   6:      {
   7:          throw new Exception("沒有題目卷");
   8:      }
   9:      if (paper_question == null)
  10:      {
  11:          throw new Exception("沒有答案卷");
  12:      }
  13:      //
  14:      //  如果都沒作答, 此題放棄
  15:      //
  16:      if (paper_question.SelectNodes("item[@checked='true']").Count == 0)
  17:      {
  18:          Console.WriteLine("偵測到沒作答的答案,此題放棄");
  19:          return 0;
  20:      }
  21:      //
  22:      //  確認題目跟答案的選項數目一致
  23:      //
  24:      if (paper_question.SelectNodes("item").Count != quiz_question.SelectNodes("item").Count)
  25:      {
  26:          throw new Exception("此題的選項跟題目定義不符合");
  27:      }

 

老實說這範例我也寫不下去了,加這麼多 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;        }
   1:  public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)
   2:  {
   3:      Trace.Assert(quizDoc != null);
   4:      Trace.Assert(paperDoc != null);
   5:      Trace.Assert(quizDoc.SelectNodes("/quiz/question").Count == paperDoc.SelectNodes("/quiz/question").Count);
   6:      int questionCount = quizDoc.SelectNodes("/quiz/question").Count;
   7:      int totalScore = 0;
   8:      for (int questionPos = 0; questionPos < questionCount; questionPos++)
   9:      {
  10:          XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
  11:          XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
  12:          totalScore += ComputeQuestionScore(quiz_question, paper_question);
  13:      }
  14:      totalScore = Math.Max(0, totalScore);
  15:      Trace.Assert(totalScore >= 0);
  16:      return totalScore;
  17:  }
  18:  public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
  19:  {
  20:      int totalScore = 0;
  21:      int itemCount = quiz_question.SelectNodes("item").Count;
  22:      //if (quiz_question == null)
  23:      //{
  24:      //    throw new Exception("沒有題目卷");
  25:      //}
  26:      //if (paper_question == null)
  27:      //{
  28:      //    throw new Exception("沒有答案卷");
  29:      //}
  30:      ////
  31:      ////  確認題目跟答案的選項數目一致
  32:      ////
  33:      //if (paper_question.SelectNodes("item").Count != quiz_question.SelectNodes("item").Count)
  34:      //{
  35:      //    throw new Exception("此題的選項跟題目定義不符合");
  36:      //}
  37:      Trace.Assert(quiz_question != null);
  38:      Trace.Assert(paper_question != null);
  39:      Trace.Assert(paper_question.SelectNodes("item").Count == quiz_question.SelectNodes("item").Count);
  40:      //
  41:      //  如果都沒作答, 此題放棄
  42:      //
  43:      if (paper_question.SelectNodes("item[@checked='true']").Count == 0)
  44:      {
  45:          //Console.WriteLine("偵測到沒作答的答案,此題放棄");
  46:          Trace.WriteLine("偵測到沒作答的答案,此題放棄");
  47:          return 0;
  48:      }
  49:      //
  50:      //  題目的配分
  51:      //
  52:      int quiz_score = int.Parse(quiz_question.GetAttribute("score"));
  53:      //
  54:      //  答對一個選項的分數
  55:      //
  56:      int item_score = quiz_score / itemCount;
  57:      for (int itemPos = 0; itemPos < itemCount; itemPos++)
  58:      {
  59:          XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;
  60:          XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;
  61:          //
  62:          //  算成積
  63:          //
  64:          if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))
  65:          {
  66:              totalScore += item_score;
  67:          }
  68:          else
  69:          {
  70:              totalScore -= item_score;
  71:          }
  72:      }
  73:      Trace.Assert(totalScore >= (0 - quiz_score));
  74:      Trace.Assert(totalScore <= quiz_score);
  75:      return totalScore;
  76:  }

 

我特地把之前加的亂七八糟的 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



10/18/2008 11:53:00 AM

生產者 vs 消費者 - BlockQueue 實作

543 | C# | Threading | 作業系統 | 我的作品 | 技術隨筆 | 物件導向 | Microsoft.NET | [精選文章]

過去寫了好幾篇跟執行緒相關的文章,講的都是如何精確控制執行緒的問題。不過實際上有在寫的人就知道,那些只是 "工具",最重要的還是你該怎樣安排你的程式,讓它能有效率的用到執行緒的好處,那才是重點。大部份能有效利用到多執行緒的程式,大都是大量且獨立的小動作,可以很簡單的撒下去給ThreadPool處理,不過當你的程式沒辦法這樣切,就要想點別的辦法了。

 

開始看 code 前先講講簡單的概念。這篇要講的是另一種模式: "生產者 v.s. 消費者"。這是個很典型的供需問題,唸過作業系統 (Operation System) 的人應該都被考過這個課題吧 @_@。簡單的說如果你的程式要處理的動作可以分為 "生產者" (產生資料,載入檔案,或是第一階段的運算等等) 及消費者 (匯出資料,或是第二階段的運算等等) 這種模式,而前後兩個階段各自又適合用執行緒來加速的話,那你就值得來研究一下這種模式。第一手資料就是去看看作業系統的書,恐龍書足足有一整章在講,足夠你研究了。本篇重點會擺在怎樣用 C# / .NET 實作的部份。

 

舉個具體一點的例子,如果你想寫個程式,從網站下載幾百個檔案,同時要把它們壓縮成一個 ZIP 檔,在過去你大概只能全部下載完之後,再開始ZIP的壓縮動作。第一階段都是 IO (網路) bound 的程式,第二階段則是 CPU bound。如果是完全獨立的兩個程式,很適合擺在一起執行,因為它們需要的資源不一樣,不會搶來搶去。但是就敗在他們要處理的資料是卡在一起的。

 

把這個動作想像成我們有兩組人分別負責下載及壓縮的動作,下載的部份可以多執行緒同時進行沒問題,但是下載好一個檔案,就可以先丟給後面的那組人開始壓縮了,不用等期它人下載完成。如果下載的暫存目錄空間有限,我們甚至可以這樣調整: 當 TEMP 滿了的話,下載動作就暫停,等到 TEMP 裡的東西壓縮好清掉一部份後再繼續。而壓縮的部份則相反,如果 TEMP 已經空了就暫停,等到有東西進來再繼續,直到完成為止。

 

image

前後兩階段該如何利用多執行緒,我就跳過去了,過去那幾篇就足以應付。這種模式的關鍵在於前後兩階段的進度該如何平衡。有些範例是有照規矩的把這模式實作出來,不過... 你也知道,看起來就是像作業的那種,完全不像是可以拿來正規的用途。

 

我認定 "好" 的實作,是像 System.Collections.Generics 之於 DataStructure 那樣,能很漂亮的把細節封裝起來,很容易重複利用的才是我認為好的實作。不能容易的使用,那就只能像作業一樣寫完就丟...。這個問題看過有人用 Semaphore 來做,也是作的很棒,不過我比較推薦的是 QUEUE 的作法。

 

從上圖來看,生產者跟消費者都很簡單,就是過去講的多執行緒或是執行緒集區就搞定,關鍵在於中間的控制。我的想法是把庫存管理的東西實作成佇列 (QUEUE),生產者產出的東西就放到 QUEUE,而消費者就去 QUEUE 把東西拿出來。不過現成的 QUEUE 不會告訴你 QUEUE 滿了,QUEUE 空了也只會丟 EXCEPTION 而以。這次我做了個 BlockQueue 就是希望解決這個問題。

 

我期望這個 QUEUE 能跟一般的 QUEUE 一樣使用,但是要有三個地方不一樣:

  1. 要設定大小限制,當 QUEUE 達到容量上限時 EnQueue 的動作會被暫停 (Block),而不是丟出例外。
  2. QUEUE 已經空了的時後,DeQueue 的動作會被暫停 (Block),而不是丟出例外。
  3. 要多個 QUEUE 關機的動作 (SHUTDOWN),以免生產者都不出貨了,消費者還苦苦的等下去的窘況。

 

先看看這樣的 QUEUE 我希望它怎麼被使用。看一下簡單的範例程式 (主程式,不包含 BlockQueue):

使用 BlockQueue 來實作的生產者/消費者範例[copy code]
        public static BlockQueue<string> queue = new BlockQueue<string>(10);        public static void Main(string[] args)        {            List<Thread> ps = new List<Thread>();            List<Thread> cs = new List<Thread>();            for (int index = 0; index < 5; index++)            {                Thread t = new Thread(Producer);                ps.Add(t);                t.Start();            }            for (int index = 0; index < 10; index++)            {                Thread t = new Thread(Consumer);                cs.Add(t);                t.Start();            }            foreach (Thread t in ps)            {                t.Join();            }            WriteLine("Producer shutdown.");            queue.Shutdown();            foreach (Thread t in cs)            {                t.Join();            }        }        public static long sn = 0;        public static void Producer()        {            for (int count = 0; count < 30; count++)            {                RandomWait();                string item = string.Format("item:{0}", Interlocked.Increment(ref sn));                WriteLine("Produce Item: {0}", item);                queue.EnQueue(item);            }            WriteLine("Producer Exit");        }        public static void Consumer()        {            try            {                while (true)                {                    RandomWait();                    string item = queue.DeQueue();                    WriteLine("Cust Item: {0}", item);                }            }            catch            {                WriteLine("Consumer Exit");            }        }        private static void RandomWait()        {            Random rnd = new Random();            Thread.Sleep((int)(rnd.NextDouble() * 300));        }        private static void WriteLine(string patterns, params object[] arguments)        {            Console.WriteLine(string.Format("[#{0:D02}] ", Thread.CurrentThread.ManagedThreadId) + patterns, arguments);        }
   1:  public static BlockQueue<string> queue = new BlockQueue<string>(10);
   2:  public static void Main(string[] args)
   3:  {
   4:      List<Thread> ps = new List<Thread>();
   5:      List<Thread> cs = new List<Thread>();
   6:      for (int index = 0; index < 5; index++)
   7:      {
   8:          Thread t = new Thread(Producer);
   9:          ps.Add(t);
  10:          t.Start();
  11:      }
  12:      for (int index = 0; index < 10; index++)
  13:      {
  14:          Thread t = new Thread(Consumer);
  15:          cs.Add(t);
  16:          t.Start();
  17:      }
  18:      foreach (Thread t in ps)
  19:      {
  20:          t.Join();
  21:      }
  22:      WriteLine("Producer shutdown.");
  23:      queue.Shutdown();
  24:      foreach (Thread t in cs)
  25:      {
  26:          t.Join();
  27:      }
  28:  }
  29:  public static long sn = 0;
  30:  public static void Producer()
  31:  {
  32:      for (int count = 0; count < 30; count++)
  33:      {
  34:          RandomWait();
  35:          string item = string.Format("item:{0}", Interlocked.Increment(ref sn));
  36:          WriteLine("Produce Item: {0}", item);
  37:          queue.EnQueue(item);
  38:      }
  39:      WriteLine("Producer Exit");
  40:  }
  41:  public static void Consumer()
  42:  {
  43:      try
  44:      {
  45:          while (true)
  46:          {
  47:              RandomWait();
  48:              string item = queue.DeQueue();
  49:              WriteLine("Cust Item: {0}", item);
  50:          }
  51:      }
  52:      catch
  53:      {
  54:          WriteLine("Consumer Exit");
  55:      }
  56:  }
  57:  private static void RandomWait()
  58:  {
  59:      Random rnd = new Random();
  60:      Thread.Sleep((int)(rnd.NextDouble() * 300));
  61:  }
  62:  private static void WriteLine(string patterns, params object[] arguments)
  63:  {
  64:      Console.WriteLine(string.Format("[#{0:D02}] ", Thread.CurrentThread.ManagedThreadId) + patterns, arguments);
  65:  }

 

 

 

 

 

主程式很簡單,你知道怎麼寫多執行緒程式的話那麼一看就懂了。一開始替 Producer / Consumer 各建立三個執行續,而每個 Producer 只作很簡單的事,就是連續生產 30 個字串放到 BlockQueue, 當所有的 Producer thread 都執行完後,會呼叫 queue.Shutdown( ); 通知 QUEUE 已經全部生產完畢。

Consumer 也很簡單,每個 Consumer 只是去 Queue 拿東西出來,顯示在 Console 上。直到 Dequeue 動作失敗,接到 Exception 之後就結束。

要試試生產者/消費者模式的各種狀況,可以試著調整兩者的執行緒數量。舉例來說,調大 Producer 執行緒數量時 (P: 10 / C:5),結果是這樣:

image

Producer 的進度大約就是領先 Consumer 的進度 10 筆資料左右,領先的幅度就暫停了,不會無止境的成長下去。證明卡在 QUEUE 內的數量受到控制。接下來再來看看調高 Consumer 的執行緒數量的結果:

image

好像 iPhone 上市搶購熱潮一樣 @_@,供不應求,Producer 提供的資料馬上被搶走了...。

 

效果不錯,看來這樣的實作有達成它的目的。最後來看的是 BlockQueue 的程式碼:

 

BlockQueue<T> 實作的完整程式碼[copy code]
    public class BlockQueue<T>    {        public readonly int SizeLimit = 0;        private Queue<T> _inner_queue = null;        private ManualResetEvent _enqueue_wait = null;        private ManualResetEvent _dequeue_wait = null;        public BlockQueue(int sizeLimit)        {            this.SizeLimit = sizeLimit;            this._inner_queue = new Queue<T>(this.SizeLimit);            this._enqueue_wait = new ManualResetEvent(false);            this._dequeue_wait = new ManualResetEvent(false);        }        public void EnQueue(T item)        {            if (this._IsShutdown == true) throw new InvalidCastException("Queue was shutdown. Enqueue was not allowed.");            while (true)            {                lock (this._inner_queue)                {                    if (this._inner_queue.Count < this.SizeLimit)                    {                        this._inner_queue.Enqueue(item);                        this._enqueue_wait.Reset();                        this._dequeue_wait.Set();                        break;                    }                }                this._enqueue_wait.WaitOne();            }        }        public T DeQueue()        {            while (true)            {                if (this._IsShutdown == true)                {                    lock (this._inner_queue) return this._inner_queue.Dequeue();                }                lock (this._inner_queue)                {                    if (this._inner_queue.Count > 0)                    {                        T item = this._inner_queue.Dequeue();                        this._dequeue_wait.Reset();                        this._enqueue_wait.Set();                        return item;                    }                }                this._dequeue_wait.WaitOne();            }        }        private bool _IsShutdown = false;        public void Shutdown()        {            this._IsShutdown = true;            this._dequeue_wait.Set();        }    }
   1:  public class BlockQueue<T>
   2:  {
   3:      public readonly int SizeLimit = 0;
   4:      private Queue<T> _inner_queue = null;
   5:      private ManualResetEvent _enqueue_wait = null;
   6:      private ManualResetEvent _dequeue_wait = null;
   7:      public BlockQueue(int sizeLimit)
   8:      {
   9:          this.SizeLimit = sizeLimit;
  10:          this._inner_queue = new Queue<T>(this.SizeLimit);
  11:          this._enqueue_wait = new ManualResetEvent(false);
  12:          this._dequeue_wait = new ManualResetEvent(false);
  13:      }
  14:      public void EnQueue(T item)
  15:      {
  16:          if (this._IsShutdown == true) throw new InvalidCastException("Queue was shutdown. Enqueue was not allowed.");
  17:          while (true)
  18:          {
  19:              lock (this._inner_queue)
  20:              {
  21:                  if (this._inner_queue.Count < this.SizeLimit)
  22:                  {
  23:                      this._inner_queue.Enqueue(item);
  24:                      this._enqueue_wait.Reset();
  25:                      this._dequeue_wait.Set();
  26:                      break;
  27:                  }
  28:              }
  29:              this._enqueue_wait.WaitOne();
  30:          }
  31:      }
  32:      public T DeQueue()
  33:      {
  34:          while (true)
  35:          {
  36:              if (this._IsShutdown == true)
  37:              {
  38:                  lock (this._inner_queue) return this._inner_queue.Dequeue();
  39:              }
  40:              lock (this._inner_queue)
  41:              {
  42:                  if (this._inner_queue.Count > 0)
  43:                  {
  44:                      T item = this._inner_queue.Dequeue();
  45:                      this._dequeue_wait.Reset();
  46:                      this._enqueue_wait.Set();
  47:                      return item;
  48:                  }
  49:              }
  50:              this._dequeue_wait.WaitOne();
  51:          }
  52:      }
  53:      private bool _IsShutdown = false;
  54:      public void Shutdown()
  55:      {
  56:          this._IsShutdown = true;
  57:          this._dequeue_wait.Set();
  58:      }
  59:  }

 

重點只在重新包裝 Queue 的 Enqueue / Dequeue ,及追加的 Shutdown( ) 裡做的執行緒同步機制。在 BlockQueue 尚未 Shutdown 之前,Enqueue / Dequeue 都不會引發 Exception, 取代的是用 ManualResetEvent 的 WaitOne( ) 來暫停這個動作,直到另一端資料準備好為止。

然而當 Shutdown 被呼叫過之後,Queue 就不再接受新的東西被塞進來了,而東西拿光因為不再補貨,所以就維持原本 Queue 的設計扔出 Exception。

 

其實真的要挖的話,這個 Queue 可以進一步的改善,以資料結構來看,這種有固定 SIZE 上限的 QUEUE,最適合用 CircleQueue 來實作了。有興趣的朋友們可以換上回介紹過的 NGenerics 改看看,我就不再示範了。其實還有其它變型,像是 Priority Queue, 進去跟出來的順序不一定一樣,意思是你地位比較高的話是可以 "插隊" 的,後加入 QUEUE 的物件,可以優先被拿出來。這些機制都是可以進一步改善 "生產者/消費者" 模式的方法,有需要的讀者們可以朝這個方向思考看看!

 

這篇只是個開始,運用這種機制,可以進一步延伸出 Pipeline 模式 (生產線),甚至更進一步運用到串流 (Stream) 的應用。運氣好的話下個月應該看的到完整的探討跟解說吧 ...,敬請期待 :D



10/14/2008 2:52:00 AM

NGenerics - DataStructure / Algorithm Library

Microsoft.NET | 543 | C# | MSDN | 小技巧 | 技術隨筆

其實本來沒打算寫這篇的,不過之前在寫第二篇: [該如何學好 "寫程式" #2. 為什麼 programmer 該學資料結構 ??] 時,寫的太高興,忘了查 System.Collections.Generics.SortedList 的 KEY 能不能重複... 結果貼出來後同事才提醒我,SortedList 的 KEY 一樣得是唯一的... Orz

 

實在是不想自己再去寫這種資料結構的作業... 一來我寫不會比較好,二來自己寫的沒支援那堆 ICollection, IEnumerable 等 interface 的話,用起來也是很難用... 就到 www.codeplex.com 找了找,沒想到還真的找到一個: NGenerics :D 找到之後才發現挖到寶了,裡面實作的資料結構還真完整,Heap, BinaryTree, CircularQueue, PriorityQueue...  啥的一應俱全,好像看到資料結構課本的解答本一樣 @_@,有興趣研究的人可以抓它的 Source Code 來看..

 

這套 LIB 的實作範圍很廣,除了我前兩篇介紹很基本的那幾個之外,其它連一些數學的跟圖型,甚至是各種排序法的實作都包含在內。要看介紹就到它的官方網站看吧! 很可惜的是它的文件不像 MSDN 一般,有明確的標示時間複雜度... 不過它有附 Source Code, 拼一點的話還是可以自己看程式... 哈哈 :D

 

我就拿 NGenerics 來改寫之前我提供的範例程式吧,那個查通訊錄的程式就不用再改寫了,看不大出來效果差在那。我們來改寫複雜一點的,也就是高速公路的例子。

 

先來看看有什麼東西可以用? NGenerics.DataStructures.General 這個 Namespace 下竟然有現成的 Graph 類別!! 而 NGenerics.Algorithms 下也有現成的 GraphAlgorithm 這演算法的實作... Orz, 裡面提供了三種演算法,光看名字還真搞不懂它是啥... 分別查了一下,是這三個... 找到的都是教授或是考古題之類的網站 ... 咳咳...

  1. Dijkstras Algorithm (代克思托演算法): ... 這種名字難怪我記不住 @_@,這演算法就是我在第三篇提過比較好的演算法,由起點一路擴散出去的作法。
  2. Kruskals Algorithm: 這名字大概太難翻了,沒人把它翻成中文的.. 哈哈,這演算法是找出 minimal spanning tree (最小生成樹),這篇不講教條了,跳過跳過,有興趣自己看 :D
  3. Prims Algorithm (普林演算法): 這名字好記多了... 一樣是找最短路逕 minimal spanning tree 的演算法

來看看原本我寫了上百行的程式 (請參考上一篇),用這個 LIB 改寫有多簡單吧! 先來看看建地圖的部份。Graph<T> 的 T 是指圖的節點型別。暫時不管收費站的問題,因為 GRAPH 的模型裡,只有路逕是有成本的,點本身沒有。直接用 string 來識別點 (vertex),兩個點跟它的距離就當作路段 (edge)。建資料還真有點囉唆,打了不少字:

利用 NGeneric 的 Graph 來建立高速公路的模型[copy code]
            Graph<string> highway = new Graph<string>(false);            highway.AddVertex("基金");            highway.AddVertex("七堵收費站");            highway.AddVertex("汐止系統");            // 以下略            highway.AddEdge(                highway.GetVertex("基金"),                 highway.GetVertex("七堵收費站"),                4.9 - 0);            highway.AddEdge(                highway.GetVertex("七堵收費站"),                 highway.GetVertex("汐止系統"),                 10.9 - 4.9);            // 以下略
   1:  Graph<string> highway = new Graph<string>(false);
   2:  highway.AddVertex("基金");
   3:  highway.AddVertex("七堵收費站");
   4:  highway.AddVertex("汐止系統");
   5:  // 以下略
   6:  highway.AddEdge(
   7:      highway.GetVertex("基金"), 
   8:      highway.GetVertex("七堵收費站"),
   9:      4.9 - 0);
  10:  highway.AddEdge(
  11:      highway.GetVertex("七堵收費站"), 
  12:      highway.GetVertex("汐止系統"), 
  13:      10.9 - 4.9);
  14:  // 以下略

 

都是我一行一行慢慢打的 @_@... 地圖建完後,怎麼找出兩點之間的最短路逕? 只要這段...

找出 [機場端] 到 [基金] 的最短路逕[copy code]
            Graph<string> result = GraphAlgorithms.DijkstrasAlgorithm<string>(                highway,                highway.GetVertex("機場端"));            Console.WriteLine(result.GetVertex("基金").Weight);
   1:  Graph<string> result = GraphAlgorithms.DijkstrasAlgorithm<string>(
   2:      highway,
   3:      highway.GetVertex("機場端"));
   4:  Console.WriteLine(result.GetVertex("基金").Weight);

 

因為每個路段的 weight 我是填上油錢 (一公里兩塊錢),所以印出來的就是兩端要花的油錢。那麼被我們忽略掉的收費站怎麼算? 因為圖型的 MODEL 裡,點是沒有 weight 的,因此我們必需把路段改成有方向的,也就是南下及北上分別算不同的路段 (edge), 同時把過路費加到 weight 裡。

 

這個演算法的實作有個小缺點,它只傳回結果,沒把過程傳回來...,所以我們只能算出要花多少錢,沒有很簡單的方法拿到該怎麼走。不過好在它有附原始碼,需要的人就拿來改一下吧 :D,多傳個 delegate 或是用它定義的 IVisitor 讓它走完所有的點,你就可以取得沿路的資訊了。

 

這篇主要是介紹這個意外發現的LIB,就不深入的挖這些細節了,有興趣的讀者們可以自己試看看,不難的。見識到這類演算法函式庫的威力了嗎? 用起來一點都不難,不過要知道怎麼用還真的要好好研究一下...。整套 NGenerics 都是這類的東西,有興趣的讀者好好研究吧 :D



10/10/2008 3:49:46 AM

終於升速了!

543 | 敗家

都什麼年代了,台北市宣稱光纖覆蓋率要達到八成,結果我家這邊到現在還是沒 FTTB 可以用... 決定把龜了很久的 ADSL 從 2M/256K 升級到 8M/640K ... 上傳速度提升了 2.5 倍,多少應該有快一點吧?

雖然填單變更速率的過程碰到一堆鳥狀況,不過總算升速成功了 :D

特此留念



10/8/2008 3:32:00 AM

該如何學好 "寫程式" #3. 進階應用 - 資料結構 + 問題分析

543 | C# | 小技巧 | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章]

接續前文:

  1. 該如何學好 "寫程式" ??
  2. 該如何學好 "寫程式" #2. 為什麼 programmer 該學資料結構 ??

這類文章還真不好寫,想了好幾天,才擠的出一篇文章。第一篇已經講了一堆教條了,第二篇也舉了簡單的例子,說明挑對資料結構的重要性,接下來這篇會把主題放在更複雜的例子上,到底那些地方該注重技術,而那些地方該把重點放在基礎的資料結構及演算法身上。

這次不囉唆半天了,先來回顧一下第一篇,我出的題目是這樣:

以台灣高速公路為題 (中山高、北二高、國道二號),你有辦法寫程式,讓使用者指定起點跟終點的交流道,然後替它找出建議的路線嗎? (把延路經過的交流到跟收費站列出來就好)。

舉這個題目,是怕前面的例子被嫌太簡單,一點都不能符合實際的情況。沒錯,絕大部份的情況都不會像上一篇的範例一樣,放一堆資料在記憶體裡 SEARCH 出來就了事那麼簡單。高速公路的問題核心一樣是在資料結構,不過這次多了必需自己實作的演算法。

跟我一樣五六年級的人,都聽過這句話吧,PASCAL 之父 (Niklaus Wirth) 講的這句名言: "程式 = 資料結構+演算法",沒錯,這個範例就需要用到這兩種能力才搞的定。依我的看法,解決這問題有三道關卡要闖:

  1. 你該用什麼樣的方式來儲存這樣的地圖資訊?
    這裡會用到的知識,是資料結構裡的 GRAPH,典型的方法就是分成點跟線來記錄..
  2. 你該用什麼樣的演算法,找出你要的最佳路線?
    最基本的是要找出所有可走的路線 (走迷宮),再找出其中最便宜的一條路。
  3. 你的程式的結構該如何設計?
    這部份跟課本比較無關,講的是你對程式語言及可用的函式庫/工具的掌握,還有架構等等。

這三道關卡,要依序破解,前一關的決定會影響到後面的解決方式。先從資料來說,連該怎麼記錄這些資料,就別想解題了。最基本的 GRAPH 需要點 (NODE) 及點跟點之間的連線 (LINK) 組成。很直覺的就可以定出這兩個類別:

描述交流道/收費站的 class[copy code]
    public class Node    {        public string Name = null;        public int TollFee = 0;        public List<Link> Links = new List<Link>();        public Node(string name, int tollFee)        {            this.Name = name;            this.TollFee = tollFee;        }    }
   1:  public class Node
   2:  {
   3:      public string Name = null;
   4:      public int TollFee = 0;
   5:      public List<Link> Links = new List<Link>();
   6:      public Node(string name, int tollFee)
   7:      {
   8:          this.Name = name;
   9:          this.TollFee = tollFee;
  10:      }
  11:  }

 

描述兩個點之間的路段 (Link) 的 class[copy code]
    public class Link    {        public double Distance = 0D;        public Node FromNode = null;        public Node ToNode = null;        public RoadNameEnum Road;        public Link(Node from, Node to, double distance, RoadNameEnum road)        {            this.FromNode = from;            this.ToNode = to;            this.Distance = distance;            this.Road = road;        }        public enum RoadNameEnum        {            Highway1,            Highway2,            Highway3        }    }
   1:  public class Link
   2:  {
   3:      public double Distance = 0D;
   4:      public Node FromNode = null;
   5:      public Node ToNode = null;
   6:      public RoadNameEnum Road;
   7:      public Link(Node from, Node to, double distance, RoadNameEnum road)
   8:      {
   9:          this.FromNode = from;
  10:          this.ToNode = to;
  11:          this.Distance = distance;
  12:          this.Road = road;
  13:      }
  14:      public enum RoadNameEnum
  15:      {
  16:          Highway1,
  17:          Highway2,
  18:          Highway3
  19:      }
  20:  }

好像沒什麼特別的,每個點除了搭配 List<Link> 來記錄所有經過它的路段 (Node.Links) 之外,也標上了這個點的名字 (Node.Name),跟過路費 (Node.TollFee)。而每個路段則記錄了它兩個端點 (Link.FromNode, Link.ToNode) 之外,也額外記錄了路段的距離 (Node.Distance),及它是屬於那一條高速公路的資訊 (Link.Road)。

接下來就要載入資料了。我偷個懶,只記中山高跟北二高新竹以北的部份,還有機場國道。實在是沒力氣把全部的路段打完... 哈哈。資料來源是參考國道高速公路局的網頁。以下是 Map 類別的部份程式碼,及載入部份地圖資訊的程式碼:

MAP[copy code]
    public class Map    {        private Dictionary<string, Node> _nodes = new Dictionary<string, Node>();        public Map()        {            //            //  construct / load map data            //            this.AddNode("基金", 0);            this.AddNode("七堵收費站", 40);            this.AddNode("汐止系統", 0);            this.AddNode("樹林收費站", 40);            // 略            this.AddLink("基金", "七堵收費站", 4.9-0, Link.RoadNameEnum.Highway3);            this.AddLink("七堵收費站", "汐止系統", 10.9-4.9, Link.RoadNameEnum.Highway3);            // 略        }        private void AddNode(string name, int tollFee)        {            Node n = new Node(name, tollFee);            this._nodes.Add(name, n);        }        private void AddLink(string n1, string n2, double distance, Link.RoadNameEnum road)        {            Node node1 = this._nodes[n1];            Node node2 = this._nodes[n2];            Link link = new Link(this._nodes[n1], this._nodes[n2], distance, road);            node1.Links.Add(link);            node2.Links.Add(link);        }        public Link FindLink(string name1, string name2)        {            foreach (Link way in this._nodes[name1].Links)            {                if (way.GetOtherNodeName(name1) == name2) return way;            }            return null;        }    }
   1:  public class Map
   2:  {
   3:      private Dictionary<string, Node> _nodes = new Dictionary<string, Node>();
   4:      public Map()
   5:      {
   6:          //
   7:          //  construct / load map data
   8:          //
   9:          this.AddNode("基金", 0);
  10:          this.AddNode("七堵收費站", 40);
  11:          this.AddNode("汐止系統", 0);
  12:          this.AddNode("樹林收費站", 40);
  13:          // 略
  14:          this.AddLink("基金", "七堵收費站", 4.9-0, Link.RoadNameEnum.Highway3);
  15:          this.AddLink("七堵收費站", "汐止系統", 10.9-4.9, Link.RoadNameEnum.Highway3);
  16:          // 略
  17:      }
  18:      private void AddNode(string name, int tollFee)
  19:      {
  20:          Node n = new Node(name, tollFee);
  21:          this._nodes.Add(name, n);
  22:      }
  23:      private void AddLink(string n1, string n2, double distance, Link.RoadNameEnum road)
  24:      {
  25:          Node node1 = this._nodes[n1];
  26:          Node node2 = this._nodes[n2];
  27:          Link link = new Link(this._nodes[n1], this._nodes[n2], distance, road);
  28:          node1.Links.Add(link);
  29:          node2.Links.Add(link);
  30:      }
  31:      public Link FindLink(string name1, string name2)
  32:      {
  33:          foreach (Link way in this._nodes[name1].Links)
  34:          {
  35:              if (way.GetOtherNodeName(name1) == name2) return way;
  36:          }
  37:          return null;
  38:      }
  39:  }

 

第一步準備動作完成了。接下來就是想辦法在 class Map 裡加上 FindBestWay( ) method, 來找出最佳路線。在這邊先定義一下什麼叫最佳路線。一般不外乎是找最短的路線,或是通過最少的收費站,我們來點實際的,以油價每公升 30 元為例,車子就假設一公升可以跑 15 公里好了,因此每跑一公里要花兩塊錢。最佳路逕就是花的油錢跟過路費最少的為準。

沒唸過資料結構的朋友們,現在大概卡住了。該怎樣找出最佳的路逕? 電腦什麼不行,就是計算很快,這種最佳解的問題,通常都可以用暴力法解決。只要把所有的路線找出來,然後找出總花費最便宜的那個路線就好。雖然資料結構的書通常會舉其它更有效率的演算法,其中一個演算法的名字我不記得了,大致的步驟是由起點開始往外擴散,先算走一步可以走到那些點,再往外推,如果到同一點有兩條以上的路線,就保留便宜的那個... 直到推到終點為止。

不過這方法寫起來比較麻煩,我挑另一個簡單一點的 (相對的比較沒效率),就是搭配 STACK 走迷宮的方法,把所有路線試過一次,找出所有能從起點到終點的路線,再從其中挑出最經濟的。

為什麼我會挑這個? 只是因為它的邏輯比較簡單易懂,畢竟這個程式不是在比賽,要去拼最快的話就不用了.. 哈哈。資料結構在講到 TREE 一定會講到怎麼樣把 TREE 的每個節點都走一次的方法,就是要搭配 STACK,把走過的點都 PUSH 進去,當作麵包屑來用,等走到沒路了就 POP 退回上一步,改走第二個分岔,直到所有的點都走完為止。

接下來就要把 GRAPH 切掉幾條線,把它想像成長的不大整齊的 TREE,就從起點開始走下去。因為 GRAPH 不像 TREE,有可能會走回原點,因此我們在走的過程中需要跳過已經走過的點,免的最後都在兜圈子繞不出去。

這邊我搭配了遞迴 (RECURSIVE) 的方式來簡化問題。其實就邏輯來說,遞迴幾乎可以跟 STACK 劃上等號。因為遞迴的過程中也是有 STACK 在輔助 (就是 CALL STACK)。不過我偏愛 RECURSIVE,因為藉著 CALL STACK 加上 FUNCTION CALL 傳遞的參數,可以自動幫我處理不少 push / pop, 及替每個階段保存暫時的資料,程式看起來會簡單很多。

找出最經濟路線的程式碼[copy code]
        private double _cost = 0;        private string[] _best_path = null;        private Stack<string> _path = null;        private void Search(string startName, string endName, double current_cost)        {            this._path.Push(startName);            if (startName == endName)            {                if (this._cost == 0 || current_cost < this._cost)                {                    this._cost = current_cost;                    this._best_path = this._path.ToArray();                }                this._path.Pop();                return;            }            foreach (Link way in this._nodes[startName].Links)            {                string next = way.GetOtherNodeName(startName);                if (this._path.Contains(next) == false)                {                    this.Search(                        next,                        endName,                        current_cost + this._nodes[next].TollFee + way.Distance * 3);                }            }            this._path.Pop();        }        public string[] FindBestPath(string startName, string endName, out double cost)        {            try            {                this._cost = 0;                this._path = new Stack<string>();                this.Search(startName, endName, 0);                cost = this._cost;                return this._best_path;            }            finally            {                this._cost = 0;                this._path = null;            }        }
   1:  private double _cost = 0;
   2:  private string[] _best_path = null;
   3:  private Stack<string> _path = null;
   4:  private void Search(string startName, string endName, double current_cost)
   5:  {
   6:      this._path.Push(startName);
   7:      if (startName == endName)
   8:      {
   9:          if (this._cost == 0 || current_cost < this._cost)
  10:          {
  11:              this._cost = current_cost;
  12:              this._best_path = this._path.ToArray();
  13:          }
  14:          this._path.Pop();
  15:          return;
  16:      }
  17:      foreach (Link way in this._nodes[startName].Links)
  18:      {
  19:          string next = way.GetOtherNodeName(startName);
  20:          if (this._path.Contains(next) == false)
  21:          {
  22:              this.Search(
  23:                  next,
  24:                  endName,
  25:                  current_cost + this._nodes[next].TollFee + way.Distance * 3);
  26:          }
  27:      }
  28:      this._path.Pop();
  29:  }
  30:  public string[] FindBestPath(string startName, string endName, out double cost)
  31:  {
  32:      try
  33:      {
  34:          this._cost = 0;
  35:          this._path = new Stack<string>();
  36:          this.Search(startName, endName, 0);
  37:          cost = this._cost;
  38:          return this._best_path;
  39:      }
  40:      finally
  41:      {
  42:          this._cost = 0;
  43:          this._path = null;
  44:      }
  45:  }

 

先來看看結果。主程式是要找出 "機場端" 跟 "基金" 交流道之間的最經濟路線,看看程式跑出來的結果:

image

不相信的人就拿紙筆畫一畫算一算吧! 應該是沒算錯啦。這個例子我就不像上一個例子,放上千萬個節點來拼拼看速度到底多快了,因為我沒有現成的資料啊,這東西要產生假資料也麻煩的多,就略過這個步驟了。不過我們倒是可以回過頭來看看,目前這段程式有什麼可以改進的?

首先,在資料數量遽增的情況下,演算法的改善一定是第一要務。你會發現程式碼從五行變成三行,或是從 100ms 進步到 90ms, 這種程度的改善相較之下都是微不足道的,一來這種改善程度通常是固定的,因為演算法沒有變,整體來說可能只是從 100sec 進步到 90sec,我是客戶的話,還不如換顆快一點的 CPU 就好了...。但是演算法的改進,則是讓你迴圈的次數變少,或是比較的次數變少等等,改變幅度通常是以倍數來算,隨便就提升好幾倍的效能。這就不是升級 CPU 可以解決的問題...。還記得上個例子嗎? 從 List 換成 SortedList, 搜尋速度差了 6000 倍... 你要花多少錢才買的到運算速度快 6000 倍的電腦?

除了演算法之外,程式也是有其它地方可以改善的。看到第 20 行程式碼了嗎? 就是找出下一步是不是已經走過了的程式碼:

if (this._path.Contains(next) == false)

其中 _path 是 Stack<string> 物件,養成好習慣,順手查查它的時間複雜度吧,在 MSDN 裡是這麼寫的:

http://msdn.microsoft.com/en-us/library/xeaek790.aspx

This method performs a linear search; therefore, this method is an O(n) operation, where n is Count.

看起來它的效果跟 List 一樣,搜尋都很慢,有幾筆就要比對幾次。還記得上一篇提過什麼方法? 如果排序過的資料,要花的時間是 O(log n), 如果是採用 HashTable 結構的,則只要 O(1)... 再把 MSDN 拿出來翻翻看,發現除了 Dictionary<TKey, TValue> 之外,還有另一個更適合的 HashSet (.NET 3.5 only):

http://msdn.microsoft.com/en-us/library/bb359438.aspx

The HashSet<(Of <(T>)>) class provides high performance set operations. A set is a collection that contains no duplicate elements, and whose elements are in no particular order.

The capacity of a HashSet<(Of <(T>)>) object is the number of elements that the object can hold. A HashSet<(Of <(T>)>) object's capacity automatically increases as elements are added to the object.

馬上看一下,新增一筆及找出某一筆需要的時間複雜度:

 

HashSet<T>.Add( T ):
If Count is less than the capacity of the internal array, this method is an O(1) operation. If the HashSet<(Of <(T>)>) object must be resized, this method becomes an O(n) operation, where n is Count.

 

HashSet<T>.Contains( T ):
This method is an O(1) operation.

 

看起來沒什麼好挑的了。把資料塞進去跟找出來的時間都是固定的,當地圖的節點夠多,你要找的目標夠遠,多花一倍的空間另外放一份 HashSet 絕對是值得的。也因為 HashSet 有這樣的特性,因此它特別適合拿來作集合的運算。比如兩堆資料要找出交集 (Intersection),聯集 (Union) 等等都很方便。既然都講了就順手查看看:

HashSet<T>.IntersectWith(Hash<T>):
If the collection represented by the other parameter is a HashSet<(Of <(T>)>) collection with the same equality comparer as the current HashSet<(Of <(T>)>) object, this method is an O(n) operation. Otherwise, this method is an O(n + m) operation, where n is Count and m is the number of elements in other.

 

 

 

 

---------------------------

本系列文章 [該如何學好 "寫程式"] 第一部份就先到這裡。在這裡作個小結。既然第一部份我是在探討要成為一個好的 "programmer" 該具備的能力,我自然是把重點放在怎樣把你拿到的需求,忠實且正確的寫成 code 為主。這時邏輯及觀念,還有資料結構等等基礎的知識是我認為的重點。也許有些讀者很不以為然,我猜想的大概會有這幾個理由:

  • 我不會這些,程式還不一樣寫的好好的?
  • 都什麼年代了,現在講求的是程式架構!
  • 物件導向不是都講求封裝? 幹嘛還要去挖這些?
  • 現在資料都放資料庫了,還學這幹嘛?
  • ...

其實這些論點也沒錯,不過上一篇可以看到,不懂得這些基礎的話,現成的物件給你挑也不見得知道要挑那一個,更慘的是連之間的差別都不曉得。還有,資料結構通常包含兩個層面,一個是怎麼 "描述" 你的資料? 另一個是怎麼去應用你的資料? 以這題為例,如果你都不曉得要把地圖拆成點跟線來記錄,你會知道 TABLE 該怎麼建嗎?

另外,很多資料庫上面效能的問題,也都跟資料結構有關。就跟上一篇該挑那一種 Collection 一樣,資料庫也可以把它當成一個更巨大,功能更多的 Collection 來看待,因此能不能有效的利用它,資料結構也是很重要的觀念之一。

再來講到架構的部份,我覺的這位網友在他的 blog (我不認得他本人,只是常看他文章) 發表的這兩篇文章很不錯:

1. 程式設計的兩個觀點 (1/2)
2. 程式設計的兩個觀點 (2/2)

他這兩篇講的就是兩個極端,一個講求效率跟演算法,另一個則是講求架構跟程式的美感。而這兩者通常不容易兼顧。以我來說我比較偏後者,效能的部份,我會捨棄一些小地方以換來程式碼的可擴充性,可讀性,架構等等。不過我不會放棄的是資料結構跟演算法的正確性,就如同前面寫的例子一樣,程式碼有沒有最佳化,差的是 xx % 的效能,但是演算法跟資料結構的差距,則是好幾倍。我一向認為不會跑就要學飛,遲早會跌下來的,所以才會寫這三篇針對 programmer 的文章。

接下來,就換到 software engineer 了。這個階段就不只是把程式碼 "寫對" 或是 "寫出來" 而以,而是要開始考慮怎樣才 "寫的好" 了。有興趣的讀者們請耐心等待續集 :D

 

--
範例程式下載: Taiwan-Highway.zip



10/1/2008 10:43:22 PM

得獎了 :D~~~

Microsoft.NET | 543 | C# | Threading | 小技巧 | 安德魯的當年勇 | 我的作品 | 技術隨筆 | 物件導向

 IMG_9142

 

雖然上禮拜就知道了,不過獎品還沒拿到,當然要忍一下再發表... 哈哈!

花了幾個晚上拼了猜數字的程式,運氣不錯,順利拼到冠軍了。除了寫程式,把心得貼到 BLOG 也花了不少時間.. 主要貼的這四篇:

  1. Thread Sync #1. 概念篇 - 如何化被動為主動?
  2. Thread Sync #2. 實作篇 - 互相等待的兩個執行緒
  3. [C#: yield return] #1. How It Work ?
  4. [C# yield return] #2. 另類的應用 - Thread Sync 替代方案

 

蠻有意思的比賽。雖然過去也參加過不少比賽,運氣不錯也騙到一些獎品...,不過這次倒是寫的最起勁,因為其它比賽都是 "廠商" 讚助,不是 Microsoft 就是 Cisco ... 都要想儘辦法把他們的技術發揮出來才能得獎,老實說寫起來跟工作差不多,總是要滿足那些 "市場" 的需求。

這次題目老實說很 "不實用",純粹是比 code 誰寫的又快又好而已,不過還蠻合我胃口的 :D。正好這次碰到誰呼叫誰這種結構上的問題,就是上面四篇文章一直在討論的 GameHost 為主還是 Player 為主的思考方式,解決這問題花的工夫還比較多。想到這兩套解決方式,我覺的收穫是蠻值得的,至少我多學到兩種不同的設計模式。

最後當然要感謝一下主辦人,下班專程騎車過來頒獎... 哈哈,獎品對我還蠻實用的,算是大獎一枚! 正好是我需要的東西,看來可以開始物色新硬碟,還有要準備來更新我的 SERVER 了 :D



10/1/2008 4:09:00 AM

該如何學好 "寫程式" #2. 為什麼 programmer 該學資料結構 ??

543 | C# | MSDN | 小技巧 | 安德魯的當年勇 | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章]

自從貼了上一篇 [該如何學好 "寫程式"] 一文,原本以為這種老生常談的內容沒什麼人會看,沒想到還有人給我回應.. :D 原來這種文章還是有市場的。接下來這篇,是延續上一篇,來談談要成為合格的 programmer, 我認為應該要俱備的 "內功" 是什麼。上篇我提到,我認知的 programmer,就是要有實作 (CODING) 的能力。要有能力把技術規格 (像是輸入格式,操作介面等等) 具體的寫成可以執行的程式碼。當然是要寫的又快又好,穩定不當機又沒 BUG ...。

 

在這個階段 (programmer),會一些具體的工具或是技術是必要的,但是它絕對不是主角。如何去運用你的工具才是關鍵。我認為 "資料結構" 就是能正確運用你的工具 (程式語言及函式庫) 最重要的知識。我常看到很多會一堆 "先進" 的技術,卻寫出很可笑的 code ... 。這種例子太多了,兩層迴圈擺錯順序,或是某些動作 *不小心* 擺到迴圈內,多花了好幾倍的時間在做冤枉事...。這種例子我通通把它規在基本功夫不好,或是常聽的邏輯觀念不佳。所以在上一篇我會提到,好的 programmer 至少能滿足我講的三個基本要求:

  1. 丟一付洗過的撲克牌給你 (不要多,黑桃1 ~ 13就好),你知道怎麼用 Bubble Sort / Quick Sort 的步驟把它排好嗎? 丟一個陣列,裡面隨便打幾個數字,你能寫程式把它由小到大排好印出來嗎?
  2. 假設記憶體夠大的話,你有辦法把一百萬筆通訊錄資料讀到記憶體內 (用什麼物件都隨你),然後還能用很快的速度找到你要的資料嗎? 不同的搜尋方式,你知道該用什麼樣的方式找才有效率嗎?
  3. 以台灣高速公路為題 (中山高、北二高、國道二號),你有辦法寫程式,讓使用者指定起點跟終點的交流道,然後替它找出建議的路線嗎? (把延路經過的交流到跟收費站列出來就好)

第一個只要你知道排序的方法,剩下的就是你有沒有本事把腦袋的想法寫成 CODE 而以。這個要求大部份的人都能過關,我就不多作解釋了。來看看第二個要求,它考驗的是你該用什麼樣的方式 "SEARCH" ?

我就以 C# 為例來說明這個問題該怎樣思考。以資訊系的 "資料結構" 這門課的角度來思考,你應該要找出個適合的資料結構 (Binary Tree, Heap, Linked List ... etc) 來存放這堆資料。不過資料結構這麼多種,你都要自己做嗎? .NET framework 已經在 System.Collection.Generic 這命名空間內提供了一堆好用的 Collection 給你用了,你該怎麼挑選才好? 課堂上老師不會教你實作的東西,而公司的前輩也不會教你這種基礎的東西,那你該怎麼把這兩者應用在一起?

就先從 (2) 的例子開始吧! 通訊錄最基本的要求,就是儲存的資料要能按照姓名/EMAIL/電話號碼排序。輸入名字後,要能很快的找到這個人完整的通訊錄。如果能像手機一樣,邊輸入名字就邊過濾名單,直到名字打完人就找到的話更好。在宣告了 class ContactData { ... } 類別來處理一筆資料後,下一步你會怎麼做?

 

ContactData 類別定義[copy code]
        public class ContactData        {            public string Name;            public string EmailAddress;            public string PhoneNumber;            public void OutputData(TextWriter writer)            {                writer.WriteLine("Name:\t{0}", this.Name);                writer.WriteLine("Email:\t{0}", this.EmailAddress);                writer.WriteLine("Phone:\t{0}", this.PhoneNumber);                writer.WriteLine();            }        }
   1:  public class ContactData
   2:  {
   3:      public string Name;
   4:      public string EmailAddress;
   5:      public string PhoneNumber;
   6:      public void OutputData(TextWriter writer)
   7:      {
   8:          writer.WriteLine("Name:\t{0}", this.Name);
   9:          writer.WriteLine("Email:\t{0}", this.EmailAddress);
  10:          writer.WriteLine("Phone:\t{0}", this.PhoneNumber);
  11:          writer.WriteLine();
  12:      }
  13:  }

 

開始來看看,有基本功夫的 programmer 跟一般 "熟 C# 熟 .NET" 的 programmer 差在那裡吧! 程式很簡單,先產生一百萬筆假資料,然後去找 A123456 這個人的資料,接著再找出手機號碼為 0928-1234 開頭的所有人資料。事後會分別計算花掉的時間跟程式佔用的記憶體大小。

 

1. 大概有 70% 的人,會選擇用 List<ContactData>,不為什麼,只因為他沒想到別的方法,或是直覺就覺的要這樣寫... 來看看這樣的 code:

用 List<ContactData> 寫的範例程式[copy code]
        private static void Sample1()        {            Stopwatch timer = new Stopwatch();            timer.Reset();            timer.Start();            // 產生假資料庫            List<ContactData> contacts = new List<ContactData>();            {                for (int index = 999999; index >= 0; index--)                {                    ContactData cd = new ContactData();                    cd.Name = string.Format("A{0:D6}", index);                    cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);                    cd.PhoneNumber = string.Format("0928-{0:D6}", index);                    contacts.Add(cd);                }            }            Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            timer.Reset();            timer.Start();            {                // 搜尋 A123456 這個人的資料                ContactData data = null;                data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });                Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);                //data.OutputData(Console.Out);            }            timer.Reset();            timer.Start();            {                // 列出電話號碼為 0928-1234* 開頭的使用者                foreach (ContactData match in contacts.FindAll(delegate(ContactData x) { return x.PhoneNumber.StartsWith("0928-1234"); }))                {                    //match.OutputData(Console.Out);                }                Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            }            Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);        }
   1:  private static void Sample1()
   2:  {
   3:      Stopwatch timer = new Stopwatch();
   4:      timer.Reset();
   5:      timer.Start();
   6:      // 產生假資料庫
   7:      List<ContactData> contacts = new List<ContactData>();
   8:      {
   9:          for (int index = 999999; index >= 0; index--)
  10:          {
  11:              ContactData cd = new ContactData();
  12:              cd.Name = string.Format("A{0:D6}", index);
  13:              cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);
  14:              cd.PhoneNumber = string.Format("0928-{0:D6}", index);
  15:              contacts.Add(cd);
  16:          }
  17:      }
  18:      Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  19:      timer.Reset();
  20:      timer.Start();
  21:      {
  22:          // 搜尋 A123456 這個人的資料
  23:          ContactData data = null;
  24:          data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });
  25:          Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  26:          //data.OutputData(Console.Out);
  27:      }
  28:      timer.Reset();
  29:      timer.Start();
  30:      {
  31:          // 列出電話號碼為 0928-1234* 開頭的使用者
  32:          foreach (ContactData match in contacts.FindAll(delegate(ContactData x) { return x.PhoneNumber.StartsWith("0928-1234"); }))
  33:          {
  34:              //match.OutputData(Console.Out);
  35:          }
  36:          Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  37:      }
  38:      Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);
  39:  }

 

憑良心說,寫的出這樣程式碼的人,已經算是高手了。因為這樣已經用到不少高級技巧,像是 delegate, anonums method, 還有知道 List<T>.Find( ) 怎麼用等等... 以下是他的執行結果:

image

 

 

2. 更進階一點的人 (另外 25%),也許會額外加上 Dictionary 當作索引,來改善 search A123456 這筆資料的效率...

加上 Dictionary 當作索引的 code[copy code]
            // 略            // 產生假資料庫            Dictionary<string, ContactData> name_index = new Dictionary<string, ContactData>();            List<ContactData> contacts = new List<ContactData>();            {                for (int index = 999999; index >= 0; index--)                {                    ContactData cd = new ContactData();                    cd.Name = string.Format("A{0:D6}", index);                    cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);                    cd.PhoneNumber = string.Format("0928-{0:D6}", index);                    name_index.Add(cd.Name, cd);                    contacts.Add(cd);                }            }            Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            timer.Reset();            timer.Start();            {                // 搜尋 A123456 這個人的資料                ContactData data = name_index["A123456"];                //data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });                Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);                //data.OutputData(Console.Out);            }           // 略
   1:   // 略
   2:   // 產生假資料庫
   3:   Dictionary<string, ContactData> name_index = new Dictionary<string, ContactData>();
   4:   List<ContactData> contacts = new List<ContactData>();
   5:   {
   6:       for (int index = 999999; index >= 0; index--)
   7:       {
   8:           ContactData cd = new ContactData();
   9:           cd.Name = string.Format("A{0:D6}", index);
  10:           cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);
  11:           cd.PhoneNumber = string.Format("0928-{0:D6}", index);
  12:           name_index.Add(cd.Name, cd);
  13:           contacts.Add(cd);
  14:       }
  15:   }
  16:   Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  17:   timer.Reset();
  18:   timer.Start();
  19:   {
  20:       // 搜尋 A123456 這個人的資料
  21:       ContactData data = name_index["A123456"];
  22:       //data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });
  23:       Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  24:       //data.OutputData(Console.Out);
  25:   }
  26:  // 略

 

會寫到這樣,也算強者了。不但對 C# 夠熟,也有用 Collection 物件來當作索引的觀念。程式碼只有兩行不同,一個是多宣告了 Dictionary 物件 (第3行),另一個是搜尋的地方 (第21行)。來看看執行結果:

image

 

果然有效,建資料從 5151ms 變成 5843ms, 記憶體從 288MB 變成 340MB,不過 search(A123456) 卻快的嚇人, 0ms... 破錶了!

 

但是這樣的 CODE 老實說只能算是及格而以,因為它沒有挑對 Collection 來用。怎麼說? 我的理由有這幾個:

  1. List<T> 的搜尋效能不好
  2. 沒能滿足用多種排序方式的要求 (需要時要當場執行 List<T>.Sort( ))

如果這是某個 Mail Client 內的 CODE,產品經理一定會問:

"如果資料從一百萬筆,變成一億筆,程式的表現會是什麼情況?"

 

有沒有基本功夫,這裡開始有差別了。唸過資料結構的都知道有個叫 "時間複雜度" (time complexity) 的東西,用 O(n) 表示。O(n) 代表花費的時間會跟資料比數成線性的成長。100倍的資料大概就要花上100倍的時間.. 如果是 O(n^2) 的演算法,則 100 倍的資料就會花上 10000 倍的時間。

MSDN 專業的地方就在這裡。Microsoft 真的很細心的在每一個 Collection 物件的說明文件上,都會標上 time complexity。有唸書有保佑,瞄到那行字我的問題就都解決掉了。先來看看 List<T> 的行為:

 

List<T>.Add(T item)

If Count is less than Capacity, this method is an O(1) operation. If the capacity needs to be increased to accommodate the new element, this method becomes an O(n) operation, where n is Count.

 

List<T>.FindAll(Predicate<T> match)

This method performs a linear search; therefore, this method is an O(n) operation, where n is Count.

 

再來看看 Dictionary<TKey, TValue> 的行為:

Dictionary<TKey, TValue>.Add(TKey key, TValue value)

If Count is less than the capacity, this method approaches an O(1) operation. If the capacity must be increased to accommodate the new element, this method becomes an O(n) operation, where n is Count.

 

Dictionary<TKey, TValue>.Item[TKey key] {get; set;}

Getting or setting the value of this property approaches an O(1) operation.

 

好,答案出來了。當資料變成一百倍時,List.Add 是 O(1), 所以每加一筆資料的時間不會越來越久 (safe). 但是搜尋時間是 O(n), 意思是現在找 A123456 要花 60ms, 未來有一億筆就要花 60x100=6000ms=6sec, 找 0928-1234* 則要花 240x100=24000ms=24sec... 以這樣的成長速度,記憶體還沒用完,你的程式就會慢到受不了了。有沒有其它的解決辦法?

 

換成 Dictionary 就酷多了,搜尋時間是 O(1), 代表不管你有幾筆,搜尋的時間都差不多。為什麼? MSDN 說的很清楚...

http://msdn.microsoft.com/en-us/library/xfhwa508.aspx

The Dictionary<(Of <(TKey, TValue>)>) generic class provides a mapping from a set of keys to a set of values. Each addition to the dictionary consists of a value and its associated key. Retrieving a value by using its key is very fast, close to O(1), because the Dictionary<(Of <(TKey, TValue>)>) class is implemented as a hash table.

 

什麼是 HashTable? 又是一個好例子,唸過資料結構的都知道吧? 我就不多說了,請看 wiki:

http://en.wikipedia.org/wiki/Hashtable

 

一樣是看 MSDN 文件,有沒有唸過資料結構,到這裡就差這麼多了。體會到學校教的東西真的有用了嗎? 這個例子還沒完,再看下去。

 

事實上,以上的實作方式都不合格。LIST 效能不好,Dictionary 拿來作索引有個致命的缺點:

它的 KEY 不能重複!!!

是的,對應到資料庫的話,它就好像是個 unique key 一樣。拿來當 NAME 的索引還沒問題,拿來當其它欄位的索引就糟糕了,別說效能問題,連用都不能用。

另外,針對排序的問題也是無解,這是 HashTable 的特性,要照順序排,就要另請高明。

 

事實上,以上的實作方式都不合格。List 搜尋的效能不算好,而 Dictionary 也只能處理 exact match 的狀況,同時也無法處理需要排序的問題。

 

唸過書的再想想,這時該怎麼辦?

標準解法是分別照這幾個欄位排序,然後用 Binary Search. 這才是正解。因為排序好的資料就像一般資料庫的 index, 可以讓你很容易的 order by, 同時又能讓你很快的找到你要的資料,甚至是列出某一段範圍的資料都沒問題。

不過寫成程式要怎麼作? MSDN 就在手上嘛,System.Collection.Generic 就把它當購物網站,逛一逛... 看有沒有其它合用的。

 

不錯,又找到兩個: SortedListSortedDictionary,還是一樣,那一個比較合適? MSDN 都寫的很清楚,足夠你判斷了,前題是資料結構教的幾個基本觀念 (像是前面講的 Hash Table, Time Complexity 等) 人家寫出來你要看的懂,看的懂就知道該挑那一個。

 

至於挑選的過程我就不多說了。我最後決定用 SortedList, 列一下這個 Collection 的特性:

SortedList.Add( )

This method is an O(n) operation for unsorted data, where n is Count. It is an O(log n) operation if the new element is added at the end of the list. If insertion causes a resize, the operation is O(n).

 

新增一筆需要的時間是 O(n), 唯一特例是加在最後面,而且沒引發 resize 的動作,就是 O(log n)。至於排序? 通通是 O(1),因為在 Add() 把資料加進來時就排好序了,所以 Add() 花的 O(log n) 就是在排序。要照順序印資料或找資料,完全不費吹灰之力,拿來印就是了。不過比較可惜的是,SortedList 並沒有提供 BinarySearch,因此要找 "0928-1234*" 這樣的資料要辛苦點,自己用 BinarySearch 的邏輯,簡單寫一下吧。如果前面的關卡都過了,這應該不難吧?

改用 SortedList 最大的缺點就是載入資料時會比較慢,不過其它在程式的處理上,還有效能都更貼近這個題目的需求。來看看程式碼,這次我用了兩個 SortedList,分別代表替 name 及 phone number 作排序:

 

改用 SortedList<> 的範例[copy code]
        private static void Sample3()        {            Stopwatch timer = new Stopwatch();            timer.Reset();            timer.Start();            // 產生假資料庫            SortedList<string, ContactData> name_index = new SortedList<string, ContactData>();            SortedList<string, ContactData> phone_index = new SortedList<string, ContactData>();            {                for (int index = 0; index < 1000000; index++)                {                    ContactData cd = new ContactData();                    cd.Name = string.Format("A{0:D6}", index);                    cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);                    cd.PhoneNumber = string.Format("0928-{0:D6}", index);                    name_index.Add(cd.Name, cd);                    phone_index.Add(cd.PhoneNumber, cd);                }            }            Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            timer.Reset();            timer.Start();            {                // 搜尋 A123456 這個人的資料                ContactData data = name_index["A123456"];                Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);                //data.OutputData(Console.Out);            }            timer.Reset();            timer.Start();            {                // 列出電話號碼為 0928-1234* 開頭的使用者                for (int pos = BinarySearch<string, ContactData>(phone_index, "0928-1234");                    pos < BinarySearch<string, ContactData>(phone_index, "0928-1235");                    pos++)                {                    //phone_index.Values[pos].OutputData(Console.Out);                }                Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            }            Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);        }        private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key)        {            return BinarySearch<TKey, TValue>(index, key, 0, index.Count - 1);        }        private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key, int start, int end)        {            if (start == end) return end;            int pos = (start + end) / 2;            int compareResult = index.Comparer.Compare(key, index.Keys[pos]);            if (compareResult == 0)            {                return pos;            }            else if (compareResult > 0)            {                return BinarySearch<TKey, TValue>(index, key, pos + 1, end);            }            else            {                return BinarySearch<TKey, TValue>(index, key, start, pos - 1);            }        }
   1:  private static void Sample3()
   2:  {
   3:      Stopwatch timer = new Stopwatch();
   4:      timer.Reset();
   5:      timer.Start();
   6:      // 產生假資料庫
   7:      SortedList<string, ContactData> name_index = new SortedList<string, ContactData>();
   8:      SortedList<string, ContactData> phone_index = new SortedList<string, ContactData>();
   9:      {
  10:          for (int index = 0; index < 1000000; index++)
  11:          {
  12:              ContactData cd = new ContactData();
  13:              cd.Name = string.Format("A{0:D6}", index);
  14:              cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);
  15:              cd.PhoneNumber = string.Format("0928-{0:D6}", index);
  16:              name_index.Add(cd.Name, cd);
  17:              phone_index.Add(cd.PhoneNumber, cd);
  18:          }
  19:      }
  20:      Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  21:      timer.Reset();
  22:      timer.Start();
  23:      {
  24:          // 搜尋 A123456 這個人的資料
  25:          ContactData data = name_index["A123456"];
  26:          Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  27:          //data.OutputData(Console.Out);
  28:      }
  29:      timer.Reset();
  30:      timer.Start();
  31:      {
  32:          // 列出電話號碼為 0928-1234* 開頭的使用者
  33:          for (int pos = BinarySearch<string, ContactData>(phone_index, "0928-1234");
  34:              pos < BinarySearch<string, ContactData>(phone_index, "0928-1235");
  35:              pos++)
  36:          {
  37:              //phone_index.Values[pos].OutputData(Console.Out);
  38:          }
  39:          Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  40:      }
  41:      Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);
  42:  }
  43:  private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key)
  44:  {
  45:      return BinarySearch<TKey, TValue>(index, key, 0, index.Count - 1);
  46:  }
  47:  private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key, int start, int end)
  48:  {
  49:      if (start == end) return end;
  50:      int pos = (start + end) / 2;
  51:      int compareResult = index.Comparer.Compare(key, index.Keys[pos]);
  52:      if (compareResult == 0)
  53:      {
  54:          return pos;
  55:      }
  56:      else if (compareResult > 0)
  57:      {
  58:          return BinarySearch<TKey, TValue>(index, key, pos + 1, end);
  59:      }
  60:      else
  61:      {
  62:          return BinarySearch<TKey, TValue>(index, key, start, pos - 1);
  63:      }
  64:  }

 

執行結果:

image

 

至於前面產品經理問的問題,各位就試著自己到 MSDN 找看看答案吧! 比較過之後,你就會瞭解為什麼我會挑選 SortedList .. 我只挑 SEARCH 時間來看,List 的搜尋是 O(n), 而 SortedList 的搜尋是排序過的資料作 BinarySearch, 找找書就知道是 O(log n), 分別來比較一下:

當 N 等於 1000000 時:

List: 3131861 ticks
SortedList: 39294 ticks (快 80 倍)

 

推算一下,N 放大為 100 倍 (100000000) 時:

List: 3131861 x 100000000 / 1000000 = 313186100 ticks
SortedList: 39294 x log 100000000 / log 1000000 = 52392 ticks (快 5978 倍)

 

看到了嗎? 換個 Collection 物件,對於 Search 這個動作,一百萬筆資料時差了 80 倍,當資料成長一百倍 (100000000 筆) 時,搜尋速度差異爆增為近 6000 倍! 這就是資料結構或是演算法的差異,這樣的差異已經大到其它地方最佳化怎麼作都補不回來的地步,唯一一個關鍵就是要用對演算法!

   

終於打完這篇了。沒想到前一篇寫一堆老生常談的話,這次又變成一堆 sample code 了。不過我的目的就是讓各位瞭解,基礎一定要顧好啊,不然寫程式是一定會碰到瓶頸的。這次從很簡單的需求,帶到資料結構的觀念,再帶到 MSDN 裡面特別標記的資訊...。看完後應該不會再有人說學校教的東西沒用了吧?

 

有網友問過我有沒有推薦什麼書? 很抱歉,我也只看過課本而以 ... 哈哈,這些純粹是出來工作後,無意間還想到要去翻翻課本得來的經驗。其實這種例子很多,過去我常貼的 multi-thread 的文章也是很多這樣的例子,只不過課本從資料結構換成作業系統了,這個主題才寫到 1-2, 後面還有, 有什麼看法或心得就請留在回應給我吧! 如果能支持一下旁邊的讚助商的話也算是種鼓厲啦.. 敬請期待下一篇..

--

調查一下,有人看這篇之前就知道 SortedList 嗎? 留個話給我吧,我很好奇這種東西有多少人會去用... :D



9/28/2008 12:44:00 AM

Google 讓人越來越笨?

543 | 技術隨筆

無意間在網站上看到這則新聞:

 

GOOGLE 讓人變笨?網路便利後遺症
http://n.yam.com/bcc/life/200806/20080626204869.html

 

文內有這麼一段話:

但是大家都忽視了這種便捷要付出的代價。「網路似乎粉碎了人們專注與沉思的能力,到如今,腦袋只盼著以網路提供資訊的方式來獲取資訊」。

影響所及,傳統媒體也跟著零碎化,長篇大論的東西不再有人要看,一篇文章超過四個段落,讀者就想落跑,電視節目加入滾動字幕和不斷跳出的小廣告,報刊則儘量縮短文章長度,改以一小塊一小塊的摘要取代,在版面上堆砌各種易於瀏覽的零碎資訊。

這篇文章還沒找到真正源頭的那篇文章,不過這篇一定要推一下,真的是講到我心坎裡了。我要講的不是這篇文章,而是現在 INTERNET 環境下,GOOGLE 實在是太方便了,讓很多事開始可以 "速成" 起來。技術的進步不是壞事,不過在什麼都要 "速成" 的文化下,到底還有多少人願意去深入的研究某一件事物呢?

 

以我的老本行 "軟體開發" 為例,很多人半路出家,學了一些工具或是簡單的網頁程式設計,就可以出家了。天資好一點的,碰到問題還曉得到 GOOGLE 去查查有無合適的 SOLUTION,找到了就東拼西湊湊出個 "可以動" 的 SOLUTION。聽起來沒什麼不好啦,不過這樣一來,沒有幾個人還會願意再深入瞭解為什麼要這樣做等等議題了。我面試過的工程師也有幾十個了 (不知道破百了沒?),有幾個問題是我必問的,其中一個就是:

 

"你碰到不知道怎麼解決的問題時,你會怎麼做?"

 

我沒真的統計啦,不過印像中,問題答案的榜首是:

  1. GOOGLE 找看看
  2. 各大論譚去問問題 or 搜尋看看
  3. 問朋友
  4. 翻書
  5. ... (不外乎是對外求救之類的方法)

我期望的答案一直沒有聽到,而聽到的都是透過 INTERNET + SEARCH ENGINE 這類的回答。當然這是沒錯啦,因為 GOOGLE 實在太方便了,不過我期望聽到的 "再進修" "想辦法去瞭解問題的原因" 等等答案都沒聽到。就算要找資料,連知道要去查第一手資料 (M$ 的東西,大部份 MSDN 都有提供第一手的資訊,不然就是某技術產品的官方網站) 的觀念都沒了,只因為英文看不懂? 或是鄉民寫的東西看的比較簡單? 咳咳...。

而這件事的負面影響,就是人才越來越 M 型化了。INTERNET 只需要有 M 型頂端的人提供資訊,讓其它人來找資源就夠了。GOOGLE 越強,落在 M 型左邊人才的需求就越少。越來越多人依賴 GOOGLE找到零碎的資訊拼湊起來,而少了獨立思考解決問題的能力。我想人類的智能,重組的部份一定會越來越多,創造的部份一定會越來越少,會不會到最後創造的部份就會消失不見了? 難怪 INTEL 會如此預言,2050年後,機器的智慧會開始超越人類了。很有道理耶,因為搜尋 & 重組不就是電腦的專長嗎? 資料夠多運算速度夠快就能做的到。而電腦追不上的 "創造" 這種能力,人類卻漸漸的在喪失...

也許實際情況不會這麼悲觀,但是我相信這一定是個趨勢。能怎樣對抗這樣的 "退化" ? 想辦法讓自己的能力落在 M 型的左邊吧。跟上一篇講的一樣,沒有速成的方式,先把內功練好吧! 與其花時間研究那些兩三年就會淘汰掉,看起來很炫的 "新技術",不如多花點時間學一學能讓你終身受用的基礎能力吧。



9/27/2008 4:00:00 AM

該如何學好 "寫程式" ??

[精選文章] | 543 | 技術隨筆

會寫這篇是因為上禮拜,有個資深的同事問我個問題,如何把底下的 programmer 素質拉上來? 跟他講這問題害我那天拖到晚上十點才回家吃晚飯 @_@,不過我想這也是現在台灣軟體業普遍碰到的人才問題,就順手寫了這篇。這篇是打算要貼在公司的 BLOG 裡給同事看的,這裡先貼一下,到時整理好再搬過去...。

 

 

-----------------------------------------------------------------------------------------

其實在這之前,一直有人問我這類問題,我的回答總是一樣: "要從基本功做起。"   只不過大部份人都會皺皺眉頭,想說 "為什麼沒有速成一點的方法? "

先撇開軟體開發一定要有其它領域的 domain know-how 之類的東西,我就針對到底有沒有辦法把 CODE 寫好這件事來討論。如果真的存在速成的方法,那滿街就都是高手強者了。技術及工具的進步,是簡化你操作工具及開發過程的細節,但是其中累積的知識及理論,只會越來越多

現在要寫程式所必須俱備的技術門檻越來越低,但是要能專精所必要的知識及經驗則是越來越高。我常在想,我是一路學習摸索上來的,大學四年唸了電機學到了硬體的底子,大三大四本來想修資工當輔系,後來直接去考資工所... 這三四年下來也有點資訊的底子,這些知識讓我到現在都還不用辛苦的去 "追" 技術,我只要看看 overview,大概就能掌握這些技術能解決什麼問題,不能解決什麼問題等等。真正需要用的時後,文件翻一翻就能找到我要的段落,就能夠上手了。

不過現在的年輕人就沒那麼好運了,要從 CPU 是怎麼設計,怎麼執行指令,到瀏覽器為什麼點下網址就能看到網頁,這一連串的細節,大概現在的大學畢業生都回答不出來吧? 現在隨便買本書翻一翻,就能寫出一個漂亮的網站,誰還願意去唸那些基本功? 也因為這樣現在的人都少了那份 "內功",只剩漂亮的招式,出招很勵害,不過打沒兩下就後繼無力,或是對手出了沒看過的招,就不知怎麼接下去了。

如果你真的有心把底子練好,我是有幾個建議的方向,雖然看起來沒什麼用,但是看熟了你一定會發現,你寫什麼程式都逃不了這幾個基礎知識。

  1. 最基本的: 計算機概論 & 資料結構
    這些有助於你用正確的邏輯寫程式。要成為一個合格的 programmer 一定要有這樣的能力。
  2. 進階一點的系統層面,作業系統 & 系統程式
    這些有助於你瞭解系統層面如何運作,如果你開發的系統需要些基礎建設,像是元件等等,這些知識很有用。成為 software engineer 就應該要有這些基礎。
  3. 再來就專精一點了,我推薦 OOP 理論 / Design Patterns、或是軟體工程的方法論 ( XP, TDD ... 等 )
    這個層次的知識能幫助你設計正確的架構,或是用正確的方式開發軟體,是成為 ARCHITECT 的必要技能。

每一項都代表一個階段。上禮拜跟同事討論的,其實只有討論到 (1) 的部份。如果工程師都 "" 寫程式,但是用的邏輯看起來都 "怪怪的",那就是要加強 (1) 的部份了。我簡單的舉個例子,資料結構在學什麼? 跟實際寫程式能有什麼關聯?

想到資料結構,不外忽一堆排序 (SORT) 的演算法,或是各種 TREE / LIST 等怎麼 "放" 資料,及怎麼 "找" 資料的問題,如 LINKED LIST,HASH TABLE,BINARY TREE,HEAP,STACK 等等。再來就是什麼問題可以用什麼資料結構來處理? 像是走迷宮要靠 STACK,各種資料結構的特性為何? 它們的時間複雜度 (Time Complexity) 為何? 什麼時後該用那一種?

這些是很基礎的問題,不過你如果不是科班的,只是翻翻書就會寫程式的,那這些問題應該都回答不出來吧? 針對這部份,我強烈建議要學的人一定要先搞懂這些 "邏輯"。我不稱為理論,是因為他們還太淺,只是個作法而以。搞懂這些邏輯,你至少要有能力把程式寫出來。

因此第一課很簡單,挑幾種 SORT 的方式,比如 Bubble Sort, Quick Sort 等等,不用多,兩三個就好,步驟搞清楚了,還能用你熟悉的程式寫出來 ( 如: C# / JavaScript,當然你不能作弊用現成的 SORT 函式庫 ),你就過關了。

再來就是搞懂各種資料結構,我舉幾個 .NET 內建的,卻又常讓人搞混的幾個 Collection。List / LinkedList 用的方式都一樣,那麼兩者到底有什麼不一樣? 只塞一百筆,找出一筆要 10 ms 的話,塞一萬筆找出一筆要花多少時間? 是 100 倍嗎? 還是 10 倍? 還是都一樣?? 是 Microsoft 工程師太無聊,故意寫來讓你傷惱筋的嗎?

如果這部份你也搞懂了,接下來就是應用了。就拿導航系統來說就好,地圖要用什麼方式存才好? 使用者選定起點及終點,你該怎麼幫它找出最佳的路逕? 不管畫面等等問題,你有辦法寫出程式找到答案嗎? 這就是典型的資料結構的應用。你沒學好資料結構的話,看再多 C# / ASP.NET 的書,一點用都沒有啦,碰到這類問題,管你用 VB / C++ / C# 還是  Java, 只能坐在螢幕前發呆而以。

 

總結一下,你符合我講的 (1) 基本要求嗎? 很簡單,這些問題或程式你都寫的出來就符合了:

  • 丟一付洗過的撲客排給你 (不要多,黑桃1 ~ 13就好),你知道怎麼用 Bubble Sort / Quick Sort 的步驟把它排好嗎? 丟一個陣列,裡面隨便打幾個數字,你能寫程式把它由小到大排好印出來嗎?
  • 假設記憶體夠大的話,你有辦法把一百萬筆通訊錄資料讀到記憶體內 (用什麼物件都隨你),然後還能用很快的速度找到你要的資料嗎? 不同的搜尋方式,你知道該用什麼樣的方式找才有效率嗎?
  • 以台灣高速公路為題 (中山高、北二高、國道二號),你有辦法寫程式,讓使用者指定起點跟終點的交流道,然後替它找出建議的路線嗎? (把延路經過的交流到跟收費站列出來就好)

看起來就像是作業,沒錯。不過它是很實際的基本功夫,如果寫不出來,那就真的該好好唸個書了。其實這些問題,都跟熱門的技術 (如 DB / WEB / RIA 等等) 無關,就很單純的看你的邏輯能力而以。這個主題我會繼續寫下去,大概一兩個禮拜一篇吧。我的目的是希望大家底子打好再來學這些熱門技術,這樣你才有辦法更進一階,否則就只能隨著技術規格推陳出新,不斷的在追新技術而以。上面那三個問題,有興趣的話歡迎找我聊聊。在這留話或是 mail 給我都可以。



9/21/2008 10:30:00 PM

[C# yield return] #2. 另類的應用 - Thread Sync 替代方案

Microsoft.NET | 543 | C# | Threading | 小技巧 | 我的作品 | 技術隨筆

上篇,講了一些 yield return 編譯後產生的 Code, 說明了 C# compiler 如何用簡單的語法替你實作了 IEnumerator 介面,而完全不會增加程式的複雜度,這是我認為 C# 提供最讚的 Syntax Sugar ...。

不過無意間我想到了 yield return 還有另一種應用方式。靈感來自之前 Darkthread 舉辦的 [黑暗盃程式魔人賽]。因為參賽題目 [xAxB猜數字遊戲] 原本就是考驗演算法,邏輯就不大簡單了,加上要配合 GameHost 的呼叫方式,難度更提高不少。因此之前貼了兩篇文章 [ThreadSync #1. 概念篇 - 如何化被動為主動?, #2. 實作篇 - 互相等待的兩個執行緒],介紹了我改寫的 AsyncPlayer,讓程式可以分別以獨立的執行緒執行 GameHost 及 Player 的程式碼。藉著這方式讓兩者都可以 "獨立思考",邏輯不會中斷,讓程式能夠簡單一些。

不過執行緒同步機制是很花時間的,因為兩方都要等來等去...。多了 Sync 的動作,就要至少 10 ms 的時間來完成這動作。跑個十幾萬次下來,額外花費的時間太多了,因此我貼了那兩篇文章後,就一直在思考這樣的作法有沒有其它效能較佳的方式?

有的,最後我找到的答案就是 yield return,不過大家看了一定很納悶...

"yield return (Iteration) 跟執行緒同步機制有什麼關聯?"

不多說,先看看之前畫的兩張時序圖:

image

先看之前 ThreadSync #1 裡提到的圖,我這次加上紅線當 "輔助線",紅線代表執行 GameHost 的主程式,這個執行序必需反反覆覆的在 GameHost / Player 兩份類別的程式碼跑來跑去,主程式是 GameHost 發起的,當然被強迫切成好幾段的就只有 Player 了。

image

這是修改過後的版本,GameHost / Player 有各自的執行緒,紅色是 GameHost,藍色是 Player。當執行緒跑到中間時代表它在等待了,等另一方也跑到中間把執行結果放到共用變數,同時叫醒對方之後才交換過來。兩方都各自照著自己的邏輯跑,不過這種等待 & 喚醒的動作,相較於一般的 function call / return 而言,實在是太慢了...。我就是從這張圖得來的靈感,這個解決方式不就跟 yield return 很像嘛? 都是為了避免多次呼叫之間,被呼叫的另一方的邏輯被破切斷的問題... 因此我就開始思考 AsyncPlayer 是不是有機會用 yield return 寫出另一個版本...。

原本的結構很直覺,透過共用變數來傳遞資訊,用 AutoResetEvent 來通知另一個等待中的執行緒可以醒來拿資料去用。而 yield return 則要換個角度來想這件事。yield return 是實作 Iterator 的一種方式,目的是讓你的程式自己決定如何把 collection 裡的 element 照什麼方式丟出去,原本的問題就要想成:

"GameHost 要跟 Player 拿所有 Player 會問的問題,而 Player 會透過 yield return 一次一次的把問題丟出去。"

看起來好像可行,不過方向只有單向,就是 Player 丟問題給 GameHost,還缺了 GameHost 把問題答案交給 Player 這段。不過這部份好解決,一樣用共用變數就搞定。細節我就不講太多,直接來看程式碼:

用 yield return 改寫過的 AsyncPlayer[copy code]
        public abstract IEnumerable<HintRecord> Think();        private HintRecord last_record = null;        public override int[] StartGuess(int maxNum, int digits)        {            base.StartGuess(maxNum, digits);            this._enum = this.Think().GetEnumerator();            this._enum.MoveNext();            return this._enum.Current.Number;        }        public override int[] GuessNext(Hint lastHint)        {            this._enum.Current.Hint = lastHint;            if (this._enum.MoveNext() == true) return this._enum.Current.Number;            throw new InvalidOperationException("Player Stopped!");        }        public override void Stop()        {            base.Stop();            this._enum.Current.Hint = new Hint(this._digits, 0);            try { this._enum.MoveNext(); }            catch {                Console.WriteLine("!!!!");            }        }        protected virtual HintRecord GameHost_AskQuestion(int[] number)        {            this.last_record = new HintRecord(                (int[])number.Clone(),                new Hint());            return this.last_record;        }        protected HintRecord GameHostAnswer        {            get            {                return this.last_record;            }        }
   1:  public abstract IEnumerable<HintRecord> Think();
   2:  private HintRecord last_record = null;
   3:  public override int[] StartGuess(int maxNum, int digits)
   4:  {
   5:      base.StartGuess(maxNum, digits);
   6:      this._enum = this.Think().GetEnumerator();
   7:      this._enum.MoveNext();
   8:      return this._enum.Current.Number;
   9:  }
  10:  public override int[] GuessNext(Hint lastHint)
  11:  {
  12:      this._enum.Current.Hint = lastHint;
  13:      if (this._enum.MoveNext() == true) return this._enum.Current.Number;
  14:      throw new InvalidOperationException("Player Stopped!");
  15:  }
  16:  public override void Stop()
  17:  {
  18:      base.Stop();
  19:      this._enum.Current.Hint = new Hint(this._digits, 0);
  20:      try { this._enum.MoveNext(); }
  21:      catch {
  22:          Console.WriteLine("!!!!");
  23:      }
  24:  }
  25:  protected virtual HintRecord GameHost_AskQuestion(int[] number)
  26:  {
  27:      this.last_record = new HintRecord(
  28:          (int[])number.Clone(),
  29:          new Hint());
  30:      return this.last_record;
  31:  }
  32:  protected HintRecord GameHostAnswer
  33:  {
  34:      get
  35:      {
  36:          return this.last_record;
  37:      }
  38:  }

程式碼一如往常,又是只有一點點 (謎之音: 你到底有沒有寫過長一點的程式碼? -_-) ...。 原本的 Think 改成會傳回 IEnumerable<HintRecord> 的型別,因此內部就可以透過一連串的 yield return xxxx; 指令來把問題交給 GameHost。而 GameHost 拿到題目就會開始計算答案,然後再呼叫 Player.GuessNext( ) 把上次的答案傳回去。透過 Player 的實作,GuessNext 會呼叫 _enum.MoveNext( ), 控制權會再交到 Think( ) 上次呼叫 yield return 的地方,直到又執行到下一個 yield return 為止。這時 GameHost 又取得下一個問題,不斷重複這樣的動作直到結束。

同樣的,我們用 DummyPlayer 改寫,看看用 yield return 的版本寫起來是怎麼樣?

DummyYieldPlayer 的程式碼[copy code]
    public class DummyYieldPlayer : YieldPlayer    {        private Random _rnd = new Random();        private int[] randomGuess()        {            int[] _currAnswer = new int[this._digits];            List<int> lst = new List<int>();            for (int i = 0; i < _digits; i++)            {                int r = _rnd.Next(_maxNum);                while (lst.Contains(r))                    r = _rnd.Next(_maxNum);                lst.Add(r);                _currAnswer[i] = r;            }            return _currAnswer;        }        public override IEnumerable<HintRecord> Think()        {            while (true)            {                yield return this.GameHost_AskQuestion(this.randomGuess());            }        }    }
   1:  public class DummyYieldPlayer : YieldPlayer
   2:  {
   3:      private Random _rnd = new Random();
   4:      private int[] randomGuess()
   5:      {
   6:          int[] _currAnswer = new int[this._digits];
   7:          List<int> lst = new List<int>();
   8:          for (int i = 0; i < _digits; i++)
   9:          {
  10:              int r = _rnd.Next(_maxNum);
  11:              while (lst.Contains(r))
  12:                  r = _rnd.Next(_maxNum);
  13:              lst.Add(r);
  14:              _currAnswer[i] = r;
  15:          }
  16:          return _currAnswer;
  17:      }
  18:      public override IEnumerable<HintRecord> Think()
  19:      {
  20:          while (true)
  21:          {
  22:              yield return this.GameHost_AskQuestion(this.randomGuess());
  23:          }
  24:      }
  25:  }

跟上次的 DummyAsyncPlayer (用 ThreadSync 的版本) 一樣,超簡單,實在沒什麼需要說明的了。唯一要特別記得的是,如果你需要取得 GameHost 傳回的答案,應該在 22 ~ 23 行之間,使用 this.GameHostAnswer( ) 來取得答案。有人問我為什麼不把它包成 function call ? 在 function 內接到參數後呼叫 yield return, 而把答案 return 回來不是很好嗎?

很無奈,除非 C# 支援像 C/C++ 那樣的 MACRO 語法,不然這個東西是不可能單靠 yield return 就做出來。你使用 yield return 的條件就是 function return type 一定要是 IEnumerable<T>,這是配對的,代表你不能任易的把 yield return 移到其它 function call 內。除非你不靠 C# yield return 來自動產生對應的 IEnumerator,一切自己來就可以。不過這樣不就又回到原點了? 咳咳... 就乖乖的寫兩行吧。

這樣的寫法執行效率就好的多,我用 DummyYieldPlayer 來測試,跟 DarkThread 提供的版本不相上下,意思是差異小到可以不理它的地步了 :D 這樣的方式不會有太大的效能損失,因為最後要執行的程式碼,跟直接手寫是差不多的,只是中間難寫的那段 code 是 C# compiler 幫我們解決掉,而不是像上回 AsyncPlayer 是用兩個執行緒來解決的。

效果很滿意,當然最後參賽的版本就改這寫法了 :D。不過寫的太晚,來不及幫到其它參賽者 :P,想到這方法算是我佔了 C# compiler 一點便宜,有幸找到方法坳 C# compiler 幫我把最難的部份寫好了,我自己則樂的輕鬆,專心研究怎樣才能少猜幾次... 這裡把我另類應用 yield return 的方法貼給各位參考一下,也算作個筆記 :D,各位高手如果還有發現 yield return 解決過你什麼樣的怪問題,也歡迎到我這留個言 :D



9/10/2008 4:44:00 AM

莫明奇妙的錯誤訊息: 找不到 VJSharpCodeProvider ?

Microsoft.NET | 543 | ASP.NET | BlogEngine.NET | 小技巧 | 技術隨筆

話說前陣子處理了 BlogEngine.NET 升級到 1.4.5.0,另外也寫了 SecurePost.cs 這個 extension, 其時都碰過這個鳥問題,只是一直沒去理它而以。接下來為了要改 PostViewCounter.cs (BE extension, too), 又碰到... 於是就認真的研究了一下...。

過程是這樣,為了建立 BlogEngine 的開發環境,首先我從官方網站下載了 source code, 解開後編譯都沒問題,OK。

接下來 WEB 的部份我把網站上的 source code 搬過來 (不包含 ~/App_Data,太大了),編譯也 OK。

不過我要改 Counter 的 Code 啊,沒有一些 SAMPLE DATA 很難測試,只好把資料檔也搬過來.. 結果 Visual Studio 2008 就冷冷的回了這訊息給我:

(0): Build (web): The CodeDom provider type "Microsoft.VJSharp.VJSharpCodeProvider, VJSharpCodeProvider, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" could not be located.

 

我沒有漏貼前面的訊息... 的確是沒有檔名,也沒有行號(0)。我最不能忍受的就是沒頭沒尾的 ERROR MESSAGE 了。除了告訴你 "掛掉了" 之外,無頭無腦的對於追查問題實在沒什麼幫助。只好靠自己了...。雖然這是個 compile error message,不過我要 RUN 的畢竟是個 web site, 不編譯也是可以跑,除了那個惱人的錯誤訊息之外,要執行倒是沒問題。只不過編譯失敗,我就不能設中斷點,直接 F5 執行測試。雖然可以另外手動 Attach Process 的方式來除錯,不過每次都要這樣搞實在是很煩..

 

仔細想了想,沒錯,我是沒裝 Visual J#。不過我的確沒要用 Visual J# 啊,如果真的用到 J# 的話,出這訊息是應該的。訊息沒有原始檔? 也沒有錯誤行號? 那問題應該是 Global 的範圍,第一個想到的就是 web.config 是不是定義了 CodeDom 或是指定了相關的 CodeProvider ? 無奈查了一遍沒看到,VS2008 的 PROJECT 設定也沒看到引用任何 J# 相關的 LIB...

 

已經到了死馬當活馬醫的地步... 開始亂找一通碰碰運氣。搜尋了一下有沒有 *.java 的檔? OUCH,還真的有... 在 ~/App_Data/files 下找到我古董檔案,研究所時代寫的 Java Applet .... 順手試一下,刪掉後還真的就過了? 這個無頭無腦的問題,就在不知不覺中找到 solution, case closed!

 

怒... 這樣也算? 找到 .java 的程式碼,去找 VJ# 來編譯還說的過去,不過找 "source code" 找到 ~/App_Data 實在是太超過了一點... 好歹也列個要編譯那個檔案,然後找不到對應的 CodeProvider,這樣要排除問題也簡單一點...

 

結論是: 各位別太鐵齒,看來 ~/App_Data 下的檔案也是不能亂塞的...



9/2/2008 1:07:01 AM

好酷的漆彈陣列...

543

 

 

雖然我很少貼這些五四三的 (明明 543 這 tags 點下去有一堆..) ,不過無意間在癮科技逛到這段實在是太酷了... 流言終結者這節目的 Adam & Jamie用漆彈示範來比喻 CPU / GPU 繪圖的差別... 不管它的比喻精不精確或妥不妥當啦,那一瞬間噴出蒙那麗莎實在是太酷了... :D



9/1/2008 2:11:00 AM

請正確的引用文章內容!

543 | 不爽

在開始講 "為什麼" 之前,先講一下該如何正確的引用我的文章。

  1. 明確的提供本站及該文的網址。例如:

    ........ 網路上有個叫安德魯的寫過一篇文章: "任意放大/縮小網頁的內容" ...........


  2. 若要轉貼,請以不超過 1/3 內容為原則 (程式碼及檔案下載不算),同時也要包含第一點的資訊。引用的部份請在格式上跟你的原文有所區隔,用斜體或是改顏色都可以。例如:

    ........ 網路上有個叫安德魯的寫過一篇文章: "任意放大/縮小網頁的內容" ,他提到一個用法:
    把“放網頁大 200%”的功能直接加在我的最愛

    只要把這個 link 加到書籤, 就大功告成了!

    javascript:document.body.style.zoom="150%";void(0);

    之後看到任何網頁, 覺的看不過癮想放大看, 再點一下這個 Bookmark 就好了.
    覺的固定 150% 不好用, 那可以把 link 改成這樣:

    javascript:document.body.style.zoom=prompt('縮放比例 (%):','100%');void(0);

  3. 如果要轉貼的部份超過 1/3,或是會牽扯到商業行為,請務必取得我的同意
    聯絡方式很簡單,在這裡留言我會看的到。

    如果你要透過 RSS 自動轉貼,我也很歡迎! 不過一樣要符合前面兩點要求,請勿轉貼超過 1/3 的內容。如果你的 RSS 轉貼系統只能轉貼整篇,很抱歉,只好請你停止 RSS 轉貼。

    另外,所謂的商業行為,不只是拿去投稿出書賺稿費之類的,如果你轉載的網頁有刊登廣告,就已經算是商業行為了。

 

 

--------------------------------------------------------------------------------------------------------------------------------

引用說明結束。以下為本文內容,純發洩用。

 

為什麼突然要貼這篇? 該說是 "一回生二回熟" 嘛? 沒錯,每次一時興起,到 GOOGLE 找一下我自己的資訊,總是會發現有人沒經過我同意轉貼我的文章...。轉貼對我實質上的影響並不大,畢竟我寫東西是興趣,不是拿還養家糊口的,所以被轉貼除了心理不大爽之外,其它都還好。不過這樣會大大的降低我寫東西的意願。

舉例來說,最不滿的就是像上次在對岸的入口網站碰到的案例,原文照貼,沒註明出處,留 comment 提醒還被砍掉!? 回報站方也是一副不理不睬的樣子... 這種沒什麼好說的了,直接用我的方式表達我的不滿。

碰過另一個例子,也是很令人厭惡,就是到處轉貼文章,不管有沒有註明出處。落落長的一整篇文章就被 COPY / PASTE 過去,最後加個小小的 "出處" 就算免責了? 如果他的網頁還放了廣告,那就等於是不當的取得利益。講的難聽一點,這跟盜版沒兩樣,不都是用別人的作品去賺取利益? 最後是通知 BLOGGER,站方停權結束這件事。

 

看過 Jedi 的文章,我很認同他在這篇 [引用不能祇是引用] 文章裡的看法。很多人好像以為只要加了 trackback 就是盡到道義上的責任了? Trackback 太方便了,之前用的 CS 甚至方便到連 trackback 的 URL 都不需要點,一切全都無腦化自動的進行。方便到好像原本該尊重作者的動作都不用作了,直接貼了就好。所以我的網站刻意不放 trackback,因為我不喜歡這樣的結果。

我自己認為尊重作者的引用方式,要有兩個前題。一個是清楚的讓你的讀者知道,這段引用的內容出自那個人的那一篇文章。另外就是轉貼的部份。如果你不能只提供 LINK,而需要轉貼它人的內容到你的文章內,請把握好 "片段" 的原則。轉貼幾句話是合理的,轉貼一整篇就是白目了。這跟書本只影印一部份 (好像是 1/3 以內?) 是合法的,整本影印就侵害到原作者版權了。網路上的內容,複製更容易了,也讓人更容易忽略掉這些問題。

 

所謂 "不教而殺謂之虐",就藉著這個機會說明一下,怎樣作我才不會認為我的權益被侵害。我很歡迎同好轉貼我的文章,不過我既然是我自己的文章,我會保有這幾項權益:

  1. 個人形象
    我會發佈的東西都是在 internet 上代表我這個人。要幫我散佈可以,不過請務必讓讀者知道我的網址及該篇文章的網址。
  2. 讀者 feedback
    收到有人看了我的文章有 feedback, 對我是一大鼓勵。如果你引用的方式違背了第一點,就有可能讓想給我 feedback 的讀者找不到管道。
  3. 點閱率 / 廣告
    很實際的一點,如果你不當的引用會影響我的點閱率 (如同那些全文照貼的),不論你有沒有註明出處,在這個層面上就已經侵犯到我的權益了。雖然我不靠廣告過日子,不過我把它當作打電動的經驗值看待,看著點閱次數增加也是一種成就感。
  4. 原作者要知道作品如何被利用
    如果你有不得以的理由 (比如要發布在內部,不能連到 internet, 或是發部在平面媒體、報章雜誌) 必需原文照貼,沒有辦法滿足上面三點...,或是你要拿去賣錢等等之類的商業應用,請務必取得我本人的同意。

 

這篇不是合約,也不是在訂什麼法律條文...。我只是想表達出我對於 "轉貼" 及 "引用" 的看法而以。其實任何方式的引用我都歡迎,只要不違背上面四點原則為前題就好。只是一直碰到類似的事件,我不想成為處理盜文事件的老手啊啊啊...。希望打了這篇,可以讓那些沒有惡意的轉貼行為,不會被我誤認為又是盜文事件。



8/29/2008 1:52:00 AM

升級到 BlogEngine.NET 1.4.5.0 了

543 | BlogEngine.NET

哈,升級還蠻簡單的,一下子就搞定了... 特此留念 :D

 

枉我還大陣仗的對付它,VSS (Visual Source Safe), VSS (Volume Shadow copy Service) 都搬出來用了,結果只是目錄搬一搬就好... 咳咳。

除了 Bot Checker 沒有搬過來之外,其它應該都搞定了吧? 如果有發現我的網站有那裡沒弄好,記得留言跟我講一下 :D



8/28/2008 2:57:00 AM

Canon G9 害我沒睡好... 相片自動轉正的問題

Microsoft.NET | 543 | WPF | 技術隨筆 | 小技巧

抱怨一下,因為在看照片時發現,有些直的拍的照片看起來是正確的 (會自己轉 90 度),有些卻不是... 得歪著頭看,所以就很好奇到底是怎麼回事...。

 

image

這幾張是正確的 (右上 & 左下,會自動轉 90 度)

 

image

這幾張是錯的 (應該自動轉 90 度才對)

 

image

第二張是錯的 (應該要轉 180 度才對)

 

越想越不對,我記得除了我那台古董 CANON G2 之外,後來 CANON 的相機都有加上偵測轉向的機制啊? 不外乎是水銀開關或是類似的東西,總之相機能夠得知目前是不是轉直的拍,同時能把這資訊寫到 EXIF 內的 ORIENTATION 欄位去...

但是為什麼拍出來的相片有的可以自動轉,有的不行? 花了點時間歸納一下,發現 G9 拍出來的 "有機會" 是正常的,而家裡大人的 IXUS55 則都不會自動轉,真是怪了... 在相機上看都會自動轉正啊...

因為我的照片都是自己用 WPF 寫程式縮圖處理的,我開始懷疑是不是我的歸檔程式的問題。G9 拍的 .CR2 檔,透過 RAW CODEC 轉成 JPEG 會自動轉正,G9 / IXUS55 拍的 JPG 檔則不會...

 

 

image

嗯,開始無聊了,拿起相機拍了四種角度,然後用 DEBUGGER 去看 EXIF 的 ORIENTATION 值為啥... .CR2 要用 "/ifd/{ushort=274}" 來查,會得到一個 UInt16 的值,如果是 .JPG 則要改成 "/app1/{ushort=0}/{ushort=274}" ...

得到的值還真怪。.CR2 / .JPG 都一樣。依照上圖的順序,得到的值分別是 0x01, 0x08, 0x01, 0x06。查了查文件,除了轉 180 度那個的值不大對之外,另外三個都正常。不過 Canon Codec 在 decode 時會自動幫我做 RotateTransform,我得在處理 .JPG 時自己補上這個動作。除了轉 180" 之外其它都正常了。

 

就是那張 180 度的不正確,害我氣的牙癢癢的... 決定跟它拼了... 改了改程式,把所有 EXIF 都印出來,用肉眼一個一個比... 本來想說是不是有別的 FLAG 可以判定出來正反? 結果看到眼睛脫窗了也沒找到,我連 EXIF 裡的 BLOB (Binary data) 都一個一個印出來看 @_@,GOOGLE 也找不到啥有用的資訊...

最後氣到,拿起相機重拍一次,這次直接在相機上看,按下 DISPLAY 看看有無其它資訊... COW,終於發現... 相機自己也認不出轉 180 度的狀況? 嘖嘖... 搞了半天 CANON 只有偵測左轉及右轉 90 度的情況,轉 180 度就不理它了.. 哈哈!

不過有誰會轉 180 度拍照? 當然有...

  1. 用右手拿相機自拍,按不到快門... 只好轉 180 度,用底下的姆指來按
  2. 小孩拿著自己亂拍
  3. 想不出來了...

總算水落石出,犧牲了幾個小時的睡覺時間,咳咳... 不過既然本版都是討論 .NET 程式設計的,最後就貼點 CODE 充個數... 也算沒偏離主題了 :D 不過我想應該沒人像我一樣無聊在搞這些吧?

 

取得 ORIENTATION 的值,並且判定要往那個方向旋轉[copy code]
                        BitmapMetadata metadata = null;                        Rotation rotate = Rotation.Rotate0;                        // ...	                        ushort value = (ushort)metadata.GetQuery("/app1/{ushort=0}/{ushort=274}");                        if (value == 6)                        {                            rotate = Rotation.Rotate90;                        }                        else if (value == 8)                        {                            rotate = Rotation.Rotate270;                        }                        else if (value == 3)                        {                            rotate = Rotation.Rotate180;                        }
   1:  BitmapMetadata metadata = null;
   2:  Rotation rotate = Rotation.Rotate0;
   3:  // ...  
   4:  ushort value = (ushort)metadata.GetQuery("/app1/{ushort=0}/{ushort=274}");
   5:  if (value == 6)
   6:  {
   7:      rotate = Rotation.Rotate90;
   8:  }
   9:  else if (value == 8)
  10:  {
  11:      rotate = Rotation.Rotate270;
  12:  }
  13:  else if (value == 3)
  14:  {
  15:      rotate = Rotation.Rotate180;
  16:  }

 

 

 

利用 TransformGroup, 一次套用 ScaleTransform (縮放) + RotateTransform (旋轉) 兩種轉換特效[copy code]
            JpegBitmapEncoder target = new JpegBitmapEncoder();            TransformGroup tfs = new TransformGroup();            tfs.Children.Add(new ScaleTransform(0.1, 0.1));            switch (rotate)            {                case Rotation.Rotate90:                    tfs.Children.Add(new RotateTransform(90));                    break;                case Rotation.Rotate180:                    tfs.Children.Add(new RotateTransform(180));                    break;                case Rotation.Rotate270:                    tfs.Children.Add(new RotateTransform(270));                    break;            }            target.Frames.Add(BitmapFrame.Create(                    new TransformedBitmap(sourceFrame, tfs),                    null,                    null,                    null));            target.QualityLevel = quality;            //            // save to temp file            //            string temp = Path.Combine(Path.GetDirectoryName(trgFile), string.Format("{0:N}.tmp", Guid.NewGuid()));            FileStream trgs = File.OpenWrite(temp);            target.Save(trgs);            trgs.Close();
   1:  JpegBitmapEncoder target = new JpegBitmapEncoder();
   2:  TransformGroup tfs = new TransformGroup();
   3:  tfs.Children.Add(new ScaleTransform(0.1, 0.1));
   4:  switch (rotate)
   5:  {
   6:      case Rotation.Rotate90:
   7:          tfs.Children.Add(new RotateTransform(90));
   8:          break;
   9:      case Rotation.Rotate180:
  10:          tfs.Children.Add(new RotateTransform(180));
  11:          break;
  12:      case Rotation.Rotate270:
  13:          tfs.Children.Add(new RotateTransform(270));
  14:          break;
  15:  }
  16:  target.Frames.Add(BitmapFrame.Create(
  17:          new TransformedBitmap(sourceFrame, tfs),
  18:          null,
  19:          null,
  20:          null));
  21:  target.QualityLevel = quality;
  22:  //
  23:  // save to temp file
  24:  //
  25:  string temp = Path.Combine(Path.GetDirectoryName(trgFile), string.Format("{0:N}.tmp", Guid.NewGuid()));
  26:  FileStream trgs = File.OpenWrite(temp);
  27:  target.Save(trgs);
  28:  trgs.Close();


8/25/2008 1:42:00 AM

敗家: HTPC

543 | 敗家

 

image

沒錯,我又敗家了... :D

 

想弄 HTPC 已經很久了,不過老是碰到一些怪問題。原本想直接用桌機,不過桌機已經接了雙螢幕,要再接 LCD TV,沒在主機板上插兩張 VGA 卡就辦不到 (其它用 PCI / USB 之類的效能太差就不考慮了)。不過三個輸出都需要數位輸出 (兩台桌上的 LCD 都有 DVI,SHARP TV 又很有個性的不提供 D-SUB,只有 HDMI ... 不然就是色差...),沒有插兩張 DUAL DVI 的中價位 VGA 卡也沒辦法...。我用不到這麼高級的顯示卡啊 :~~~

不過就算硬體搞定了也是個大問題。試過暫時接到 TV 過,有時我要 MCE 開出來開在桌上的 LCD,有時又要開在 TV ... 光是操作就很麻煩...。最後體認到了沒有弄一台專用機的話,HTPC大概只能試一下試爽的而以。

與其是想升級換張可以插兩張顯卡的主機板 + 新的顯卡,實在不是太便宜... 尤其我須要的是 DVI x 2 + HDMI ... 後來無意間看到一堆在介紹 AMD 780G 的文章... 算一算好像比換主機板還便宜...。

於是不知不覺,八月初某日下班就多提著一袋零件回來了... 哈哈。因為有一堆東西有備料,陸陸續續買了下列東西就開始享受了:

  1. 主機板: ASUS M3A78-EM NTD 2750
  2. CPU: AMD 4850e (45W 低耗電) NTD 2250
  3. RAM: DDR2-800 2GB x 2 (實在用不到這麼多,不過看到一條 999,真的找不到理由去買 1G 的...) NTD 990 x 2
  4. POWER: SEASONIC 330W (這個也很奢侈,明明整台機器全速運轉也沒有破 100W ... 不過其它牌都不信賴... 330W 是最低的規格了) NTD 1700
  5. 搖控器: Toshiba MCE 搖控器 NTD 550
  6. 便宜的 HDMI 線 3 公尺 NTD 130
  7. 喇叭: JungleRex AF-31S NTD 3800
  8. 現有的硬碟: Hitachi 7K250 250GB SATA2 HDD
  9. 現有的舊機殼、光碟機、藍芽接收器、鍵盤滑鼠... ETC

其它像機殼,光碟機,硬碟機,鍵盤滑鼠等等就直接用現成的...。至於 MCE 一定要的 TV CARD 我反而沒買...。一方面現在的卡我都覺的太貴了 (因為之前買過 ATI theater pro 550 的卡,一張不到一千...),再加上我很少在客廳看 LIVE TV...,就算要看也是先用 MCE 錄下來。我跟家裡大人的桌機都已經有 MCE + TV 卡了,實在不大需要再買第三張... 哈哈。

果然裝了 HTPC 這樣才覺的這台電視真的是好物啊 :D,隨便一部 DVD 或是 MPEG4 的片子 (來源當然是牧場來的...) 看起來都很棒。扣掉奢侈品,其實一萬以內都搞定了。硬體沒啥好介紹的,網路一查一堆,總是有魔人有一堆測試報告,看都看不完,輪不到我在這邊講。除了耗電量實在很低一定要講一下之外... 其它硬體介紹我就跳過去了。耗電量實在是不錯,一開機耗電量 85 W 左右,開機完進入 VISTA 後就降到 50 ~ 55W,看片時看 DVD (MPEG2) 或是 AVI ( DIVX / MPEG4 ),耗電量輕微升到 60 ~ 65W,跑一些變態的 BENCHMARK 才會飆到 75 ~ 80W...。進入待命模式才 1 ~ 2W,真的是算省電了。

 

那特別要講的是啥? 主要要講怎樣不靠鍵盤滑鼠操作 MCE ... :D

不用 KB / MOUSE,那當然是用搖控器了。我買的這個我覺的很不錯,接收器插上去就可以用,什麼 DRIVER 設定都免了,VISTA X64 也一切正常。搖控器上那顆綠綠的鈕,按下去就會自動叫出 MCE。叫出 MCE 後,主要的幾個功能也都有對應的按鈕,操作起來很方便。只要可以直接在 MCE 裡播放的話,用搖控器就搞定了。現在我連放 MP3 (原本用 Media Player) 跟放照片 (Windows Live Photo Gallery) 也都改用 MCE 了... (Y) 上面那張圖就是用 MCE 放 MP3 的樣子...。

搖控器上的電源鈕,也會直接對應到電腦的 "SLEEP" 及喚醒 SLEEP 中的電腦。如果你都只用 MCE,其實操作就很簡單了。按下搖控器的開關,電腦就醒來。看完再按一下電腦就 SLEEP 去了。康博的 TV CARD 使用者比較幸福,卡片上可以直接對應到電腦的電源開關。不過... 開機關機就要等的比較久。算算 SLEEP 模式下耗掉 1W 左右的電,就算了... 隨時想看一按就開還是比較爽啊..

 

另一個比較特別的也要講一下。過去曾貼過一篇 [一萬多塊錢的簡報器],提到有軟體 (Salling Clicker for Windows 3.5)可以當簡報器,搖控 PPT 播放,也能搖控 Media Player ...,現在這些東西有內建在 VISTA 的版本了,主要就是透過 SlideShow。在 Vista 推出一年多之後,M$ 總算推出 Preview 版的軟體,可以讓你的 Windows Mobile 5 / 6 的手機及PDA,搖身一變成為 Vista SlideShow Device。再搭配上 Microsoft 才開發到 BETA 版的 SlideShow Gadget for MCE... 耶! 手機可以變成 MCE 搖控器了!

image image

這跟 MCE 搖控器有什麼不一樣? 這篇文章有簡單的介紹... 這裡也找到另一篇...。首先,TV 的節目表都可以直接秀在手機上,播放的曲目也都會顯示在手機上,如果你不希望中斷影片的播放,這就很有用。這些功能是沒有螢幕的搖控器作不到的。不過... 大部份時間,還是搖控器好用。

但是為什麼我還是要特別提一下? 因為... 我還要聽音樂啊,聽音樂時我就懶的開電視了。不開電視的情況下,這就很好用了,直接在手機上選好媒體櫃裡的專輯,按下播放,整個客廳就有音樂可以聽了 :D,這完全是搖控器辦不到的 :D

 

Microsoft Vista Slideshow Media Centre Remote

不過這兩種搖控方式不能合二為一嗎? 有... 只要肯花錢就有。這支搖控器就是符合 vista slideshow device 規格的 MCE 搖控器,一隻要 USD 199... 看到價錢,我還是買隻十分之一不到的搖控器 + 手機就好...

有打算用 HTPC 又有 WM5/WM6 手機的人一定要試一下。反正通通是現成的啊 (除了藍芽接收器你可能得花個一百塊買一支之外)。如果你也是 HTPC 一族的,有啥 TIPS 記得提供一下,如果不是的話,快點去敗吧 (H)!



8/22/2008 8:23:00 PM

Tips: 踢掉遠端桌面連線的使用者

543 | 小技巧

近日同事連不進 SERVER,因為連線人數已滿,又摸不到本機,正在那邊苦惱...。原來大家都知道怎麼連,但是都不知道怎麼砍人... 。從 windows 2000 開始就有 RDP 可以用了,當時學到的一個指令一直到現在都可以用,就藉這個機會貼一下。

什麼秘技都一樣,說穿了就不值錢。半年前貼了一篇 [遠端桌面連線的小技巧],裡面講到加上 /console 這參數就能連到 console session,不會跟其它人去搶那兩個連線,就可以把不順眼的 USER 砍了。連進去後要砍人很簡單,工作管理員叫出來,最後一頁 [USER] 就會列出有多少人掛在上面...

image

通常這樣就能解決 90% 的問題了。如果連這個秘密連線都被用掉了,那只剩另一招: TSDISCON.exe

 

image

TSDISCON: Disconnects a terminal session. 讚! 就是要這種東西... 用法很簡單,如果你有遠端 SERVER 的管理者權限,防火牆又沒把 NETBIOS 關掉,那麼可以這樣用:

 

  1. 先登入遠端 server
    NET USE \\MYSERVER /user:MYACCOUNT *
  2. 踢掉其它人
    TSDISCON 1 /SERVER:MYSERVER

BINGO,其中要注意一下就是 SESSION ID,也就是上面工作管理員 ID 那一欄。0 代表 console,其它就是額外的連線。不過除非你有另外買 LICENCE,否則 OS 內建的授權只有兩個連線,意思就是亂猜一通,1 跟 2 隨便挑一個砍了就好...

指令成功的話,被你挑中的連線就會中斷了。趁對方還沒重新連上去之前,快點連進去佔名額吧 :D



8/14/2008 2:09:16 AM

話題人物?

543 | 不爽

之前因為被盜文,除了被弄的不大爽之外,倒也沒什麼事,不過有人給我個 link,無意間發現對岸竟然有這麼篇文章...

(最原始的網頁不知為何被下架了,底下提供的 LINK 是目前還連的到的...)

URL: http://www.cnblogs.com/hullfqaz/archive/2008/07/28/1254315.html

 

image

 

看了又好氣又好笑... 沒想到對岸有網友在討論我碰到的事件啊,底下還真的有人回應批評百度...。前同事 在 MSN 跟我打屁,說這篇引用我的事例來討論的網站,也一樣沒有經過我的同意就轉貼,我的言論可能會引起兩岸對立,引發第三次世界大戰... Orz...

這次好一點,雖然也是原文照貼我的文章,至少是全文照貼,頭尾都留著,LINK 也留著,沒有很 "自動" 的翻成簡體中文...後來好奇用相關關鑑字再去 GOOGLE 找看看,多找到一篇:

URL: http://blog.const.net.cn/news/20080728/ec3b4ba924bde2b4.htm

 

不過這次運氣差了點,我的 IP 被封鎖不能看... 我應該沒有偉大到讓對方直接封我的 IP 吧?

image

 

不過人氣好像不大夠,搜了半天才搜到這兩篇... 看來要上頭條新聞還久的很.. 哈哈..






精選文章

RUN! PC 文章及範例下載
2010/07. 結合檔案及資料庫的交易處理
2010/05. TxF讓檔案系統也能達到交易控制
2010/04. 生產者 vs 消費者 - 執行緒的供需問題
2008/11. 生產線模式的多執行緒應用
2008/09. 用ThreadPool發揮CPU運算能力
2008/06. SEMAPHORE在ASP.NET的應用
2008/04. 以ASP.NET開發同步WEB應用程式

如何學好 "寫程式" 系列
#1. 該如何學好 "寫程式" ??
#2. 為什麼 programmer 該學資料結構 ??
#3. 進階應用 - 資料結構 + 問題分析
#4. 你的程式夠 "可靠" 嗎?

#5. 善用 TRACE / ASSERT

安德魯是誰?

Andrew Wu | Create Your Badge

我喜歡鑽研物件導向、軟體工程及作業系統等相關技術。我會在這裡發表我的研究心得,也當作我自己的學習筆記。


Recent comments

Comment RSS