3/11/2010 3:00:58 AM

[Tips] 用 “磁碟鏡像" 更換硬碟 #2, Windows 2003 的跨距磁區

543 | TROUBLE SHOOTING | 小技巧 | 技術隨筆

上一篇,介紹了如何用windows server内建的磁碟鏡像 (Mirror) 更換硬碟後,這次剛好有windows 2003,就拿來試了一下...

廢話就不多說了,先來看一下 2003 的步驟,再來看看跟 2008 / 2008R2 差在那裡:

  1. 原本的樣子,磁碟1 (8.00GB) 是舊的硬碟,磁碟2 (16.00GB) 是要換上來的新硬碟:
    image001

  2. 磁碟1 + 磁碟2 做成鏡像 (MIRROR):
    image002


    等待重新同步化 (Resync) 完成:
    image003


  3. Resync 完成後,移除鏡像:
    image004


    移除鏡像完成後的狀態:
    image005 

  4. 用 2003 的延伸磁區,把磁碟2後面沒用到的空間也併進 D: 來
    image006


    合併之後的狀態:
    image007 



整個步驟跟前一篇都差不多,主要都是靠鏡像(MIRROR)搬完資料後,再把新硬碟多的可用空間併進來。唯一的差別就在這裡: 2003 的 "延伸磁區" (英文: extend volume) 是可以把兩個硬碟,或是兩個分割區併在一起使用。在磁碟管理員還是看的到這些分割區的存在。這就有點像 JBOD (Just a Bunch Of Disks) 的模式。

不過在 2008 之後 (其實 vista 也是),這個功能就改了。原本的名字 extend volumn 現在改成直接擴大原分割區,把原分割區後面可用的空間都納進來,就像你砍掉再重建一個大的分割區一樣,只是資料會留著不會掉。當然有 extend 也有相對的 shrink volume, 這功能會把分割區縮小,騰出空間來讓你多切一個分割區...。而原本 JBOD 模式的功能,則改為 span disk, 用起來效果就如上圖一樣,當然你願意的話,也可以把多顆硬碟 (可以不同容量) 通通併成一個來使用,最多可以併到 32 顆硬碟...。

沒經過這次更換硬碟,還真沒發現 2008 / vista 總算內建這組 extend / shrink volume 的功能進來。雖然很陽春,不過已經很實用了,在過去這種動作是得搬出像 partition magic 這類軟體才能做的到,而這種東西每次用起來心裡都會毛毛的,深怕一不小心就把資料都給毀了...。

這篇小品文章就記到這裡,希望有幫到需要的人 :D



3/6/2010 2:54:22 AM

[Tips] 用 “磁碟鏡像" 無痛更換硬碟

543 | TROUBLE SHOOTING | 小技巧 | 技術隨筆 | 敗家

老是寫一堆像外星文,沒人看的懂的 multi-threading 文章,偶爾也來換換口味吧。前陣子把 SERVER 的兩顆 750GB HDD (RAID1) 升級了一下,升級成兩顆 1.5TB HDD (RAID1)。更換硬碟是小事,不過這個硬碟上有些服務,如網站,資料庫,還有一些重要資料,跟分享資料夾,想一想要更換也是挺囉唆的...。

想了幾個辦法,不過都不符合我既懶又挑毛病的個性... 原本考慮的更換方式有:

  1. 硬上... 新硬碟裝上去,檔案COPY過去,能停的服務就停掉,花一堆時間搬資料,然後再恢復原服務 (如 SQL、IIS等等)。至於比較麻煩的,像是目錄分享的,只好移掉再重建。中斷服務的時間就是從關機裝硬碟,一直到全部完成為止。
  2. 用 disk clone 的工具,如 true image / ghost 之類的軟體。不過這樣通常得停機做 clone, 750GB 也是要執行好一段時間,加上我這次買的是 Advanced Format 的硬碟,用這類工具會有效能的問題,事後還得校正回來… ouch, 算了...

想來想去,我用的是 windows server, 有內建的 Mirror set, 就拿來用一用好了。我真正作的是把 mirror set 的兩顆都升級,不過為了簡化說明,我底下的例子就只以替換一般的硬碟就好,反正道理是一樣的。

說穿了不值錢,就是用 mirror 的磁碟複製特性,加上 extend volume 的功能,我除了需要關機裝上新硬碟之外,其它包含資料複製的所有時間,原服務都不用中斷 (當然速度會慢一點),所有服務的設定也都不用修改,算是既無腦又防呆的完美方案... 只要簡單的按幾下滑鼠就可以達成我的目標。

 

直接來看看怎麼作的吧! 很簡單,先利用 mirror 把資料轉移到新硬碟... 然後中斷 mirror, 再用 extend volume 把磁區大小調大就可以收工了。來看分解步驟:

 

  1. 我原本的磁碟組態是長這個樣子 (圖我是事後用 VM 模擬的),其中 Disk 1 (8.00GB) 就是我要換掉的...
    image 


  2. 關機裝上一顆新的硬碟 Disk 2 之後,變成這樣 (Disk 2 (16.00GB) 是新的硬碟):
    image


  3. 把 disk1 / disk2 做成磁碟鏡像之後,就變成這樣:
     image


  4. 鏡像做好,Resync 完成後,就可以中斷鏡像了。中斷之後變成這個樣子:
    image 


  5. 目前為止,看來磁碟轉移已經完成了,剩下就是想辦法把後面的空間吃進來。接下來的就用 Extend Volume 括大 D: 的大小:
    image 
    image

 

之後就大功告成了。這方法不但簡單,而且整個過程中,全程 D:\ 都可以正常的使用。除了 (1) --> (2) 需要關機裝硬碟之外, (2) ~ (5) 全程,放在 D:\ 的 SQL DB,IIS 網站,還有 pagefile 通通都正常運作中。有了 windows server 的磁碟陣列還真是好用啊 :D

不過,事情也是有黑暗面的... 這個方法是有幾個小缺點啦...

  1. 被迫使用 "動態磁碟" :
    dynamic disk 其實不是什麼缺點啦,不過你要是會用到其它 OS,像 linux 之類的,或是用其它的磁碟管理軟體,可能就不認得了。這是缺點之一..
  2. 只有 windows server 可以使用:
    desktop os (windows 2000 pro, xp, vista, win7) 都只支援部份的磁碟管理功能,這作法關鍵的 mirror 是不支援的... 只能乾瞪眼 @@
  3. Extend Volume 只適用 windows 2008 以上的版本:
    記得 windows 2003 只支援到 span volume, 在 disk manager 裡還是會顯示兩個 partition, 只不過只會有一個磁碟機代號,容量會是兩個加起來的而已。一樣啦,是不會有什麼大問題,看起來不大爽而已 XD

偶爾換個口味,貼些小品文章,這邊我也不是很專業,有啥更好的作法也歡迎留 comment 啊 :D



12/19/2009 11:47:05 PM

[設計案例] 清除Cache物件 #2. Create Custom CacheDependency

543 | C# | CS | Microsoft.NET | MSDN | 小技巧 | 技術隨筆 | 物件導向

上一篇廢話了這麼多,其實重點只有一個,我這次打算利用 CacheDependency 的機制,只要一聲令下,我想移除的 cache item 就會因為 CacheDependency 的關係自動失效,而不用很辛苦的拿著 cache key 一個一個移除。

我的想法是用 tags 的概念,建立起一套靠某個 tag 就能對應到一組 cache item,然後將它移除。開始之前先來想像一下 code 寫好長什麼樣子:

透過 tags 來控制 cache items 的範例程式[copy code]
   1:      static void Main(string[] args)
   2:      {
   3:          string[] urls = new string[] {
   4:              "http://columns.chicken-house.net/",
   5:              // 共 50 組網址... 略
   6:          };
   7:          foreach (string url in urls)
   8:          {
   9:              DownloadData(new Uri(url));
  10:          }
  11:          Console.ReadLine();
  12:          TaggingCacheDependency.DependencyDispose("funp.com");
  13:          Console.ReadLine();
  14:      }
  15:      private static void Info(string key, object value, CacheItemRemovedReason reason)
  16:      {
  17:              Console.WriteLine("Remove: {0}", key);
  18:      }
  19:      private static byte[] DownloadData(Uri sourceURL)
  20:      {
  21:          byte[] buffer = (byte[])HttpRuntime.Cache[sourceURL.ToString()];
  22:          if (buffer == null)
  23:          {
  24:              // 直接到指定網址下載。略...
  25:              buffer = null;
  26:              HttpRuntime.Cache.Add(
  27:                  sourceURL.ToString(),
  28:                  buffer,
  29:                  new TaggingCacheDependency(sourceURL.Host, sourceURL.Scheme),
  30:                  Cache.NoAbsoluteExpiration,
  31:                  TimeSpan.FromSeconds(600),
  32:                  CacheItemPriority.NotRemovable,
  33:                  Info);
  34:          }
  35:          return buffer;
  36:      }
  37:  }

 

 

這段 sample code 做的事很簡單,程式準備了 50 個網址清單,用 for-loop 一個一個下載。下載的 method: DownloadData(Uri sourceURL) 會先檢查 cache 是否已經有資料,沒有才真正下載 (不過下載的細節不是本篇要講的,所以就直接略過了...)。

而主程式的最後一行,則是想要把指定網站 ( funp.com ) 下載的所有資料,都從 cache 移除。為了方便觀看程式結果,我特地加上了 callback method, 當 cache item 被移除時, 會在畫面顯示資訊:

image

由執行結果來看,果然被移出 cache 的都是來在 funp.com 的網址... 接著來看看程式碼中出現的 TaggingCacheDependecny 是怎麼實作的。相關的 code 如下:

TaggingCacheDependency 的實作[copy code]
   1:  public class TaggingCacheDependency : CacheDependency
   2:  {
   3:      private static Dictionary<string, List<TaggingCacheDependency>> _lists = new Dictionary<string, List<TaggingCacheDependency>>();
   4:      public TaggingCacheDependency(params string[] tags)
   5:      {
   6:          foreach (string tag in tags)
   7:          {
   8:              if (_lists.ContainsKey(tag) == false)
   9:              {
  10:                  _lists.Add(tag, new List<TaggingCacheDependency>());
  11:              }
  12:              _lists[tag].Add(this);
  13:          }
  14:          this.SetUtcLastModified(DateTime.MinValue);
  15:          this.FinishInit();
  16:      }
  17:      public static void DependencyDispose(string tag)
  18:      {
  19:          if (_lists.ContainsKey(tag) == true)
  20:          {
  21:              foreach (TaggingCacheDependency tcd in _lists[tag])
  22:              {
  23:                  tcd.NotifyDependencyChanged(null, EventArgs.Empty);
  24:              }
  25:              _lists[tag].Clear();
  26:              _lists.Remove(tag);
  27:          }
  28:      }
  29:  }

 

