由浅入深:自己动手开发模板引擎——置换型模板引擎(二)
受到群里兄弟們的竭力邀請,老陳終于決定來分享一下.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流。
方案
解決這個問題的方案大致有這么幾種:
今天我們只討論第三種情況,第一種很簡單,第二種稍微復雜一點,但相信難不倒您!第三種做法只是不太常見,但如果您接觸過編譯原理(或搜索引擎開發),就不是那么陌生了。
思路
首先,我們看看這段模板代碼按字符流輸出回是怎樣的:
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}標記):
思路有了,那么算法如何實現呢?為了避免篇幅過長,我這里直接給出一個有限狀態機的解決方案。為了更加直觀的理解這個問題,請您現在將鼠標定位在字符串"<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
下集預報:置換型模板引擎(三)將于清明節之后放出,屆時將會封裝一個簡單但完整的基于“按流替代式”的模板引擎,達到實用級別。
另外,請大家不要催促我博文的寫作,老陳畢竟不是打印機啊!哈哈!
總結
以上是生活随笔為你收集整理的由浅入深:自己动手开发模板引擎——置换型模板引擎(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 由浅入深:自己动手开发模板引擎——置换型
- 下一篇: 由浅入深:自己动手开发模板引擎——置换型