受到群里兄弟們的竭力邀請,老陳終於決定來分享一下.NET下的模板引擎開發技術。本系列文章將會帶您由淺入深的全面認識模板引擎的概念、設計、分析和實戰應用,一步一步的帶您開發出完全屬於自己的模板引擎。關於模板引擎的概念,我去年在百度百科上錄入了自己的解釋(請參考:模板引擎)。老陳曾經自己開發了一套網鳥Asp.Net模板引擎,雖然我自己並不樂意去推廣它,但這已經無法阻擋群友的喜愛了!
上次我們簡單的認識了一下置換型模板引擎的幾種情況,當然我總結的可能不夠完善,希望大家繼續補充。談到按流替代式模板引擎的原理但並沒有給出真正的實現。跟帖的評論中有一位朋友(Treenew Lyn)說的很好:“Token 解析其實是按一個字符一個字符去解析的”。的確是這樣,而且唯有這樣才能夠實現更加高效、更加准確的模板引擎機制。我們首先將模板代碼分解成一個一個的Token,然后按照順序形成Token流(順序集合),在輸出的時候替換規定好的語法標記即可。
目的
假定我們要處理的模板文件類似於如下格式(與上一節一樣):
1 /// <summary>
2 /// 模板文本。
3 /// </summary>
4 public const string TEMPLATE_STRING = @"<a href=""{url}"">{title}</a><br />";
我們的目的是將它按照{xxx}這樣的標記分解成Token流。
方案
解決這個問題的方案大致有這么幾種:
- 最直觀的就是正則表達式;
- 比較拐彎但比正則快的就是split+各種技巧;
- 按(字符)流解析 ,即將字符流轉化為Token流;
今天我們只討論第三種情況,第一種很簡單,第二種稍微復雜一點,但相信難不倒您!第三種做法只是不太常見,但如果您接觸過編譯原理(或搜索引擎開發),就不是那么陌生了。
思路
首先,我們看看這段模板代碼按字符流輸出回是怎樣的:
1 // 實現代碼
2 [Test]
3 public void Test1()
4 {
5 var s = new StringBuilder();
6
7 foreach (var c in TestObjects.TEMPLATE_STRING)
8 {
9 // 這里我們用回車換行符將輸出隔開
10 s.AppendLine(c.ToString(CultureInfo.InvariantCulture));
11 }
12
13 Trace.WriteLine(s.ToString());
14 }
15
16 /* 輸出結果如下
17 <
18 a
19
20 h
21 r
22 e
23 f
24 =
25 "
26 {
27 u
28 r
29 l
30 }
31 "
32 >
33 {
34 t
35 i
36 t
37 l
38 e
39 }
40 <
41 /
42 a
43 >
44 <
45 b
46 r
47
48 /
49 >
50 */
這個結果顯然與我們期望的相差很遠(請留意飄紅的字符們),其實我們需要的結果是這樣的:
1 <a href="
2 {url}
3 ">
4 {title}
5 </a><br />
基本上我們可以總結出如下規律(為了容易理解,我們這里只考慮{xxx}標記):
- 從開始到"{"之前的部分算作一個Token;
- "{"、"}"之間的部分(含"{"、"}")算作一個Token;
- "}"、"{"之間的部分(不含"}"、"{")算作一個Token;
- "}"到結尾的部分算作一個Token;
思路有了,那么算法如何實現呢?為了避免篇幅過長,我這里直接給出一個有限狀態機的解決方案。為了更加直觀的理解這個問題,請您現在將鼠標定位在字符串"<a href=""{url}"">{title}</a><br />"的開始處,然后使用方向鍵向右移動光標,觀察光標在每個位置(pos)的狀態,圖解如下:
這里出現了4個狀態,分別是“開始”、“進入目標”、“脫離目標”和“結束”。而在實際編碼過程中,我們通常忽略開始和結束,因為這兩個狀態始終都是需要處理的,而且各有且僅有1次,直接硬編碼實現即可。
題外話:如果您實在難以理解什么是“有限狀態機”的話,那么你可以簡單的理解為“狀態有限的機器(制)”,雖然這么說是非常不准確的,但這個可以幫助你去思考這個概念。另外可以參考“狀態機”。
將字符流轉化為Token流的過程
要利用有限狀態機,我們首先要定義一下業務狀態:
1 /// <summary>
2 /// 定義解析模式(即狀態)。
3 /// </summary>
4 public enum ParserMode
5 {
6 /// <summary>
7 /// 無狀態。
8 /// </summary>
9 None = 0,
10
11 /// <summary>
12 /// 進入標簽處理。
13 /// </summary>
14 EnterLabel = 1,
15
16 /// <summary>
17 /// 退出標簽處理。
18 /// </summary>
19 LeaveLabel = 2
20 }
在這里我們定義了三個狀態,實際上只需要兩個。None這個狀態在實踐中沒有實際意義,只是為了在編碼過程中讓語義更加接近現實(面向對象編程中會有很多這種情況)。遇到"{"或"}"的時候就進行狀態變換,而每次狀態變換都需要做一些處理動作,下面是算法的主體骨架:
1 // 這倆還需要解釋??
2 private const char _LABEL_OPEN_CHAR = '{';
3 private const char _LABEL_CLOSE_CHAR = '}';
4
5 [Test]
6 public void Test2()
7 {
8 var templateLength = TestObjects.TEMPLATE_STRING.Length;
9
10 // 為了模擬光標的定位移動,我們在這里采用for而不是foreach
11 // 在本例中用for還是foreach都無關緊要
12 // 以后我們還會討論更加復雜的情況,到時候就需要用到while(bool)了!
13 for (var index = 0; index < templateLength; index++)
14 {
15 var c = TestObjects.TEMPLATE_STRING[index];
16
17 switch (c)
18 {
19 case _LABEL_OPEN_CHAR:
20 // ...
21 this._EnterMode(ParserMode.EnterLabel);
22 break;
23
24 case _LABEL_CLOSE_CHAR:
25 // ...
26 this._LeaveMode();
27 break;
28
29 default:
30 // ...
31 break;
32 }
33 }
34
35 // 到達結尾的時候也需要處理寄存器中的內容
36 // 這就是之前提到的硬編碼解決開始和結束兩個狀態
37 // ...
38 }
在狀態變換之前,我們需要一系列的寄存器(臨時變量)來存儲當前狀態、歷史狀態(限於本例就是上次狀態)、歷史數據以及處理成功的Token等,定義如下:
1 /// <summary>
2 /// 表示 Token 順序集合(Token流)。
3 /// </summary>
4 private readonly List<string> _tokens = new List<string>();
5
6 // 為有限狀態機定義一個寄存器
7 // 注意:有限狀態機的理解在物理層的電路上和在編程概念上是相通的
8 private readonly StringBuilder _temp = new StringBuilder();
9
10 /// <summary>
11 /// 表示當前狀態。
12 /// </summary>
13 private ParserMode _currentMode;
14
15 /// <summary>
16 /// 表示上一狀態。
17 /// </summary>
18 /// <remarks>
19 /// 如果狀態多余兩個的話,我們總不能再定義一個"_last_last_Mode"吧!
20 /// 在狀態有多個的時候,需要使用 <see cref="Stack{T}"/> 來保存歷史
21 /// 狀態,這個我們將在解釋型模版引擎中用到。
22 /// </remarks>
23 private ParserMode _lastMode;
切換模式的時候需要對各個寄存器做相應的處理,我的注釋很詳細就不解釋了:
1 /// <summary>
2 /// 進入模式。
3 /// </summary>
4 /// <param name="mode"><see cref="ParserMode"/> 枚舉值之一。</param>
5 private void _EnterMode(ParserMode mode)
6 {
7 // 當狀態改變的時候應當保存之前已處理的寄存器中的內容
8 if (this._temp.Length > 0)
9 {
10 this._tokens.Add(this._temp.ToString());
11
12 this._temp.Clear();
13 }
14
15 this._lastMode = this._currentMode;
16 this._currentMode = mode;
17 }
18
19 /// <summary>
20 /// 離開模式。
21 /// </summary>
22 private void _LeaveMode()
23 {
24 // 當狀態改變的時候應當保存之前已處理的寄存器中的內容
25 // 當狀態超過2個的時候,實際上這里的代碼應該是不一樣的
26 // 雖然現在我們只需要考慮兩種狀態,但為了更加直觀的演示,我特意在這里又寫了一遍
27 if (this._temp.Length > 0)
28 {
29 this._tokens.Add(this._temp.ToString());
30 this._temp.Clear();
31 }
32
33 // 因為只有兩個狀態,因此
34 this._currentMode = this._lastMode;
35 }
然后再完善一下之前提到的主體骨架,測試,輸出結果如下:
1 <a href="
2 {url}
3 ">
4 {title}
5 </a><br />
我們得到了預期的結果!
將Token流輸出為業務數據
在上一節中我們曾經提到過Token流輸出時將標簽置換為業務數據的思路,如果您忘記了,那么請回去再看看吧!
有了思路,那么實現就非常容易了,聯合業務數據進行測試:
1 [Test]
2 public void Test3()
3 {
4 this.ParseTemplate(TestObjects.TEMPLATE_STRING);
5
6 foreach (var newsItem in TestObjects.NewsItems)
7 {
8 foreach (var token in this._tokens)
9 {
10 switch (token)
11 {
12 case "{url}":
13 Trace.Write(newsItem.Key);
14 break;
15
16 case "{title}":
17 Trace.Write(newsItem.Value);
18 break;
19
20 default:
21 Trace.Write(token);
22 break;
23 }
24 }
25
26 Trace.WriteLine(String.Empty);
27 }
28 }
經過測試輸出結果完全正確!
搞定!
總結及代碼下載
本文主要內容是闡述如何使用有限狀態機這種機制來完成“從字符流向Token流”的轉換的。不過本文為了降低入門門檻,一切舉例和算法都從簡,大家應該很容易上手!
要補充的是,本文並沒有真正的去封裝一個模板引擎,而僅僅是說明了其工作原理,我想這個比直接給大家一個已經實現的模板引擎要好的多,畢竟這是“漁”而不是“魚”。
本文代碼下載:置換型模板引擎(1-2).zip
下集預報:置換型模板引擎(三)將於清明節之后放出,屆時將會封裝一個簡單但完整的基於“按流替代式”的模板引擎,達到實用級別。
另外,請大家不要催促我博文的寫作,老陳畢竟不是打印機啊!哈哈!