30行不到... 其實程式很簡單,TaggingCacheDependency 繼承自 CacheDependency, 額外宣告一個靜態的 Dictionary<string, List<TaggingCacheDependency>> 來處理各個標簽及 TaggingCacheDependency 的關係,剩下的就沒什麼了。呼叫 DependencyDispose( ) 就可以通知 .NET Cache 機制,將相關的 cache item 移除。

用法很簡單,當你要把任何物件放進 cache 時,只要用 TaggingCacheDependency 物件來標示它的 tag:

把物件加進 Cache, 配上 TaggingCacheDependency ...[copy code]
   1:  HttpRuntime.Cache.Add(
   2:      sourceURL.ToString(),
   3:      buffer,
   4:      new TaggingCacheDependency(sourceURL.Host, sourceURL.Scheme),
   5:      Cache.NoAbsoluteExpiration,
   6:      TimeSpan.FromSeconds(600),
   7:      CacheItemPriority.NotRemovable,
   8:      Info);

在這個例子裡 (line 4), 直接在 TaggingCacheDependency 物件的 constructor 上直接標上 tags, 在此例是直接把網址的 hostname, scheme 兩個部份當作 tag, 未來就可以依照這兩種資訊直接讓 cache 裡的相關物件失效。

而要下令讓 Cache 內有標上某個 tag 的 cache item 失效,只要這行:

 

將標為 "funp.com" 的 cache item 設為失效的 cache item[copy code]
   1:  TaggingCacheDependency.DependencyDispose("funp.com");

 

結果就會如同上面的程式範例一樣,還留在 cache 的該網址下載資料,在這一瞬間通通都會被清掉...

 

用這種方式,是不是比拿到 key 再去呼叫 Cache.Remove( key ) 的方式簡單多了呢? 同時也能夠更快速的處理複雜的移除機制。其實運用 tagging 的方式只是一例,需要的話你也可以設計合適的 CacheDependency 類別。

以下是本篇文章的兩個附加參考檔案:

Download File - URL清單



12/19/2009 4:29:29 AM

[設計案例] 清除Cache物件 #1. 問題與作法

ASP.NET | C# | CS | Microsoft.NET | MSDN | 小技巧 | 技術隨筆 | 物件導向

每次心裡有什麼好點子想寫出來時,第一關就卡在想不出個好標題... 想來想去的標題,怎麼看就是既不顯眼又不聳動... 果然是個老實的工程師性格 =_= ...  這次要講的,是 .NET HttpRuntime 裡提供的 Cache 物件的操作心得。這個東西我想不用我多作介紹,大家都用到爛掉了吧? 不過好用歸好用,有個老問題其實一直困擾著我很久了...

" 我該怎麼手動的把某個物件從 cache 裡移除? "

老實說,這問題蠻沒水準的... 老叫別人要翻 MSDN,我自己怎麼沒翻? 不不... 容我花點篇幅先說明一下問題。Cache物件,是個典型的 Dictionary 型態的應用 (雖然它沒有 implement interface: IDictionary… ), 透過 key 就可以拿到 cached item. 要從 cache 裡移除某個 item, 簡單的很,只要用 Remove 這個 method, 一行就搞定了:

從 key 移除指定的 cache item[copy code]
   1:  HttpRuntime.Cache.Remove(“cache-key”);

別小看這一行,實作起來障礙還不少。首先,你得額外去記著 cache key 的值。當你要移除的 cache item 有多個的時後,或是移除的 items 之間的關係有點複雜時,這些 code 就不怎麼漂亮了。下一個問題是:

" 我該如何得知所有存在 Cache 內的 keys 有那些? "

這個問題單純的多,那些把 intelligent sense 當購物網站的人 (平常不看文件,只會按下 . 然後挑個順眼 method 來用的人),可能這次就碰壁了... Cache 物件不像一般的 Dictionary 一樣,有提供 Keys 這樣的 property ... 它藏在 GetEnumerator 這 method 內,它會把所有的 keys 給巡一遍,你需要所有的 keys 的話,可以這樣用:

跑過 cache 裡每一個 key[copy code]
   1:  foreach (string key in HttpRuntime.Cache) { 
   2:      // … 
   3:  }

不過這樣的風險也是蠻高的,誰曉得你拿到 key 後的下一秒,這個 cache item 還在不在 cache 內?

 

 

 

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

本文正式開始! 哈哈,前面那一段只是廢話 + 碎碎唸,現在才是正題。前面想表達的只是,因為 cache 的不確定性 (資料隨時都會被 remove), 操作起來變的要格外小心, 即使它用起來像一般的 Dictionary 一樣。

我舉個案例,來說明我應用 cache 的情況。假如我想實作一個簡單的 web browser, 透過網路下載資源是很慢的動作,每種 browser 都會有某種程度的 cache 機制。我們就拿 Cache 物件替代 IE 的 "temporary internet files” 目錄吧。這時很簡單,只要用 URL 當作 KEY,下載的 content 就當物件塞進去就好...

不過事情沒那麼簡單。如果程式運作了一陣子,我想提供使用者手動清除 "部份" cache 的功能的話,那該怎麼辦? 我舉幾種情況:

  1. 從 cache 裡刪除所有從某個特定網站 (如: columns.chicken-house.net) 下載的資料
  2. 從 cache 裡刪除所有特定類型的資料 (如: content-type 為 image/jpeg 的圖檔)
  3. 從 cache 裡刪除所有透過特定 protocol (如: https) 下載的資料

這樣的要求應該不算過份吧? 用前面提到的兩種作法,你會想哭吧 XD .. 用這些基礎,你大概只能選這幾種作法 (各位網友有好作法也記得提供一下):

  1. 自己另外管理所有下載過的 URL, 用盡各種適合的資料結構,讓你可以順利的挑出這些 match 的 key, 然後移除它。

    缺點: 都作這麼多,你乾脆自己重寫個 cache 機制好了... 何況時間一久,你管理的 key, 那些對應的資料搞不好老早就通通從 cache 裡清掉了...
  2. 聰明一點,用 regular expression … 從 GetEnumerator( ) 一筆一筆過濾出要移除的 URL, 然後清掉它...

    缺點: 這作法只會檢查還留在 cache 內的 URL,不過這樣的 cache 隨便也有成千上萬個,每次都要 looping 掃一次實在不怎麼好看... 有違處女座有潔癖的個性...

 

這些方法 code 寫起來實在不怎麼漂亮,我就不寫 sample code 了,請各位自行想像一下寫起來的樣子。抱歉,如果你用的正好是上面的作法... 那請多包含... :D   這些都是 workable 的作法,但是看起來就是沒什麼設計感;程式可以動,不過就效能、簡潔、可讀性、美感來看,就是覺的不夠精緻 @@。跟朋友討論到這個問題時,我想到一個爛主意...

" 用蠢方法,這些 cache item 先分好類,每一類去關聯一個檔案,設 CacheDependency … 要清掉時去 touch 一下這個檔案,一整組的物件就會自動被清出 cache 了…。”

老實說,我覺的這是個既聰明又愚蠢的作法。聰明的是它很漂亮的解決我要如何移除某一群 item 的問題...,愚蠢的是這種單純程式內可以解決的事,竟然要繞到外面不必要的 file system I/O 動作... 而這通常是最慢的...

 

--

咳,寫太晚,實際的程式碼明天待續...



10/3/2009 2:24:00 PM

[設計案例] 生命遊戲 #6, 抽像化 (Abstraction)

C# | 小技巧 | 技術隨筆 | 物件導向

原定 #4 就提到的 "抽像化",竟然被我連拖兩期,拖到 #6 才提到它... 人老了果然比較囉唆... 在前面的幾篇,重點都在如何 "具體" 的描述 "生命遊戲" 裡的細胞。不過現在要把這程式擴大到能容納各種不同的生物,先作好抽像化的工作是必要的...。

一般物件導向所指的 "抽像化",是指你對某些事物的一般概念。比如有人問你:

"你會開車嗎?"

你腦袋裡想的應該是一般印像中的車子,有方向盤,排檔打下去,油門踩了就會前進,煞車踩了就會停下來...,這就是你對 "車子" 的抽像化。你不會去管車子是什麼牌的,什麼顏色,是二門跑車,或是休旅車之類的細節... 而你 "會" 開的車,也不會因為這些細節,有太大的不同。

這樣的抽像化概念,套用到考駕照這件事來說,你只要知道方向盤,油門等等的用法,同時也練習過,能正確的控制教練車,通過測驗,監理所就會發張駕照給你,證明你會開一般的車子。就算是在你學會開車後十年才上市發表的新車也是一樣。

看起來沒什麼了不起的描述,在電腦的世界裡可不是這麼一回事。Microsoft Word 1.0 想要順利開啟 Microsoft Word 6.0 的檔案,大概想都不用想,因為 1.0 版設計之初,有太多 6.0 版的變化是無法事先預料的,自然無法設計出能正確操作的程式,這現像在電腦的世界很正常。不過如果你兩年前考到的駕照,碰到兩年後的新車你就不會開了,甚至監理所還要求你重考張新的架照... 那這駕照等於一點用都沒有。中間的差別,就在於駕駛者對於開車的認知,跟實際的車子,中間是隔著一層 "抽像化" 的概念,而只要能掌握這抽像化的定義,就能順利操作未來的車種。

因此物件技術不斷的想要模擬這樣的關係,就發展出繼承這樣的方式,來表達這個概念。先用一個類別 (base class) 或是介面 (interface) 來表達這個 "抽像化" 的概念,而不表達細節。其它要跟它互動的程式,只能透過這個抽像化型別來溝通,而其它的細節或實作,則被藏在裡面,或是衍生類別。中間的故事我就不再多說了,再說我就直接去寫 OOP 的書好了 =_=,有興趣可以參考這本經典 [世紀末軟體革命],有復刻版喔。套用到我們的 "生命遊戲" 裡,要定義的就是 "世界" 如何跟 "生命" 互動? 之間的關係是什麼? 另外就是 "生命" 有各種不同的型態,所有的 "生命" 型態是否都能順利的在同一個 "世界" 裡生存?

先試著用簡單文字來描述吧。在我們的定義裡,世界是個 M * N 的棋盤,每一格都能放一個生物。每個生物有自己的狀態 (生/死),也會隨著時間與環境的不同,讓生物的狀態產生變化。畫成 UML 的 class diagram, 大概就像這樣 (手邊沒工具,用 power point 大概畫一下… Orz):

