C# 常常拿來跟 Java 比較,在 .NET 1.1 時常常是不相上下,而 .NET 又因為較年輕 & 頂著 Microsoft 的名號,往往被當成玩具一樣,不過
Microsoft 的確是在 .NET 及 C# 下了很多功夫,作了很多 Sun 不願意在 Java 身上作的事,這次要探討
的 yield return 及 IEnumerable<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 的數字
public class EnumSample1 : IEnumerator<int>
{
private int _start = 1;
private int _end = 100;
private int _current = 0;
public EnumSample1(int start, int end)
{
this._start = start;
this._end = end;
this.Reset();
}
public int Current
{
get { return this._current; }
}
public void Dispose()
{
}
object System.Collections.IEnumerator.Current
{
get { return this._current; }
}
public bool MoveNext()
{
this._current++;
return !(this._current > this._end);
}
public void Reset()
{
this._current = 0;
}
}
好不容易寫好 IEnumerator
之後,再來是拿來用,一筆一筆印出來:
取得 IEnumerator
物件後,依序取出裡面的數字
EnumSample1 e = new EnumSample1(1, 100);
while (e.MoveNext())
{
Console.WriteLine("Current Number: {0}", e.Current);
}
不過如果只是要列出 1 ~ 100,大部份的人都不會想這樣寫吧? 直接用計概第一堂教你的 loop 不就好了? 程式碼如下:
送分題: 用 LOOP 印出 1 ~ 100 的數字
for (int current = 1; current <= 100; current++)
{
Console.WriteLine("Current Number: {0}", current);
}
兩個範例都沒錯啊,那為什麼要用 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 的倍數
for (int current = 1; current <= 100; current++)
{
bool match = false;
if (current % 2 == 0) match = true;
if (current % 3 == 0) match = true;
if (match == true)
{
Console.WriteLine("Current Number: {0}", current);
}
}
再來看看 IEnumerator
的版本:
用 IEnumerator
列出 1 ~ 100 中 2 或 3 的倍數
public class EnumSample2 : IEnumerator<int>
{
private int _start = 1;
private int _end = 100;
private int _current = 0;
public EnumSample2(int start, int end)
{
this._start = start;
this._end = end;
this.Reset();
}
public int Current
{
get { return this._current; }
}
public void Dispose()
{
}
object System.Collections.IEnumerator.Current
{
get { return this._current; }
}
public bool MoveNext()
{
do {
this._current++;
} while(this._current %2 > 0 && this._current %3 > 0);
return !(this._current > this._end);
}
public void Reset()
{
this._current = 0;
}
}
而扣掉 IEnumerator
的部份,要把數字印出來的程式碼則完全沒有改變:
取出 IEnumerator
的每個數字,印到畫面上
EnumSample2 e = new EnumSample2(1, 100);
while (e.MoveNext())
{
Console.WriteLine("Current Number: {0}", e.Current);
}
可以看的到,Loop 版本的確是把 iteration 跟 process 的 code 完全混在一起了,未來任何一方的邏輯要抽換都很
麻煩,而 IEnumerator
則不會,分的很清楚,不過… 這 Code 會不會太 “髒” 了一點啊…? 試問一下,有誰會這麼
勤勞,都用 IEnumerator
來寫 Code? 有的話請留個言,讓我崇拜一下…。
屁話講了一堆,最後就是要帶出來 “的確有魚與熊掌得兼的方法”,怎麼作? 來看看用 C# 的 yield return
版本的程式碼:
傳回 IEnumerable
的 METHOD (不用再寫 CLASS,實作 IEnumerator 了)
public static IEnumerable<int> YieldReturnSample3(int start, int end)
{
for (int current = 1; current <= 100; current++)
{
bool match = false;
if (current % 2 == 0) match = true;
if (current % 3 == 0) match = true;
if (match == true)
{
yield return current;
}
}
}
用 foreach 搭配 IEnumerable
印出每一筆數字
foreach (int current in YieldReturnSample3(1, 100))
{
Console.WriteLine("Current Number: {0}", current);
}
真是太神奇了,安德魯。如何? 完美的結合兩者的優點,這種 code 實在是令人挑不出什麼缺點… 真是優雅…
不過念過 系統程式 的人一定都會吶悶… 這樣的程式執行方式,不就
完全的違背了一般結構化程式典型的 function call / return 的鐵律了? 程式呼叫某個 function 就應該完全執行完
才能 return 啊,怎麼能 yield return
後,跑完一圈又回到剛才執行到一半的 function 繼續跑,然後再 yield return
?
好像同實有兩段獨立的邏輯在運作… 還可以在兩者之間跳來跳去?
這就是 C# compiler 猛的地方了。搬出 reflector 來看看編譯出來的 code, 再被反組譯回來變成什麼樣子:
反組譯 YieldReturnSample3
public static IEnumerable<int> YieldReturnSample3(int start, int end)
{
<YieldReturnSample3>d__0 d__ = new <YieldReturnSample3>d__0(-2);
d__.<>3__start = start;
d__.<>3__end = end;
return d__;
}
耶? 看到一個多出來的 class: <YieldReturnSample3>d__0
… 再看看它的 class 長啥樣:
編譯器自動產生的 IEnumerator 衍生類別
[CompilerGenerated]
private sealed class <YieldReturnSample3>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
// Fields
private int <>1__state;
private int <>2__current;
public int <>3__end;
public int <>3__start;
private int <>l__initialThreadId;
public int <current>5__1;
public bool <match>5__2;
public int end;
public int start;
// Methods
[DebuggerHidden]
public <YieldReturnSample3>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
}
private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<current>5__1 = 1;
while (this.<current>5__1 <= 100)
{
this.<match>5__2 = false;
if ((this.<current>5__1 % 2) == 0)
{
this.<match>5__2 = true;
}
if ((this.<current>5__1 % 3) == 0)
{
this.<match>5__2 = true;
}
if (!this.<match>5__2)
{
goto Label_0098;
}
this.<>2__current = this.<current>5__1;
this.<>1__state = 1;
return true;
Label_0090:
this.<>1__state = -1;
Label_0098:
this.<current>5__1++;
}
break;
case 1:
goto Label_0090;
}
return false;
}
[DebuggerHidden]
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
Program.<YieldReturnSample3>d__0 d__;
if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2))
{
this.<>1__state = 0;
d__ = this;
}
else
{
d__ = new Program.<YieldReturnSample3>d__0(0);
}
d__.start = this.<>3__start;
d__.end = this.<>3__end;
return d__;
}
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
return this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
void IDisposable.Dispose()
{
}
// Properties
int IEnumerator<int>.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
}
耶? 不就完全跟之前手工寫的 IEnumerator
一樣嘛? 只不過這個 IEnumerator
是自動產生出來的,不是手寫的…。 畢竟是機器
產生的 CODE,總是沒那麼精簡。想到了嗎? 沒錯,這就是 C# compiler 送給你的 syntax sugar …,你可以腦袋裡想像著計概課
入門時教你的 LOOP 那樣簡單的想法,compiler 就幫你換成 IEnumerator
的實作方式,讓你隨隨便便就可以跟別人宣稱:
看! 我的程式有用到 Iterator 這個設計模式喔…
聽起來好像很臭屁的樣子… 哈哈! 如果是在真的用的到 Iterator Patterns 的
情況下,真的是可以很臭屁的拿出來炫耀一下。不過,我幹嘛突然講起 yield return
? 各位看的過程中有沒有聯想到
前幾篇 POST 講的 Thread Sync 那兩篇文章 ( #1,
#2 ) ? IEnumerator
跟 Thread Sync 又有什麼關係? 賣個關子,下篇繼續!
話說前陣子處理了 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 下的檔案也是不能亂塞的...
因為家裡大人開出條件,除非新的 BLOG 系統 (就是我在用的 BlogEngine 啦) 有特定文章要輸入密碼才能看的功能,否則她就不想換系統了 (原來是用 CommunityServer 2007)。要弄密碼其實很簡單,不過過去試過 IIS 加上整合式驗證... 弄到最後該看的人看不到,也沒擋到該擋的人而作罷...。
仔細想了想大人的需求,要的就是簡單的控制機制。不需要先建立帳號,也不需要登入,就是特定幾篇文章要輸入暗號才能看到內容,就這樣而以。無耐 BlogEngine 還算很年輕,替它寫的 Extension 也還不多,官方網站提供了幾個 Extension 列表,找到最接近的是這個: Password Protected Post... 不過它是以登入 BE 為使用者認證的方式,再依照 ROLE 跟 CATEGORY 的配對為授權方式,來控制那些讀者能看到那些文章...。就是不想要替每個人建帳號啊,看來只好自己寫了... Orz。
以往都是想要作什麼很簡單,難是難在把它作出來..。現在都反過來了,工具越來越強,系統也越來越完整,難的反而是思考要怎麼作,程式碼沒幾行就搞定了。之前的文章介紹過 BlogEngine 的 Extension 機制,這次就實際來試看看。我要寫的東西很簡單,就一組密碼就好,要有夠簡單的方式讓大人能夠指定那幾篇文章是要保護的,而所有的人 (已登入的除外) 只能看到提示輸入密碼的訊息,密碼打對了才會顯示文章內容。至於密碼要不要加密? 會不會被竊聽? 不重要啦,只要保護不要遜到按右鍵簡示原始碼,密碼跟內容都看光光了就好。
順手寫了幾行 CODE,先驗證一下最基本的動作做不做的到 (POC: Prove Of Concept)。第一步是先把顯示內容的動作攔下來,換成制示的輸入密碼訊息... 這個簡單,沒幾行就搞定了:
直接從 CodePlex 抓下來的 Source Code, 解壓縮完就可以寫了。加上這段 CODE 並不難,整個 Extension 只有這樣而以:
1: [Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]
2: public class SecurePost
3: {
4: static SecurePost()
5: {
6: Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
7: }
8:
9: private static void Post_Serving(object sender, ServingEventArgs e)
10: {
11: Post post = sender as Post;
12: StringBuilder bodySB = new StringBuilder();
13: {
14: // 略。透過 bodySB 輸出 HTML
15: }
16: e.Body = bodySB.ToString();
17: }
18: }
看起來 CODE 還不少,不過算一算真正在作事的都是在湊那堆 HTML ... 關鍵只有一開始去攔 Post.Serving 事件,接到自己的事件處理器 Post_Serving( ) 上,之後所有會輸出 Post 內容的地方,都會觸發這個事件。然後只要在事件處理器內去調整 Post 內容就可以了。
好,好的開始是成功的一半,已經完成 1/3 了 (什麼???) 第一部份的 CODE 產生的 HTML,會引導使用者輸入密碼,按下 [GO] 之後,就會連到 POST 的網址了。不過除了原本網址之外 (post.AbsoluteLink) 後面還要加上 "?pwd=xxxxxx" 帶上使用者輸入的密碼。前面講過我只要最基本的防護,其它進階的安全問題就不理它了。我只要掌握兩個原則:
另外補一件事,我也不要讓全部的文章都用這種機制保護。只要有特別標示的 POST 要密碼就好。看到 BlogEngine 內建的 BreakPost 這個擴充程式,我就仿照它的作法,內文找到特定字串就啟用。我定的規則是整篇 POST 內容開頭一定要是 "[password]" 才會啟用密碼保護機制。
既然這樣,第二步也很簡單。如果密碼對,一切照原狀顯示內容。密碼不對的話就一樣攔下來...。程式碼.... 只是在第一步的程式碼多了... 兩行...
1: private static void Post_Serving(object sender, ServingEventArgs e)
2: {
3: Post post = sender as Post;
4: if (HttpContext.Current.Request["pwd"] == Password) return;
5: if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;
6: StringBuilder bodySB = new StringBuilder();
7: {
8: // 略。透過 bodySB 輸出 HTML
9: }
10: e.Body = bodySB.ToString();
11: }
啥米? 就是第一部份的 CODE 加上第四及第五行就搞定了? 程式不挑的話,現在已經寫完了... 哈哈! 上面的輸入密碼畫面,輸入正確密碼後就可以看到文章內容了。我特地連網址列一起複製下來,在網址列上會看到密碼明碼。照道理應該是要先 HASH 啦,不過 CLIENT SIDE 跟 SERVER SIDE 都要有同樣的 HASH 機制才行,想用 MD5 / SHA256 之類的來算,無耐 CLIENT 要弄這些也是很煩,就決定不理它了...。明碼就明碼吧,執行後的畫面像這樣:
剩下的部份就沒什麼了,想想加上去好了。就是透過 BlogEngine 的 Extension Manager,讓使用者可以簡單的調整參數。要讓使用者自定的參數只有三個:
這些東西自己做的話,就還得想要開檔案或寫資料庫,有點小囉唆,不過已經有 Extension Manager 了,只要在原本的 static constructor 再加幾行就搞定:
1: static SecurePost()
2: {
3: Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
4: ExtensionSettings settings = new ExtensionSettings("SecurePost");
5: settings.AddParameter(
6: "SecurePostMessage",
7: "顯示訊息:");
8: settings.AddParameter(
9: "PasswordHint",
10: "密碼提示:");
11: settings.AddParameter(
12: "PasswordValue",
13: "指定密碼:");
14: settings.AddValues(new string[] {
15: "本篇文章已受密碼保護,請依照題示輸入密碼。",
16: "一二三四",
17: "1234"});
18: settings.IsScalar = true;
19: settings.Help = "用密碼保護文章的內容。";
20: ExtensionManager.ImportSettings(settings);
21: _settings = ExtensionManager.GetSettings("SecurePost");
22: }
我已經很努力的多撐幾行了... 不過也只有這廿行,寫完了...。整個 .cs 檔案直接丟到 ~/App_Code/Extension 就算安裝完成。用管理者身份登入 BE 後,在 Extension 那頁可以看到:
不錯,SecurePost 已經出現在 Extension Manager 裡了。因為有加上 settings 的程式碼,所以右邊有 [編輯] 的字樣出現。點下去之後會到這個畫面:
嗯,看起來真專業,沒想到從頭到尾所有的 CODE 還不到一百行...。幾十行 CODE 寫出來的 Extension 就可以唬人了.. :D,試看看還真的會動耶 (廢話)。早知道寫起來那麼快,當初就不花那麼多時間去找人家寫好的了...。最後附上整段完整的程式碼,有需要的人就拿去用吧! 用法很簡單,全部複製下來 (可以按 [COPY CODE] 就好),存檔,把檔案放在 ~/App_Code/Extension/SecurePost.cs 下,然後用管理者身份進入 BlogEngine Extension Manager 改一改就好了!
大功告成! 這個 Extension 如果對你有用的話就拿去用吧,要散佈也歡迎,不過只有個小要求,請不要把程式碼存到別的地方供人下載,請直接提供我這篇文章的網址就好。覺的好用就留個話給我,要幫我推一下文或讚助就更好了 :D,謝謝收看!
--
1: using System;
2: using System.Web;
3: using System.Web.UI;
4: using BlogEngine.Core.Web.Controls;
5: using BlogEngine.Core;
6: using System.Text;
7:
8:
9:
10:
11: [Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]
12: public class SecurePost
13: {
14: private static string SecurePostMessage { get { return _settings.GetSingleValue("SecurePostMessage"); } }
15: private static string Password { get { return _settings.GetSingleValue("PasswordValue"); } }
16: private static string PasswordHint { get { return _settings.GetSingleValue("PasswordHint"); } }
17:
18: private static ExtensionSettings _settings = null;
19:
20: static SecurePost()
21: {
22: Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
23:
24: ExtensionSettings settings = new ExtensionSettings("SecurePost");
25:
26: settings.AddParameter(
27: "SecurePostMessage",
28: "顯示訊息:");
29: settings.AddParameter(
30: "PasswordHint",
31: "密碼提示:");
32: settings.AddParameter(
33: "PasswordValue",
34: "指定密碼:");
35:
36: settings.AddValues(new string[] {
37: "本篇文章已受密碼保護,請依照題示輸入密碼。",
38: "一二三四",
39: "1234"});
40:
41: //settings.ShowAdd = false;
42: //settings.ShowDelete = false;
43: //settings.ShowEdit = true;
44: settings.IsScalar = true;
45: settings.Help = "用密碼保護文章的內容。";
46:
47: ExtensionManager.ImportSettings(settings);
48:
49: _settings = ExtensionManager.GetSettings("SecurePost");
50:
51: }
52:
53: private static void Post_Serving(object sender, ServingEventArgs e)
54: {
55: Post post = sender as Post;
56:
57:
58: if (HttpContext.Current.User.Identity.IsAuthenticated == true) return;
59: if (HttpContext.Current.Request["pwd"] == Password) return;
60: if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;
61:
62:
63: StringBuilder bodySB = new StringBuilder();
64: {
65: bodySB.AppendFormat(
66: "<b>{0}</b><p/>",
67: HtmlEncode(SecurePostMessage));
68:
69: if (e.Location == ServingLocation.Feed)
70: {
71: }
72: else
73: {
74: bodySB.Append("<div>");
75: bodySB.AppendFormat(
76: @"請輸入密碼(提示: <b>{0}</b>): <input id=""postpwd"" type=""password""/><button onclick=""document.location.href='{1}'+'?pwd='+escape(this.parentNode.all.postpwd.value);"">GO</button>",
77: PasswordHint,
78: post.AbsoluteLink);
79: bodySB.Append("</div>");
80: }
81: }
82: e.Body = bodySB.ToString();
83: }
84:
85: private static string HtmlEncode(string text)
86: {
87: return HttpContext.Current.Server.HtmlEncode(text);
88: }
89: }
感謝編輯賞光,第三篇順利刊出 :D
執行緒這種東西,實在不是什麼主流的文章,不過雜誌社願意刊到第三篇,真是感謝... 前兩篇分別介紹了同步機制跟旗標,這次用執行緒集區作總結,提供了綜合的應用,也對效能的影響作整理,讓讀者具體的瞭解使用前後的效能差異。
這次文章內提到了 ThreadPool 的應用,不過因為內容及篇幅的關係,沒有挖到 ThreadPool 本身怎麼設計。對這部份有興趣的讀者可以參考我寫的這三篇:
ThreadPool 實作 #3. AutoResetEvent / ManualResetEvent
雖然好像沒有人因為看到雜誌才連到這裡來,不過還是要囉唆一下,看到文章有任何意見都可以在這裡留言給我。文章內提到的 SAMPLE CODE 可以在這裡下載! 這次的範例程式是 Console application,不提供直接在網頁上執行,下載回去試試吧!
雖然我很少貼這些五四三的 (明明 543 這 tags 點下去有一堆..) ,不過無意間在癮科技逛到這段實在是太酷了... 流言終結者這節目的 Adam & Jamie用漆彈示範來比喻 CPU / GPU 繪圖的差別... 不管它的比喻精不精確或妥不妥當啦,那一瞬間噴出蒙那麗莎實在是太酷了... :D