一、定义

定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种行为型模式。

二、描述

解释器模式是一种使用频率相对较低但学习难度较大的设计模式,它主要用于描述如何使用面向对象语言构成一个简单的语言解释器,包含以下四个角色:
1、AbstractExpression(抽象表达式)在抽象表达式中声明了抽象的解释操作,它是所有终结符表达式和非终结表达式的公共父类。
2、TerminalExpression(终结符表达式):TerminalExpression(终结符表达式):终结符表达式是抽象表达式的子类,它实现了与文法中的终结符相关联的解释操作,在句子中的每一个终结符都是该类的一个实例。通常,在一个解释器模式中只有少数几个终结符表达式类,它们的实例可以通过非终结符表达式组成较为复杂的句子。
3、NonterminalExpression(非终结符表达式):非终结符表达式也是抽象表达式的子类,它实现了文法中非终结符的解释操作,由于在非终结符表达式中可以包含终结符表达式,也可以继续包含非终结符表达式,因此其解释操作一般通过递归的方式来完成。
4、Context(环境类):环境类又称为上下文类,它用于存储解释器之外的一些全局信息,通常临时存储了需要解释的语句。

三、例子

X公司开发了一套简单的基于字符界面的格式化指令,可以根据输入的指令在字符界面输出一些格式化内容,例如输入“LOOP 2 PRINT 杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉”,将输出以下结果:其中,关键词LOOP表示循环,后面的数字表示循环次数;PRINT表示打印,后面的字符串表示打印的内容;SPACE表示空格;BREAK表示换行;END表示循环结束。每一个关键词对应一条指令,计算机程序将根据关键词执行相应的处理操作。
Context:环境类

/// /// 环境类:用于存储和操作需要解释的语句,/// 在本实例中每一个需要解释的单词都可以称为一个动作标记(ActionToker)或命令/// public class Context{    private int index = -1;    private string[] tokens;    private string currentToken;    public Context(string text)    {        text = text.Replace("  ", " ");        tokens = text.Split(' ');        NextToken();    }    // 获取下一个标记    public string NextToken()    {        if (index < tokens.Length - 1)        {            currentToken = tokens[++index];        }        else        {            currentToken = null;        }        return currentToken;    }    // 返回当前的标记    public string GetCurrentToken()    {        return currentToken;    }    // 跳过一个标记    public void SkipToken(string token)    {        if (!token.Equals(currentToken, StringComparison.OrdinalIgnoreCase))        {            Console.WriteLine("错误提示:{0} 解释错误!", currentToken);        }        NextToken();    }    // 如果当前的标记是一个数字,则返回对应的数值    public int GetCurrentNumber()    {        int number = 0;        try        {            // 将字符串转换为整数            number = Convert.ToInt32(currentToken);        }        catch (Exception ex)        {            Console.WriteLine("错误提示:{0}", ex.Message);        }        return number;    }}

Node:抽象节点类,充当抽象表达式

public abstract class Node{    // 声明一个方法用于解释语句    public abstract void Interpret(Context context);    // 声明一个方法用于执行标记对应的命令    public abstract void Execute();}

ExpressionNode、CommandNode、LoopCommandNode:表达式节点类、语句命令节点类、循环命令类,充当非终结符表达式

public class ExpressionNode : Node{    // 用于存储多条命令的集合    private IList nodeList = new List();    public override void Interpret(Context context)    {        // 循环处理Context中的标记        while (true)        {            // 如果已经没有任何标记,则退出解释            if (context.GetCurrentToken() == null)            {                break;            }            // 如果标记为END,则不解释END并结束本次解释过程,可以继续之后的解释            else if (context.GetCurrentToken().Equals("END", StringComparison.OrdinalIgnoreCase))            {                context.SkipToken("END");                break;            }            // 如果为其它标记,则解释标记并加入命令集合            else            {                Node node = new CommandNode();                node.Interpret(context);                nodeList.Add(node);            }        }    }    // 循环执行命令集合中的每一条指令    public override void Execute()    {        foreach (var node in nodeList)        {            node.Execute();        }    }}public class CommandNode : Node{    private Node node;    public override void Interpret(Context context)    {        // 处理LOOP指令        if (context.GetCurrentToken().Equals("LOOP", StringComparison.OrdinalIgnoreCase))        {            node = new LoopCommand();            node.Interpret(context);        }        // 处理其他指令        else        {            node = new PrimitiveCommand();            node.Interpret(context);        }    }    public override void Execute()    {        node.Execute();    }}public class LoopCommand : Node{    // 循环次数    private int number;    // 循环语句中的表达式    private Node commandNode;    public override void Interpret(Context context)    {        context.SkipToken("LOOP");        number = context.GetCurrentNumber();        context.NextToken();        // 循环语句中的表达式        commandNode = new ExpressionNode();        commandNode.Interpret(context);    }    public override void Execute()    {        for (int i = 0; i < number; i++)        {            commandNode.Execute();        }    }}

PrimitiveCommandNode:基本命令类,充当终结符表达式

public class PrimitiveCommand : Node{    private string name;    private string text;    public override void Interpret(Context context)    {        name = context.GetCurrentToken();        context.SkipToken(name);        if (!name.Equals("PRINT", StringComparison.OrdinalIgnoreCase)             && !name.Equals("BREAK", StringComparison.OrdinalIgnoreCase)            && !name.Equals("SPACE", StringComparison.OrdinalIgnoreCase))        {            Console.WriteLine("非法命令!");        }        if (name.Equals("PRINT", StringComparison.OrdinalIgnoreCase))        {            text = context.GetCurrentToken();            context.NextToken();        }    }    public override void Execute()    {        if (name.Equals("PRINT", StringComparison.OrdinalIgnoreCase))        {            Console.Write(text);        }        else if (name.Equals("SPACE", StringComparison.OrdinalIgnoreCase))        {            Console.Write(" ");        }        else if (name.Equals("BREAK", StringComparison.OrdinalIgnoreCase))        {            Console.Write("\r\n");        }    }}

Program:客户端测试类

string instruction = "LOOP 2 PRINT 杨过 SPACE SPACE PRINT 小龙女 BREAK END PRINT 郭靖 SPACE SPACE PRINT 黄蓉";Context context = new Context(instruction);Node node = new ExpressionNode();node.Interpret(context);Console.WriteLine("源指令 : {0}", instruction);Console.WriteLine("解释后 : ");node.Execute();Console.ReadLine();

四、总结1、优点

(1)解释器模式易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
(2)在解释器模式中,每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
(3)实现文法较为容易。在抽象语法树中每一个表达式结点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成结点类代码。
(4)增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合开闭原则。

2、缺点

(1)解释器模式对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义-个类,因此如果一个语言包含太多的文法规则,类的个数将会急剧增加,从而导致系统难以管理和维护,此时可以考虑使用语法分析程序等方式来取代解释器模式。
(2)其执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。

测试签名