image

我們在撰寫程式時,就必需思考題目中講到的生物各種特性,那些是所有的生命共有的特色? 這部份要把它定義在 Life … 另一部份是某種細胞特有的,則要放在衍生類別 Cell 裡。而世界必需要能跟生命作適當的互動,讓生命的進行能繼續下去。這樣的架構好處是,未來如果有第二種 Cell 或是其它的生物,只要是從 Life 繼承下來,都能很順利的在 World 裡活著,因為物件導向技術的 "抽像化" 概念,保證這樣程式的可行性。

好,我們就以需要跟 World 接觸跟互動的部份為主,把原程式的 Cell 抽離出來,放到它的上層類別 Life 裡。這也是物件技術裡常提到的 "generalization" (一般化),越一般的特性要越往上層類別移動,而越往下就是 "specialization" (特殊化),底層的類別要去實作特殊的部份,或是特有的細節。

先把原程式作好調整吧。原 Cell 的程式碼,部份被搬移到 Life, 同時這兩個類別有了繼承關係,如下:

 ClassDiagram1

Life 的部份,定義了所有 Life 都該表達出來的特性,也就是我們對於 Life 的認知,都應該描述在裡面,像是 Life 活在 World (CurrentWorld) 裡,會有它的座標 (PosX, PosY), 也會有它在這個棋盤內顯示的方式 (DisplayText) 等。而跟 World 互動的方面,Life 則透過 GetNextWorldTask( ) 來讓 World 來讓 Life 驅動它生命的進行。

在 World 的這邊,不管是那種 Life 衍生類別的物件,一律都當成 Life 的 "抽像概念" 來操作。這樣的優點,在還不曉得未來這世界到底還有多少種不同的 Life 會在裡面生活時,主要程式就能開發了。未來 Life 可以一直擴充,衍生出多種不同的 Life 子類別,而 Life / World 之間的互動及規範,則可以完全不用修正。

接下來就要讓這遊戲的規則,變的更真實一點了。實際的情況下,應該是我們已經知道會有那些不同的生物,經過歸納 (一般化及特殊化) 之後,可以設計出我們需要的類別架構。不過實際寫起程式來可沒這麼好命 (就像 USER 永遠不會一次給你完整確定的需求一樣),很多時後你得去 "猜" 或是 "假設",因此跟本沒有 "一般化" 這回事,你得預先去猜測未來要應付什麼問題,而在細節都還不清楚時就先定義出上層類別。

我們開始來試看看,我們定義的夠不夠抽像吧! 如果助教看你這麼快就把生命遊戲的作業交出來,覺的很沒面子,想把題目變難一點,加上有病毒感染的情況。於是原題目的四條規則追加一條,變成這樣:

  1. 孤單死亡:如果細胞的鄰居小於一個,則該細胞在下一次狀態將死亡。
  2. 擁擠死亡:如果細胞的鄰居在四個以上,則該細胞在下一次狀態將死亡。
  3. 穩定:如果細胞的鄰居為二個或三個,則下一次狀態為穩定存活。
  4. 復活:如果某位置原無細胞存活,而該位置的鄰居為三個,則該位置將復活一細胞。
  5. 感染:正常的細胞有 ( 1 + 受感染的鄰居數量 x5 )% 的機率受到病毒感染。已感染的細胞在 3 次狀態改變後會痊癒。受感染的狀況下,有 10% 的機率會死亡。

我們的程式該怎麼配合它改變? (對,機車的 USER 就都是這樣臨時修改規格...) 先來看看執行的結果,畫面上已經分的出來活著的 Cell 跟受感染的 Cell ... 除了看到 Cell 活著與死亡的變化之外,也看的到病毒擴散的狀況是怎麼樣。執行的畫面如下:

image

圖例: ◎受感染的細胞,●活著的正常細胞,○死亡的細胞

接著,來看看改版後的程式碼:

改版: 會受到病毒感染的 Cell 程式碼[copy code]
        public bool IsInfected        {            get { return this.InfectedCount > 0; }        }        private int InfectedCount = 0;        public override string DisplayText        {            get            {                if (this.IsAlive == true) return "●";                else if (this.IsInfected == true) return "◎";                else return "○";            }        }        protected override IEnumerable<TimeSpan> WholeLife()        {            yield return TimeSpan.FromMilliseconds(_rnd.Next(800, 1200));            for (int index = 0; index < int.MaxValue; index++)            {                int livesCount = 0;                int infectsCount = 0;                foreach (Cell item in this.FindNeighbors())                {                    if (item.IsAlive == true) livesCount++;                    if (item.IsInfected == true) infectsCount++;                }                bool? value = _table[this.IsAlive ? 1 : 0, livesCount];                if (value.HasValue == true)                {                    this.IsAlive = value.Value;                }                if (this.IsInfected == true)                {                    this.InfectedCount--;                    if (this.InProbability(10) == true) this.IsAlive = false;                }                else                {                    if (this.InProbability(1 + infectsCount * 5) == true) this.InfectedCount = 3;                }                yield return TimeSpan.FromMilliseconds(_rnd.Next(800, 1200));            }            this.Dispose();            yield break;        }
   1:  public bool IsInfected
   2:  {
   3:      get { return this.InfectedCount > 0; }
   4:  }
   5:  private int InfectedCount = 0;
   6:  public override string DisplayText
   7:  {
   8:      get
   9:      {
  10:          if (this.IsAlive == true) return "●";
  11:          else if (this.IsInfected == true) return "◎";
  12:          else return "○";
  13:      }
  14:  }
  15:  protected override IEnumerable<TimeSpan> WholeLife()
  16:  {
  17:      yield return TimeSpan.FromMilliseconds(_rnd.Next(800, 1200));
  18:      for (int index = 0; index < int.MaxValue; index++)
  19:      {
  20:          int livesCount = 0;
  21:          int infectsCount = 0;
  22:          foreach (Cell item in this.FindNeighbors())
  23:          {
  24:              if (item.IsAlive == true) livesCount++;
  25:              if (item.IsInfected == true) infectsCount++;
  26:          }
  27:          bool? value = _table[this.IsAlive ? 1 : 0, livesCount];
  28:          if (value.HasValue == true)
  29:          {
  30:              this.IsAlive = value.Value;
  31:          }
  32:          if (this.IsInfected == true)
  33:          {
  34:              this.InfectedCount--;
  35:              if (this.InProbability(10) == true) this.IsAlive = false;
  36:          }
  37:          else
  38:          {
  39:              if (this.InProbability(1 + infectsCount * 5) == true) this.InfectedCount = 3;
  40:          }
  41:          yield return TimeSpan.FromMilliseconds(_rnd.Next(800, 1200));
  42:      }
  43:      this.Dispose();
  44:      yield break;
  45:  }

細節我就不多介紹了。這裡的重點是經過抽像化的動作後,把 Life / Cell 之間的邏輯做明確的劃分。World 的類別程式碼完全沒有出現任何有關 Cell 的 Code, 只有出現 Life 而已。除了在主程式 GameHost 有這麼一段,明確的把 Cell 建立起來,把它放進 World:

