繼上篇,講了一些 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) 跟執行緒同步機制有什麼關聯?"
不多說,先看看之前畫的兩張時序圖:
先看之前 ThreadSync #1 裡提到的圖,我這次加上紅線當 "輔助線",紅線代表執行 GameHost 的主程式,這個執行序必需反反覆覆的在 GameHost / Player 兩份類別的程式碼跑來跑去,主程式是 GameHost 發起的,當然被強迫切成好幾段的就只有 Player 了。
這是修改過後的版本,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
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; } }程式碼一如往常,又是只有一點點 (謎之音: 你到底有沒有寫過長一點的程式碼? -_-) ...。 原本的 Think 改成會傳回 IEnumerable<HintRecord> 的型別,因此內部就可以透過一連串的 yield return xxxx; 指令來把問題交給 GameHost。而 GameHost 拿到題目就會開始計算答案,然後再呼叫 Player.GuessNext( ) 把上次的答案傳回去。透過 Player 的實作,GuessNext 會呼叫 _enum.MoveNext( ), 控制權會再交到 Think( ) 上次呼叫 yield return 的地方,直到又執行到下一個 yield return 為止。這時 GameHost 又取得下一個問題,不斷重複這樣的動作直到結束。 同樣的,我們用 DummyPlayer 改寫,看看用 yield return 的版本寫起來是怎麼樣? DummyYieldPlayer 的程式碼
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()); } } }跟上次的 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
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,不提供直接在網頁上執行,下載回去試試吧!