9/18/2008 3:25:17 AM

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

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

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 又有什麼關係? 賣個關子,下篇繼續!



Comments

1/16/2010 4:38:16 PM #

pingback

Pingback from proglab.freevar.com

yield?蝙?典??? « 摰?貊?憭拍征

proglab.freevar.com | Reply

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
Loading






精選文章

RUN! PC 文章及範例下載
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