建立世界的程式碼[copy code]
        static void Main(string[] args)        {            int worldSizeX = 30;            int worldSizeY = 30;            World realworld = new World(worldSizeX, worldSizeY);            Random _rnd = new Random();            for (int x = 0; x < worldSizeX; x++)            {                for (int y = 0; y < worldSizeY; y++)                {                    Cell item = new Cell();                    realworld.PutOn(item, x, y);                }            }            // ...
   1:  static void Main(string[] args)
   2:  {
   3:      int worldSizeX = 30;
   4:      int worldSizeY = 30;
   5:      World realworld = new World(worldSizeX, worldSizeY);
   6:      Random _rnd = new Random();
   7:      for (int x = 0; x < worldSizeX; x++)
   8:      {
   9:          for (int y = 0; y < worldSizeY; y++)
  10:          {
  11:              Cell item = new Cell();
  12:              realworld.PutOn(item, x, y);
  13:          }
  14:      }
  15:      // ...

這樣的作法,其實已經引含了 "動態聯結" 的特性了。在開發主程式的階段 (指 World / Life 這兩個主要的 class), 都還沒有 Cell 的相關細節,而事後執行的程式碼卻可以依照 Cell 裡的邏輯來執行。這代表了我們不需要改主程式的設計,就能不斷的加入新的規責,甚至是新的生物進來一起運作。

如何? 物件技術的 "抽像化" 能力,的確很有效的解決了這樣的變化需求。下一篇會沿用一樣的架構,但是執行的範例會完全不一樣 (這次不用細胞了,直接用草原上的生態: 草、羊、虎) 的生命及規則,來套進這個框架,看看它能怎麼模擬出一個新的生態系統。

這樣的架構可以應付未來未知的變化,只要你的抽像化概念不變的前題下都沒問題。這種保留彈性,卻又不用在 design time 去多做不必要的實作,才是物件技術強大的地方。我舉個反例,很多剛入行的軟體工程師,你給他一個需求,他會想太多... 一個簡單的輸入 1 + 1 要顯示 2 的結果,他會這麼想:

USER 需不需要列印啊? 我先把這需求放進去好了,然後加個 config 預設關掉它,以免以後需要我還得大改程式...

只能算 1 + 1? 如果以後 USER 要算 3 * 5 怎麼辦? 好吧,我把 + 用一個 mode 來代表好了,以後 USER 需要括充為支援 +-*/ 就不用大改程式了...

...

碰到這些狀況,我只能誇獎這位年青有為的程式設計師一句話:

"你很認真... 辛苦了..."

不過我心裡會苦笑... 只不過要你寫個 1 + 1 = 2,搞這麼一大包? 多作考慮,預留未來可能需要的功能,不是件壞事。不過既然是未知的需求,你又如何保證你能夠正確的 "預知" ,然後進一步 "預留" ? 何況這些多做的需求,未來真正會用上的有多少? 用不到的話,只是開發成本的浪費,及讓你的架構複雜性提高,維護的困難增加而已。物件技術真的解決的了這種問題嗎? 下一篇的目標,我們會定在不修改 World / Life 的設計為前題,把生命遊戲的模擬內容換成草原的生態模擬。敬請期待續集 :D

 

 

 

 



9/29/2009 12:44:02 AM

電腦時鐘越來越慢...

小技巧 | 技術隨筆

最近這兩個禮拜,很扯... 我的手機時鐘越來越不準,竟然每天會晚個幾分鐘,不到兩個禮拜,竟然跟正常時間比起來已經差了廿分鐘...

天那,這什麼時代了,一個五十塊的電子錶都比我這隻五為數價位 (幾年前的價位啦 =_=) 的手機準時... 怪的是,白天再公司又是準的 !? 越看越怪,難不成這時代,連看個時鐘都得先 DEBUG 一番... 今天就花了點心思追一下問題 @@

首先,原來慢的不是我的手機,而是我家裡的 PC,因為手機插上 USB 充電 + SYNC,會順便對時,就這樣誤差越來越大了。不過 PC 不是都會上網對時嘛? 後來就再繼續追下去...

繼續追下去,家裡 PC 是跟家裡的 SERVER (domain controller) 對時的... 原來時間慢的是家裡的 SERVER... 不過問題還是一樣啊! 現在一直都連上網的 SERVER 怎麼可能會這樣?

因為每次重灌,一堆帳號就要重建,很麻煩,所以上個月重灌 SERVER時,就順手在 VM 裡裝了台 SERVER,當作 Active Directory 的 domain controller (Guest OS)… 而 SERVER 本身 (Host OS) 才是拿來做 NAT / RRAS / FILE SERVER 等服務...

這樣的架構,機車的地方就在於: Hyper-V 本身 VM 會跟 Host 做 time sync ... 而 Host 有加入 AD, Host 又會跟 Guest 同步時間,Hyper 又替 Guest 跟 Host 同步時間... 每次誤差一點,幾週下來就變這樣了 @@ 害我的手機莫名奇妙就晚了快半小時...

image

 

果然,這選項移除後,就一切正常了 @@,頂多就讓 DC 脫褲子放屁,到外面的 server 去對時吧... 嗯,這年頭,真的什麼怪事都會發生... 還好這次有抓到問題,哈哈... 這篇就給跟我一樣宅的人參考吧 :D



8/16/2009 2:43:27 AM

HVRemote (Hyper-V Remote Management Configuration Utility)

543 | MSDN | 小技巧

被這東西搞了半天,過了幾個月後發現有善心人事寫了個工具,今天看到了特地來記一篇... 免的以後又忘了 @@

話說 Microsoft 從幾年前開始被一堆 security 的問題苦惱後,決定所有產品都把安全視為第一優先... 這是件好事啦,不過為了 security 問題,真的會把 MIS 及 DEV 的相關工作難度加上好幾倍... 今天這個就是一例: 在沒有 AD 環境下,如何遠端的管理 Hyper-V server ?

之前把家裡的 SERVER 升級到 Windows Server 2008 + Hyper-V, PC 升級為 Vista 後,當然很高興的抓了 Hyper-V 的遠端管理工具回來裝。想說大概跟以往的 MMC 一樣,輸入 SERVER 的資訊,帳號密碼打一打,就可以用了...

事情當然沒這麼簡單,不然就沒這篇了... 直接使用的結果當然只是丟個沒權限之類的訊息。GOOGLE 找了一下解決方式... 找到這文章 (有五篇,別以為很辛苦的把它照作就結束了,還有 part 2 ~ part 5 @@):

http://blogs.technet.com/jhoward/archive/2008/03/28/part-1-hyper-v-remote-management-you-do-not-have-the-requested-permission-to-complete-this-task-contact-the-administrator-of-the-authorization-policy-for-the-computer-computername.aspx

 

細節就不講了,要調整的步驟還真它X的多... 先在 CLIENT / SERVER 都建好帳號,防火牆要允許 WMI,DCOM... 再設定 WMI 相關的權限給指定的帳號,還有後續一堆安全相關的設定要開... 最後搞了半天,真的成功了,不過... 最近趕流行,把 Vista 換成 Windows 7... 真糟糕,這堆步驟又要來一次 @@

這次又找了一下解決方式,還是一樣有這堆設定要改,不過跟幾個月前找到的同一個 BLOG,版主真是個好人,他把他整理出來的步驟寫成了個工具: HVRemote.wsf … 沒錯,就只是個 script 而已,不過它可不簡單。先看一下它的網站:

http://blogs.technet.com/jhoward/archive/2008/11/14/configure-hyper-v-remote-management-in-seconds.aspx

http://code.msdn.microsoft.com/HVRemote

http://technet.microsoft.com/en-us/library/ee256062(WS.10).aspx

作者把上面那一大串的步驟都寫成 script 了,你只要把這 script 抓下來,放到 client / server 都執行一次,就搞定了 :D  真專業,還有一份很完整的操作說明 PDF 檔... 一定要推一下這個工具 (Y)

 

附帶提一下,Hyper-V 遠端管理工具是透過 MMC 來執行的,但是我喜歡像 Remote Desktop 那種簡單的作法,只要開個連線工具,驗證過之後就可以遠端桌面這樣... Hyper-V 也有提供這樣的工具。只要裝好管理工具,你的電腦就會有這檔案:

C:\Program Files\Hyper-V\vmconnect.exe

image

 

搞什麼,連登入視窗都弄的跟 Remote Desktop Client 一模一樣,有時不小心還真會弄錯 =_=

開起來後就是大家熟悉的 Hyper-V 遠端管理的畫面了。這工具只是讓你省掉從 MMC 去 connect VM 這些步驟而以,像 RDP 一樣開了就能用:

image

 

當然透過這工具,上面那堆設定步驟也要照做才會通啦,只是順帶提一下這個 tips 而已。有了 HVRemote 這工具,要設定遠端管理 Hyper-V VM 就更輕鬆了,有需要的人就參考看看吧!



4/20/2009 3:46:36 AM

個人檔案 + 版本控制...

543 | 小技巧 | 技術隨筆

自從過年時換了 SERVER 的作業系統,加上過年前 NOTEBOOK 掛掉換 X40 + SSD 之後,這幾個月都陷在東換換西調調的狀態中 @_@, 好在換了 2008 之後,有 Hyper-V 的幫忙,問題簡化不少...。不過今天要講的倒是很不起眼的小東西: SVN (Subversion)

SVN 這種版本控制系統,通常是用來作程式碼的版本管理。也對啦,除了軟體開發之外,其它場合好像也不大需要這麼複雜的版本機制。不過這類系統弄多了,平常在非軟體開發的場合,也發現其實很多時後都有檔案版本問題要處理。像是平常的文件 (WORD),簡報 (PPT) 等等,都會作好一份通用的,碰到 A 客戶就改一改拿來用,B 客戶再改一改... 這不就是 brench / merge 之類的問題嘛? 所以我一直在找這樣的 solution,看看有沒有適合一般使用的。不過到現在,也換了好幾種作法,歷年來試過的作法有好幾種:

  1. VSS (Microsoft Visual SourceSafe 5.0)

    這個有用過的人,看版本號碼就知道有多古老了… 不過真正在用是 6.0 版開始。因為工作上會用的到,就順便拿來用了。它的好處是很簡單,搞懂它的邏輯就很容易上手。架設也簡單,完全是 File Based, 不需要架設專用的 Server。不過這也是後來換掉它的原因之一。

    它的使用方式,是以嚴格的控制為主要邏輯。什麼意思? 意思是你不能隨意更改檔案,要開使改檔案之前,要先 check-out 才能開始改。這樣的邏輯就是要避免未來一連串的版本衝突 (conflict) 及合併 (merge) 帶來的問題。 以軟體開發的角度來看,這樣的作法還不錯,整個團隊的開發是值得這樣作的。不過拿來管理個人檔案的話,就太過頭了。個人檔案不大會發生 LOCK 的問題,就是我改你也改,最後存檔總會有一個人的資料被蓋掉... 不過,如果我是大老闆,有十幾個秘書在幫我打雜的話就難說了 [H]

  2. VSS (Windows Volume Shadow Copy Service)

    Visual Source Safe 用了之後,發現障礙多於它帶來的優點 (以處理一般文件而言)。主要的缺點是,VSS 透過網路 / Internet / VPN 使用的速度實在是龜到可以,雖然後來 Microsoft 推出了 LAN Boost Service (還是很慢),也另外推出了 HTTP / Web Service 的存取方式 (只能透過 Visual Studio) 速度也不快。另一個缺點是一定要先開 VSS Explorer / Visual Studio, 我不過只是想開個 WORD 檔啊...

    所以後來換了另一個角度找 solution, 就試用了 windows 2003 內建的 VSS (Volume Shadow Copy Service), 替代版本控制用的軟體。它是做在 File System 層次上的機制,用了 Copy On Write (COW.. 這是縮寫,不是在罵人...) 的方式,做版本的差異控制。因此只要把檔案放在開啟 VSS 的磁碟機,完全不用更改任何使用習慣..。

    但是太自動的東西還是不適用。這種作法主要的問題在於版本太不精確了。VSS 仰賴定期作快照 (snapshot) 來作版本的管理。定期做的快照,留下來的版本很可能是無意義的,你也無法針對特定檔案的特定版本作註記 or 回複... 另外自動的快照也無法選則那些檔案要進版本,那些要退出。總之一切全自動,沒有什麼好選的。很簡單,但是功能也很有限。

    不過即使如此,一般情況下也夠用了,操作也夠簡單,當作第二種保護機制也不錯。這個 solution 我也用了好一陣子...。

  3. TFS (Team Foundation Server)

    老實說,連一般小型軟體開發,用到 TFS 都太肥了一點,自己的檔案管理用到這個真是太離譜了... 哈哈,因此這個 solution 只是閃過念頭而已,跟本沒實際裝起來試過。用這個方案,工具會是個大問題... 用的時後得開個 Visual Studio, SERVER 還得裝一大票軟體 (IIS, TFS, SQL + Reporting, SharePoint Team Service, AD…)

  4. USB DISK + PortableApps

    其實這個算不上是個 SOLUTION,只不過順便把它列上來,待會說明用。某次無意間,同事告訴我 PortableApps.com 這個工具,它是個灌在 USB 隨身碟上面的工具 & 一些綠色軟體,有自己專用的 "開始" 選單,方便你插上隨便一台電腦,就把它當作你自己的 PC 一樣使用... 老實說還不錯用 (Y),我就試著用一陣子,把所有個人相關的資料都移到上面了。現在的工作環境有點複雜,公司一台 PC,家裡一台 PC,偶爾還需要用 notebook 去客戶那邊簡報 (咳,就是我那台只有 8GB SSD 的 X40,正好沒地方放檔案)

    用了一陣子還不錯,不過碰到的又是很常見的問題: 檔案掉了怎辦? 備份問題? 讀寫速度問題? Flash Disk 寫入次數限制問題... 不外乎常備份,每天一份 ZIP 檔,用苦力作好版本控制…


  5. USB DISK + SVN

    最後,就是現在用的方案了... 主要是補 (4) 的不足: 一般的定期 ZIP 備份就跟快照一樣,事後要追出變更其實很麻煩,每次變更想加個註解又更麻煩了。當然搭配 Visual Source Safe 這種工具,把 Working Folder 指到 USB DISK 上就可以兩全齊美了。

    不過使用便利則是另一個問題,我希望能夠找個無腦一點的工具,不需事先 check-out (lock) 的動作就可以開始編輯,改完再決定 check-in (commit) 或是 undo (revert) 的模式最好。用了 USB DISK 就是希望能拔來拔去,如果必需配合特定工具 & 要即時連上 SERVER,那就有點麻煩... 想看看,當我 USB DISK 插到 NOTEBOOK 帶到客戶那邊去,都按兩下打開 PPT 在簡報了,臨時要改幾個字,用 VSS 的話,我得關掉 PPT,打開 VSS,CHECK-OUT,打開PPT,修改...

    所以後來的首選就變成 SVN 了。SVN 因應 internet / open source project 的開發模式,採取的就跟 Microsoft 是不同的策略,就是先改再說。SVN 賭你不會多人同時編同一個檔案,就算會,也不會編同一段 code … 真的碰到就再人工處理吧。另外它支援各種不同的 protocol, 透過 internet 這種連線來使用,效能也不會很糟糕...

 

到目前為止,我用的就是 (5) USB DISK + SVN 這種 solution, 老實說越用越覺的它不錯 (Y)。SVN 我還是個新手,應該輪不到我來介紹他的特色吧 XD,不過我還是挑幾個特別的地方介紹一下,這些是我用它的主要原因啊...

  1. 操作邏輯合適

    SVN 是 CVS 的接班人,它先天就繼承了 CVS 的特性: 就是適用 open source 的開發團隊。Open Source 的開發團隊跟一般的開發團隊有什麼差別? 一般商業開發都是正職的工作,很固定且很密集的進行開發及變更,因此像 Microsoft Solution (VSS / TFS) 那種要事先 lock 的機制會比較有效率。不過 open source project 就反過來,業餘的比例比較高,而且人都散布在世界各地,如果真正用 LOCK 的機制大概會哭出來吧...。我要改的檔案被你 LOCK 住了,不過我又不知道你是誰? 除了等就沒辦法了...。

    因此 SVN 先天就是以這樣的觀點來設計: 你先改了再說,改完就 commit 。反正只要沒人跟你改同一個檔案就沒事... 如果運氣真的不好,那這個人不要跟你改同一段 code 也沒事,直接 merge 就好... 只有真的很背的時後,有人跟你改同一段,那麼後 commit 的人就要負責處理 merge 的問題。不過機率很低嘛 (沒錯,尤其是只有我自己用的時後),你可以不用管它...

    過去用 VSS 常碰到這種情況: 原本只是開個文件起來看 (READ),跟本沒想要去改它,就沒有先作 check-out 的動作了。不過看到一半發現內容有誤,想要修正時... 問題就來了。以 WORD 來說,已經開起來才去 check-out 檔案的話,WORD還是會認為檔案是唯讀的... 除非你關掉 WORD 再開啟一次才有用。不過這麼一來思緒都被打斷了...。

    當然,還是一樣,正規的開發動作還可以要求,一般的文書處理要求到這樣就有點過頭了。因此 SVN 這樣的邏輯就佔了點優勢,我最常碰到的案例就是: 要出門開會,把 USB_DISK 拔出來帶走。開會過程中 (在外面,沒有網路連線) 修修改改 PPT 的內容,回到公司後直接在 NB 或是把 USB DISK 插回 PC,再用 SVN 作 commit 的動作...。

  2. SERVER 的資訊跟著目錄

    有些工具 (像是 TFS),你的工作目錄對應到那個 SERVER,是工具在維護的 (TFS 的 workspace),這時搭配 USB DISK 可能會在不同的電腦 (可能是我的 PC,也可能是我的 NOTEBOOK,甚至是帶回家裡用)。一般把設定綁在工具上的作法就很頭痛,因為好幾台都要設成一樣的,而且 USB DISK 還有可能每次的磁碟機路逕都不大一樣...

    我用的工具是小烏龜 (TortoiseSVN),它的設定就是在每個目錄下放個 .svn / _svn 的子目錄,檔案總管按右鍵叫出 SVN 的選單後,藏在裡面的設定就自動套上來了。這種操作模式,剛好對於我的用法 (USB_DISK) 很方便...

  3. 更精確,更有效率的 "備份機制"

    現在隨身碟廠商,都很愛在商品上加一些小工具,有的有壓縮,有的有密碼保護... 不過 USB DISK 很容易掉,所以所有廠商都不會忘記附上一個備份工具。連我前面介紹的 PortableApps.com 都有附一個 ( 7-ZIP + SHELL )。不過這些備份工具都有個通病... 它就真的只是 "備份" 而已,是讓你心安的。使用時機是你自己要勤勞點,記得每天按 BACKUP。要還原回來,通常就是整支 USB DISK 的內容都還原回來了,如果你只想要還原某幾個檔,或是只要查看過去備份的某個檔,那你得點好幾下滑鼠,甚至是要把整個備份解開才看的到。

    另一個備份問題是,每次都是 FULL BACKUP ... 雖然有些工具作的比較好,有差異備份 ( PortableApps.com 就有提供 7-ZIP 的差異備份),不過不還原還好,一旦要把舊資料撈出來也是很辛苦。當然這些並不是備份工具的錯,備份本來就是作這些事。中間有落差的地方在於 USER 需要的是一個歸檔的機制啊,除了備份也需要調閱舊的版本內容。這時版本控管工具,正好就成為 USB DISK 在 PC 上的第一線 "備份資料庫" 了。當你在 check-in / commit 時,不自覺的就在版本系統內放了一份備份了,不放心的人可以再啟用像 VSS (Volume Shadow Copy Service) 或是定期壓 ZIP 這類一般的備份機制作第二層保護,就很足夠了。

    這裡的重點倒不是備份安不安全啦,而是這樣的操作方式,很自然的就會在 SVN Repository 內留下一份內容,同時也方便你替這個版本作註記,未來要調閱,甚至是比對內容差異都很容易...

  4. 異地存取

    USB DISK 雖然很方便,也可以隨身攜帶,但是我就是會常常忘掉它... 常常忘了拔就出門... 在外面如果還要存取我的 USB DISK 的內容,有網路的話,版本控制系統也很好用。我用的 SVN SERVER 是 Visual SVN,它就有個很簡易的 WEB 介面,真的忘了帶還可以連回我自己的 PC,把檔案下載回來。

    如果用的電腦有灌 SVN CLIENT,那你還可以做些基本的操作...。這套比起來就比 VSS 強的多。VSS 完全是 file system base, 透過遠端的操作必需先用網芳之類模擬 file I/O 的方式,效能很糟糕... 雖然 2003 年左右 Microsoft 替 VSS 加其了很多功能,像是 LAN Boost Service (我搞不懂它怎麼做的),或是替 VSS 加上 Web Service Interface (可以透過 HTTP),不過效果都不盡理想。

這些功能加一加,就是我現在在用的個人檔案管理方案了啦。家裡有台現成 SERVER,很多問題就更好解了。這套作法正好给有需要的人參考看看,如果你用了有什麼心得,或是有其它更好的用法也歡迎分享 :D



12/7/2008 4:35:27 PM

原來 System.Xml.XmlWellFormedWriter 有 Bug ..

Microsoft.NET | 543 | C# | MSDN | TROUBLE SHOOTING | XML | 小技巧

果然沒啥人知道的 code, bug 也會比較慢被抓出來 ... 兩個小時前我才貼了找到 XmlNodeWriter 的替代品,用了一下就被我挖到一個 BUG ... @_@

先來看看我的 Sample Code:

XmlTextWriter v.s. XmlWellFormedWriter[copy code]
   1:  // test xml text writer, correct result
   2:  // output: <?xml version="1.0" encoding="big5"?><root><a/><a/><a/><a/><a/></root>
   3:  {
   4:      Console.WriteLine("Using XmlTextWriter:");
   5:      XmlWriter writer = XmlWriter.Create(Console.Out);
   6:      writer.WriteStartElement("root");
   7:      writer.WriteRaw("<a/><a/><a/><a/><a/>");
   8:      writer.WriteEndElement();
   9:      writer.Flush();
  10:      Console.WriteLine();
  11:      Console.WriteLine();
  12:  }
  13:  // test xml node writer, wrong result
  14:  // output: <?xml version="1.0" encoding="big5"?><root>&lt;a/&gt;&lt;a/&gt;&lt;a/&gt;&lt;a/&gt;&lt;a/&gt;</root>
  15:  {
  16:      Console.WriteLine("Using XmlWellFormedWriter:");
  17:      XmlDocument xmldoc = new XmlDocument();
  18:      XmlWriter writer = xmldoc.CreateNavigator().AppendChild();
  19:      writer.WriteStartElement("root");
  20:      writer.WriteRaw("<a/><a/><a/><a/><a/>");
  21:      writer.WriteEndElement();
  22:      writer.Close();
  23:      xmldoc.Save(Console.Out);
  24:      Console.WriteLine();
  25:      Console.WriteLine();
  26:  }

 

而這是程式的輸出畫面:

image

 

兩段 code 除了拿到的 XmlWriter 來源不同之外,用它寫 XML DATA 的方式是一致的,不過寫出來的 XML 則完全不同。看來兩種 XmlWriter 對於 WriteRaw(...) 的實作不大相同。而照 MSDN 上的說明來說,XmlTextWriter的行為是對的,XmlWellFormedWriter 則太雞婆了,沒事多作一次編碼...

 

該說運氣好嘛? 哈哈... 繼上次撈到一個 SmtpMail 的 Bug 之後,這次又撈到一個... 要用的人注意一下,不過即使有這個 Bug, 也不會影響它的地位啦,這 Writer 解決了我很大的困擾,動搖國本也要用下去... (咳... 不過是避開一個 API ...)

 

最後我改了用法,一方面 API 有 BUG 是一回事,另一方面直接用這 API 也很危險,因為 MSDN 說它不會去做內容的驗證,也就是說透過 WriteRaw( ) 寫進不合法的資料,會讓你整份輸出都毀了... 第二個原因比較重要,因此我換了一個替代作法, 類似 Pipe 一樣,把 XmlReader 讀到的東西都寫到 XmlWriter:

XmlCopyPipe 實作[copy code]
   1:  /// <summary>
   2:   /// 從 XmlReader 複製到 XmlWriter
   3:   /// </summary>
   4:   /// <param name="reader"></param>
   5:   /// <param name="writer"></param>
   6:   private static void XmlCopyPipe(XmlReader reader, XmlWriter writer)
   7:   {
   8:       if (reader == null)
   9:       {
  10:           throw new ArgumentNullException("reader");
  11:       }
  12:       if (writer == null)
  13:       {
  14:           throw new ArgumentNullException("writer");
  15:       }
  16:       while (reader.Read() == true)
  17:       {
  18:           switch (reader.NodeType)
  19:           {
  20:               case XmlNodeType.Element:
  21:                   writer.WriteStartElement(reader.Prefix, reader.LocalName, reader.NamespaceURI);
  22:                   writer.WriteAttributes(reader, true);
  23:                   if (reader.IsEmptyElement)
  24:                   {
  25:                       writer.WriteEndElement();
  26:                   }
  27:                   break;
  28:               case XmlNodeType.Text:
  29:                   writer.WriteString(reader.Value);
  30:                   break;
  31:               case XmlNodeType.Whitespace:
  32:               case XmlNodeType.SignificantWhitespace:
  33:                   writer.WriteWhitespace(reader.Value);
  34:                   break;
  35:               case XmlNodeType.CDATA:
  36:                   writer.WriteCData(reader.Value);
  37:                   break;
  38:               case XmlNodeType.EntityReference:
  39:                   writer.WriteEntityRef(reader.Name);
  40:                   break;
  41:               case XmlNodeType.XmlDeclaration:
  42:               case XmlNodeType.ProcessingInstruction:
  43:                   writer.WriteProcessingInstruction(reader.Name, reader.Value);
  44:                   break;
  45:               case XmlNodeType.DocumentType:
  46:                   writer.WriteDocType(reader.Name, reader.GetAttribute("PUBLIC"), reader.GetAttribute("SYSTEM"), reader.Value);
  47:                   break;
  48:               case XmlNodeType.Comment:
  49:                   writer.WriteComment(reader.Value);
  50:                   break;
  51:               case XmlNodeType.EndElement:
  52:                   writer.WriteFullEndElement();
  53:                   break;
  54:           }
  55:       }
  56:   }

 

很好用的作法,就像過去需要 COPY XML 資料,最常見的就是把來源跟目的都用 XmlDocument 載入,直接用 ImportNode( ) 把 XML 片段資料搬到另一個 XmlDocument 再儲存。跟上一篇的原因一樣,看起來很蠢... 就想到這個作法,透過 XmlReader, 拿到的是已經 parsing 過的資料,直接寫到 XmlWriter。而我用的 Writer 正好又可避開重複作 parsing 動作的優點,正好這樣效能跟可用性都兼顧了... 經過 parsing, 至少寫出來的東西會安心一點...

 

把最後我的程式搭配這個 XmlPipeCopy 改一改:

用 XmlCopyPipe 取代 WriteRaw( )[copy code]
   1:  XmlDocument xmldoc = new XmlDocument();
   2:  XmlWriter writer = xmldoc.CreateNavigator().AppendChild();
   3:  writer.WriteStartElement("root");
   4:  XmlReaderSettings settings = new XmlReaderSettings();
   5:  settings.ConformanceLevel = ConformanceLevel.Fragment;
   6:  XmlReader reader = XmlReader.Create(
   7:      new StringReader("<a/><a/><a/><a/><a/>"),
   8:      settings);
   9:  XmlCopyPipe(reader, writer);
  10:  writer.WriteEndElement();
  11:  writer.Close();
  12:  xmldoc.Save(Console.Out);

 

試了一下,果然如預期的執行了 :D,結果也沒錯,還好 XmlWellFormedWriter 的 Bug 只存在於 WriteRaw... 閃開就沒事了:

image

 

 

其中有個陷阱,就是如何用 XmlReader 讀取 XmlFragment (可以有多個 ROOT 的 XML DATA)。其實這個解法跟程式碼,大部份都是這篇看來的,只不過在裡面加了個 LOOP 跟改了名字,各位覺的好用的話記得去謝原作者 Mark Fussell, 別謝錯人了 :D



12/7/2008 2:04:10 PM

原來 .NET 早就內建 XmlNodeWriter 了...

Microsoft.NET | C# | XML | 小技巧 | 我的作品 | 技術隨筆 | 物件導向

最近事情一堆,上班忙上班的事,下班還在忙著研究 Enterprise Library, Entity Framework, 還有一堆五四三的,文章寫的就少了... 先跟有訂閱我 BLOG 的朋友們說聲道歉...。 不過在寫新專案的過程中,意外的發現這東西,一定要提一下...

 

不知道有多少人用過 XmlNodeWriter ? 我用這東西用很久了,當年 Microsoft 推出 .NET Framework 時,強調有很強的 XML 處理能力,其中 XmlReader / XmlWriter 就是以效能為考量,讓你避開處理大型 XML 資料效能很糟糕的 XmlDocument, 也不用去碰很難寫的 SAX 的替代方案...

無奈 M$ 內建的 XmlWriter 少的可憐,只能寫到檔案或是 TextWriter ... 看看權威的 MSDN 告訴我們有那些 XmlWriter 可以用?

image

老實說除了 XmlTextWriter 之外,另外兩個很少用的到。XmlWriter 在輸出 XML 時很好用 (如果你只作輸出的話),複雜的 XML 輸出用 XmlWriter 比用 XmlDocument 簡單多了,不過最常碰到的情況是我還是想用 XmlDocument 來操作 XML,不過其中一部份的 NODE 想用 XmlWriter 來更新內容...

古早有位好心的 MVP 寫了 XmlNodeWriter, 就可以讓我這樣用:

XmlNodeWriter Sample Code:[copy code]
   1:  XmlDocument xdoc = new XmlDocument();
   2:  xdoc.LoadXml("<root><node1><data/><data/><data/></node1><node2/></root>");
   3:  XmlNodeWriter xnw = new XmlNodeWriter(xdoc.DocumentElement, true);
   4:  xnw.WriteStartElement("newNode");
   5:  xnw.WriteAttributeString("newatt", "123");
   6:  xnw.WriteCData("1234567890");
   7:  xnw.WriteEndElement();
   8:  xnw.Close();
   9:  xdoc.Save(Console.Out);

 

第一個參數是 XmlNode, 第二個參數是要不要清掉原來 Node 下的內容。很棒,我可以直接拿 XmlNode 當作 XmlWriter 輸出的對象,透過 Writer 寫出去的東西就直接反映在 XmlNode 身上了,省掉輸出成 Text 然後再 PARSING 回 XML NODE 這種蠢事...

 

前面只是緬懷 XmlNodeWriter 到底有多好用,現在找不到有多難過而已... 接下來才是正題...

 

不過現在想再去找 XmlNodeWriter 官方網站已經找不到了 @_@,原本這 lib 是 hosting 在 gotdotnet.com 這網站上,不過 M$ 已經把它關了,改成 codeplex.com / MSDN Code Gallery 取代,只好求助 GOOGLE 大神,無意間又發現這 M$ XmlTeam 的 BLOGCOMMENTS 有這麼一段:

 

# re: XML Features in the February CTP of Visual Studio “Orcas”
Friday, February 02, 2007 8:27 PM by Stuart Ballard


Is there going to be an XmlNodeWriter in Orcas? It's a fairly glaring hole, especially if you've ever wanted to apply an XSL transformation to an in-memory XmlDocument and get the result as another in-memory XmlDocument. You can pass the input to the transform via XmlNodeReader, but to get it back out again you just have to feed your XmlWriter to a StringBuilder and then parse it...

Fortunately my use case wasn't performance-critical, but it's still ugly...

 

# re: XML Features in the February CTP of Visual Studio “Orcas”
Saturday, February 03, 2007 3:22 AM by Oleg Tkachenko


Stuart, XmlNodeWriter in .NET 2.0 is hiding in

xmlNode.createNavigator().AppendChild() method. It can be used to populate XmlNode via XmlWriter API and so you can

XmlDocument doc = new XmlDocument();

using (XmlWriter writer = doc.CreateNavigator().AppendChild()) {

   xslt.Transform(input, (XsltArgumentList)null, writer);

}

Mike, am I right that  Orcas January CTP includes none of these coolness?

 

真是太機車了,這麼好用的東西藏在這種地方? @_@,枉我從 .NET 1.0 beta 就開始用 C# 處理 XML,連 XSLT Extension 都寫過一堆, Trace Code 也追到 XSLT 內抓過一堆問題... 竟然連這東西都沒發現? 可惡...

於是手又癢了,拿來試用看看,發現只要動手寫幾行 Code, 我就能把 XmlNodeWriter 變回來了,像這樣:

 

我的 XmlNodeWriter 實作[copy code]
   1:  public class XmlNodeWriter : XmlWriter
   2:  {
   3:      private XmlWriter _inner_writer = null;
   4:      public XmlNodeWriter(XmlNode node, bool clean)
   5:      {
   6:          if (clean == true)
   7:          {
   8:              node.RemoveAll();
   9:          }
  10:          this._inner_writer = node.CreateNavigator().AppendChild();
  11:      }
  12:      #region 無聊的 "延長線" 程式碼...
  13:      // 略! 共一百多行,補上廿幾個 abstract method / property, 把它接到 _inner_writer 上
  14:      #endregion
  15:  }

 

這樣果真可以 WORK 了 :D  不過要真正變出一個新的 XmlNodeWriter 代價還不低,繼承 XmlWriter 的後果是有廿幾個 abstract method / property 得補上實作... 全都是很無聊的 code, 就是拿 _inner_writer 的直接套上去而已... 像這樣:

"延長線" 型的程式碼[copy code]
   1:  public override void Close()
   2:  {
   3:      this._inner_writer.Close();
   4:  }
   5:  public override void Flush()
   6:  {
   7:      this._inner_writer.Flush();
   8:  }
   9:  public override string LookupPrefix(string ns)
  10:  {
  11:      return this._inner_writer.LookupPrefix(ns);
  12:  }

 

這堆 Code 我就不貼了,總之可以 WORK :D

 

image

 

不過對 CODE 有點潔癖的我,越看越不是味道,就動起 Factory 的腦筋了。繼續改造一下... 原本 .NET 2.0 內建的 XmlWriter 就已經提供 Factory 的用法了,像這樣:

XmlWriter my_writer = XmlWriter.Create( ... );

不過沒辦法不改 .NET FX 原始碼的情況下 "加掛" 我自己的 Create(...) 實作,原本腦筋是動到 C# 3.0 開始支援的 Extension Method, 不過它只支援 instance method, 不支援 static method ... 只好改成這樣:

 

XmlWriterFactory 實作[copy code]
   1:  public abstract class XmlWriterFactory : XmlWriter
   2:  {
   3:      public static XmlWriter Create(XmlNode node)
   4:      {
   5:          return Create(node, false, null);
   6:      }
   7:      public static XmlWriter Create(XmlNode node, bool clearnContent)
   8:      {
   9:          return Create(node, clearnContent, null);
  10:      }
  11:      public static XmlWriter Create(XmlNode node, bool cleanContent, XmlWriterSettings settings)
  12:      {
  13:          if (node == null) throw new ArgumentNullException("node");
  14:          if (cleanContent == true)
  15:          {
  16:              node.RemoveAll();
  17:          }
  18:          XmlWriter xw = node.CreateNavigator().AppendChild();
  19:          if (settings != null)
  20:          {
  21:              xw = XmlWriter.Create(xw, settings);
  22:          }
  23:          return xw;
  24:      }
  25:  }

 

沒事還繼承原本的 XmlWriter 只有一個目的,就是要延用它原來的 10 種 Create method 啊... 貼張圖為証,繼承之後我就有 13 種不同的 Create method 可以用... 不用再兩頭跑 (只是不能加在原本的 XmlWriter 上真是殘念, C# 什麼時後會支援 static method extension ?):

image

 

 

當然,原程式只要改掉如何拿到 XmlWriter 那行而已,其它都照舊就可以執行了 :D

 

有需要的就拿去用吧,CODE 才十幾行,還包成 DLL 實在太麻煩了,需要的直接貼到你自己的 CODE 裡就好! 要散怖都隨便,沒有什麼授權問題,唯一的要求就是讓我知道我的 CODE 你有在用就好 :D,想讚助我的也很簡單,BLOG 上該多點幾下的東西,沒事就點一點... 哈哈



11/18/2008 1:23:00 AM

Policy Injection Application Block 小發現...

Microsoft.NET | 543 | AOP | Application Block | C# | MSDN | 小技巧 | 技術隨筆 | 物件導向

因為工作的關係,最近正在研究 Enterprise Library 裡整合的 Patterns & Practices 介紹的各式 Application Block... 撇開其它的發現,有個東西一定要提一下,就是 Policy Injection ...

介紹文章我就不多說了,一樣網路一大堆,有興趣的可以看 MSDN 官方的說明。比較特別的是它的用法。當年剛開始研究 .NET 內建的 Role Based Security Control,才在讚嘆它的 code 寫起來真漂亮,只要加個 attribute, 就可以在 runtime 自動檢查呼叫時的身份是否滿足 attribute 的宣告,如下:

CAS範例程式: [copy code]
[PrincipalPermissionAttribute(SecurityAction.Demand, Role="Supervisor")]public void Foo() {    // ... }
   1:  [PrincipalPermissionAttribute(SecurityAction.Demand, Role="Supervisor")]
   2:  public void Foo() {
   3:      // ... 
   4:  }

 

不管你的 code 在那裡,只要呼叫這個 Foo method, 當時的身份 ( principal ) 如果不屬於 "Supervisor" 這個角色的話,就會引發 Security Exception... 當初看到這真是太棒了,我可以用宣告的方式來作安全控制,不需要在主程式裡加一堆囉哩叭唆的 code 來查權限...

不過當我開始研究如何 "自定" 這個行為,除了加上自己的安全機制之外,想更進一步的加上 Log 或是其它的檢查... 我才發現跟本辦不到。因為... 這行為是直接在 CLR 裡支援的啊,我可以加上一堆自定的 Attribute 掛上去,但是呼叫時完全不會觸發我的 code ...

之後研究過 AOP,發現 AOP 正是解決我這類問題的 Solution, 無奈那些 solution 都不大實際,就沒深入研究了。之後找到篇 MSDN 的文章,裡面提到 .NET Remoting 時,遠方會產生 Proxy, 同時 Client / Server 之間的溝通會介著中間傳輸層傳遞 IMessage 介面封裝的 message, 到另一端才會由 Proxy 解讀,然後用 Reflection 還原呼叫的動作... 利用 Proxy 在還原呼叫動作時,你就有機會插入你要的邏輯 (IMessageSink),做到跟上面例子類似的功能。

 

還是很不實際啊啊啊啊,我沒事也不會去用 .NET Remoting 啊,用不到的話這招對我也沒啥用 (大錯特錯!! 當年的我真是太過自信了 :~~~~) ... 這事就一直擱著了,直到...

最近在研究 Policy Injection Application Block 時,讓我看到了似曾相識的 code:

 

Policy Injection Sample Code #1[copy code]
[AuthorizationCallHandler("operation-name")]public void Deposit(decimal depositAmount){  balance += depositAmount;}
   1:  [AuthorizationCallHandler("operation-name")]
   2:  public void Deposit(decimal depositAmount)
   3:  {
   4:    balance += depositAmount;
   5:  }

 

 

這段 CODE 跟前面 CAS 的範例作用差不多,一樣是在 method 被呼叫前作一次權限的檢查。不同的是 AuthorizationCallHandlerAttribute 是自定的 (由 Security Application Block 提供的),它的作用比 ROLE 更進一階,是直接檢查授權的。之間的差別就如同 windows 大家都知道把 USER 加入 Administrators 角色的話,"預設" 就可以做大部份的事,但是你要在某個有 ACL 的物件 (如 NTFS 的檔案) 拒絕 Administrators 的存取也是可行的。前面 CAS 的例子就只是判定你是不是某角色的人,而這例子則是判定某個授權的定義允不允許你執行。

扯遠了,重點不在安全,重點是自定的 Code / Attribute 也可以這樣用啊! 由於我多年心裡的疑惑,挖出這段作法比研究 Policy Injection 更積極一點 (老闆對不起...) 哈哈,沒想到答案就在前文...

 

 

它ㄨ的!! 原來只是在 Local 使用 .NET Remoting ...

 

 

說穿了不值錢,你用的物件標上 Attribute 後,要透過它的建立方式 ( Create or Wrap ) 取得加料過的物件,再呼叫它就會有你預期的效果了。這加料過的物件,就是 System.Runtime.Remoting.Proxies.RealProxy 下的某類別啊啊啊啊... 意思是我拿這加料過的物件,就會透過 .NET Remoting 的方式去呼叫到我真正的物件,而 Policy Injection Application Block 正好就替我把我要作的動作給補上去...。

雖然心裡有被擺了一道的感覺,不過它的 code 包裝的真漂亮啊... 除了 Create 的方式由原本的 new .ctor( ) 改成它的 Create( ... ) 之外,其它就通通一樣了。更猛的是它還提供了幾個真的很實用的 CallHandler (就是呼叫時會加料的動作啦):

  • Authorization Handler
  • Caching Handler
  • Exception Handling Handler
  • Logging Handler
  • Performance Counter Handler
  • Validation Handler
  • Custom Pipeline Handlers

大部份的 Handlers 都望文生義,像是 Logging 就是呼叫時替你加一段 LOG,而 Performance Counter 則是呼叫時就替你戳一下 windows 內建的 performance counter, 讓你可以透過 performance monitor 看相關統計 (如你的 method 被呼叫過幾次... ),更神奇的是 Caching, 如果你的 method 跑的很慢,加上去之後甚至是 cache 裡已經有了上次的結果,這次呼叫就直接 return 了... (你還記得你寫過多少次資料不在 cache 內就 insert 進去的 code 嗎?) @_@

 

如果你看這篇期望看到啥 Enterprise Library / Policy Injection Application Block 的深入介紹的話,很抱歉... 我還沒那本事,哈哈... 再過陣子研究出心得,可能會寫幾篇吧...。 這類文章如果你不介意看英文的,官方的說明還有 QuickStart 的範例就夠你看了,可以參考看看,我就不獻醜了...。 這篇純粹是為了這 AB 解除了我多年來的遺憾,特地留下篇記念用的... :D



10/31/2008 2:20:00 AM

關不掉的 Vista UAC !?

TROUBLE SHOOTING | 小技巧

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

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

找到的方法是: msconfig.exe

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

image

 

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



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/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/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/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/18/2008 3:25:17 AM

[C#: yield return] #1. How It Work ?

Microsoft.NET | Threading | 小技巧 | 技術隨筆 | 物件導向 | C#

C# 常常拿來跟 Java 比較,在 .NET 1.1 時常常是不相上下,而 .NET 又因為較年輕 & 頂著 M$ 的名號,往往被當成玩具一樣,不過 M$ 的確是在 .NET 及 C# 下了很多功夫,作了很多 Sun 不願意在 Java 身上作的事,這次要探討的 yield returnIEnumerable<T> 這搭配的 Interface 就是一例...。

Java 在過去的版本,往往為了跨平台,把修改 VM 規格視為大忌,連帶的連語法修改都一樣,即使不影響編譯出來的 bytecode 相容性也是一樣不肯改。而 .NET 就為了語法簡潔,編譯器往往是讓步的一方,因此 C# 有相當多的 Syntax Sugar,讓你寫起 CODE 爽一點...。你只要寫簡單的 CODE,編譯器會幫你轉成較煩雜的 CODE,有點像是文言文或是成語那樣的味道。古代文人常常用簡單的四個字,就有一大票引申的意義跑出來...,寫作文時只要套上成語,就代表了成語背後的故事,寓意。

"yield return" 算是最甜的甜頭了,因為編譯器替你翻出來的 code 整整一大串。先來看個簡單的例子,如果我想實作 IEnumerator<T> Interface, 照順序輸出 1 ~ 100 的數字,正統的 C# code 看起來要像這樣:

用 IEnumerator 依序傳回 1 ~ 100 的數字[copy code]

   1:  public class EnumSample1 : IEnumerator<int>
   2:  {
   3:      private int _start = 1;
   4:      private int _end = 100;
   5:      private int _current = 0;
   6:      public EnumSample1(int start, int end)
   7:      {
   8:          this._start = start;
   9:          this._end = end;
  10:          this.Reset();
  11:      }
  12:      public int Current
  13:      {
  14:          get { return this._current; }
  15:      }
  16:      public void Dispose()
  17:      {
  18:      }
  19:      object System.Collections.IEnumerator.Current
  20:      {
  21:          get { return this._current; }
  22:      }
  23:      public bool MoveNext()
  24:      {
  25:          this._current++;
  26:          return !(this._current > this._end);
  27:      }
  28:      public void Reset()
  29:      {
  30:          this._current = 0;
  31:      }
  32:  }

 

 

好不容易寫好 IEnumerator 之後,再來是拿來用,一筆一筆印出來:

取得 IEnumerator 物件後,依序取出裡面的數字[copy code]
   1:  EnumSample1 e = new EnumSample1(1, 100);
   2:  while (e.MoveNext())
   3:  {
   4:      Console.WriteLine("Current Number: {0}", e.Current);
   5:  }

 

不過如果只是要列出 1 ~ 100,大部份的人都不會想這樣寫吧? 直接用計概第一堂教你的 loop 不就好了? 程式碼如下:

送分題: 用 LOOP 印出 1 ~ 100 的數字[copy code]
   1:  for (int current = 1; current <= 100; current++)
   2:  {
   3:      Console.WriteLine("Current Number: {0}", current);
   4:  }

 

 

兩個範例都沒錯啊,那為什麼要用 IEnumerator ? 其實 IEnumerator 並不是 Microsoft 發明的,在四人幫寫的經典書籍 (Design Patterns) 裡就有這麼一個設計模式: Iterator,它的目的很明確:

"毋須知曉聚合物件的內部細節,即可依序存取內含的每一個元素。"

(摘自 物件導向設計模式 Design Patterns 中文版,葉秉哲 譯)

這裡指的 "聚合物件" 就是指 .NET 的 Collection, List, Array 等這類物件。意思是你不需要管 collection 裡每一個物件是怎麼擺的,用什麼結構處理的,用什麼邏輯或演算法處理的,我就只管照你安排好的順序一個一個拿出來就好。沒錯,這就是它主要的目的。換另一個說法,就是我們希望把物件巡訪的順序 (iteration) 跟依序拿到物件後要作什麼事 (process) 分開,那你就得參考 Iterator Pattern。不用? 那只好讓你的 iteration / process 混在一起吧。

差別在那? 我們再來看第二個例子。如果題目改一下,要列出 1 ~ 100 的數字,但如果不是 2 的倍數,也不是 3 的倍數,就跳過去。先來看看 Loop 的版本:

進階送分題,用LOOP印出 1~100 之中,2 或 3 的倍數[copy code]
   1:  for (int current = 1; current <= 100; current++)
   2:  {
   3:      bool match = false;
   4:      if (current % 2 == 0) match = true;
   5:      if (current % 3 == 0) match = true;
   6:      if (match == true)
   7:      {
   8:          Console.WriteLine("Current Number: {0}", current);
   9:      }
  10:  }

 

 

再來看看 IEnumerator 的版本:

用 IEnumerator 列出 1 ~ 100 中 2 或 3 的倍數[copy code]
   1:  public class EnumSample2 : IEnumerator<int>
   2:   {
   3:       private int _start = 1;
   4:       private int _end = 100;
   5:       private int _current = 0;
   6:       public EnumSample2(int start, int end)
   7:       {
   8:           this._start = start;
   9:           this._end = end;
  10:           this.Reset();
  11:       }
  12:       public int Current
  13:       {
  14:           get { return this._current; }
  15:       }
  16:       public void Dispose()
  17:       {
  18:       }
  19:       object System.Collections.IEnumerator.Current
  20:       {
  21:           get { return this._current; }
  22:       }
  23:       public bool MoveNext()
  24:       {
  25:           do {
  26:               this._current++;
  27:           } while(this._current %2 > 0 && this._current %3 > 0);
  28:           return !(this._current > this._end);
  29:       }
  30:       public void Reset()
  31:       {
  32:           this._current = 0;
  33:       }
  34:   }

 

 

而扣掉 IEnumerator 的部份,要把數字印出來的程式碼則完全沒有改變:

取出 IEnumerator 的每個數字,印到畫面上[copy code]
   1:  EnumSample2 e = new EnumSample2(1, 100);
   2:  while (e.MoveNext())
   3:  {
   4:      Console.WriteLine("Current Number: {0}", e.Current);
   5:  }

 

可以看的到,Loop 版本的確是把 iteration 跟 process 的 code 完全混在一起了,未來任何一方的邏輯要抽換都很麻煩,而 IEnumerator 則不會,分的很清楚,不過... 這 Code 會不會太 "髒" 了一點啊...? 試問一下,有誰會這麼勤勞,都用 IEnumerator 來寫 Code? 有的話請留個言,讓我崇拜一下...。

 

屁話講了一堆,最後就是要帶出來 "的確有魚與熊掌得兼的方法",怎麼作? 來看看用 C# 的 yield return 版本的程式碼:

傳回 IEnumerable 的 METHOD (不用再寫 CLASS,實作 IEnumerator 了)[copy code]
   1:  public static IEnumerable<int> YieldReturnSample3(int start, int end)
   2:  {
   3:      for (int current = 1; current <= 100; current++)
   4:      {
   5:          bool match = false;
   6:          if (current % 2 == 0) match = true;
   7:          if (current % 3 == 0) match = true;
   8:          if (match == true)
   9:          {
  10:              yield return current;
  11:          }
  12:      }
  13:  }

 

 

用 foreach 搭配 IEnumerable 印出每一筆數字[copy code]
   1:  foreach (int current in YieldReturnSample3(1, 100))
   2:  {
   3:      Console.WriteLine("Current Number: {0}", current);
   4:  }

 

 

 

真是太神奇了,安德魯。如何? 完美的結合兩者的優點,這種 code 實在是令人挑不出什麼缺點... 真是優雅... 不過念過系統程式的人一定都會吶悶... 這樣的程式執行方式,不就完全的違背了一般結構化程式典型的 function call / return 的鐵律了? 程式呼叫某個 function 就應該完全執行完才能 return 啊,怎麼能 "yield" return 後,跑完一圈又回到剛才執行到一半的 function 繼續跑,然後再 "yield" return ? 好像同實有兩段獨立的邏輯在運作... 還可以在兩者之間跳來跳去?

這就是 C# compiler 猛的地方了。搬出 reflector 來看看編譯出來的 code, 再被反組譯回來變成什麼樣子:

 

反組譯 YieldReturnSample3[copy code]
   1:  public static IEnumerable<int> YieldReturnSample3(int start, int end)
   2:  {
   3:      <YieldReturnSample3>d__0 d__ = new <YieldReturnSample3>d__0(-2);
   4:      d__.<>3__start = start;
   5:      d__.<>3__end = end;
   6:      return d__;
   7:  }
   8:   
   9:   

 

耶? 看到一個多出來的 class: <YieldReturnSample3>d__0 ... 再看看它的 class 長啥樣:

編譯器自動產生的 IEnumerator 衍生類別[copy code]
   1:  [CompilerGenerated]
   2:  private sealed class <YieldReturnSample3>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
   3:  {
   4:      // Fields
   5:      private int <>1__state;
   6:      private int <>2__current;
   7:      public int <>3__end;
   8:      public int <>3__start;
   9:      private int <>l__initialThreadId;
  10:      public int <current>5__1;
  11:      public bool <match>5__2;
  12:      public int end;
  13:      public int start;
  14:   
  15:      // Methods
  16:      [DebuggerHidden]
  17:      public <YieldReturnSample3>d__0(int <>1__state)
  18:      {
  19:          this.<>1__state = <>1__state;
  20:          this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
  21:      }
  22:   
  23:      private bool MoveNext()
  24:      {
  25:          switch (this.<>1__state)
  26:          {
  27:              case 0:
  28:                  this.<>1__state = -1;
  29:                  this.<current>5__1 = 1;
  30:                  while (this.<current>5__1 <= 100)
  31:                  {
  32:                      this.<match>5__2 = false;
  33:                      if ((this.<current>5__1 % 2) == 0)
  34:                      {
  35:                          this.<match>5__2 = true;
  36:                      }
  37:                      if ((this.<current>5__1 % 3) == 0)
  38:                      {
  39:                          this.<match>5__2 = true;
  40:                      }
  41:                      if (!this.<match>5__2)
  42:                      {
  43:                          goto Label_0098;
  44:                      }
  45:                      this.<>2__current = this.<current>5__1;
  46:                      this.<>1__state = 1;
  47:                      return true;
  48:                  Label_0090:
  49:                      this.<>1__state = -1;
  50:                  Label_0098:
  51:                      this.<current>5__1++;
  52:                  }
  53:                  break;
  54:   
  55:              case 1:
  56:                  goto Label_0090;
  57:          }
  58:          return false;
  59:      }
  60:   
  61:      [DebuggerHidden]
  62:      IEnumerator<int> IEnumerable<int>.GetEnumerator()
  63:      {
  64:          Program.<YieldReturnSample3>d__0 d__;
  65:          if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2))
  66:          {
  67:              this.<>1__state = 0;
  68:              d__ = this;
  69:          }
  70:          else
  71:          {
  72:              d__ = new Program.<YieldReturnSample3>d__0(0);
  73:          }
  74:          d__.start = this.<>3__start;
  75:          d__.end = this.<>3__end;
  76:          return d__;
  77:      }
  78:   
  79:      [DebuggerHidden]
  80:      IEnumerator IEnumerable.GetEnumerator()
  81:      {
  82:          return this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
  83:      }
  84:   
  85:      [DebuggerHidden]
  86:      void IEnumerator.Reset()
  87:      {
  88:          throw new NotSupportedException();
  89:      }
  90:   
  91:      void IDisposable.Dispose()
  92:      {
  93:      }
  94:   
  95:      // Properties
  96:      int IEnumerator<int>.Current
  97:      {
  98:          [DebuggerHidden]
  99:          get
 100:          {
 101:              return this.<>2__current;
 102:          }
 103:      }
 104:   
 105:      object IEnumerator.Current
 106:      {
 107:          [DebuggerHidden]
 108:          get
 109:          {
 110:              return this.<>2__current;
 111:          }
 112:      }
 113:  }

 

耶? 不就完全跟之前手工寫的 IEnumerator 一樣嘛? 只不過這個 IEnumerator 是自動產生出來的,不是手寫的...。 畢竟是機器產生的 CODE,總是沒那麼精簡。想到了嗎? 沒錯,這就是 C# compiler 送給你的 syntax sugar ...,你可以腦袋裡想像著計概課入門時教你的 LOOP 那樣簡單的想法,compiler 就幫你換成 IEnumerator 的實作方式,讓你隨隨便便就可以跟別人宣稱:

 

"看! 我的程式有用到 Iterator 這個設計模式喔..."

 

聽起來好像很臭屁的樣子... 哈哈! 如果是在真的用的到 Iterator Patterns 的情況下,真的是可以很臭屁的拿出來炫耀一下。不過,我幹嘛突然講起 yield return ? 各位看的過程中有沒有聯想到前幾篇 POST 講的 Thread Sync 那兩篇文章 ( #1, #2 ) ? IEnumerator 跟 Thread Sync 又有什麼關係? 賣個關子,下篇繼續!



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 下的檔案也是不能亂塞的...






精選文章

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