受到群里兄弟们的竭力邀请,老陈终于决定来分享一下.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
下集预报:置换型模板引擎(三)将于清明节之后放出,届时将会封装一个简单但完整的基于“按流替代式”的模板引擎,达到实用级别。
另外,请大家不要催促我博文的写作,老陈毕竟不是打印机啊!哈哈!