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