設計模式(十五)直譯器

冬先生發表於2023-11-28

一、定義

定義一個語言的文法,並且建立一個直譯器來解釋該語言中的句子,這裡的“語言”是指使用規定格式和語法的程式碼。直譯器模式是一種行為型模式。

二、描述

直譯器模式是一種使用頻率相對較低但學習難度較大的設計模式,它主要用於描述如何使用面嚮物件語言構成一個簡單的語言直譯器,包含以下四個角色:
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:環境類

/// <summary>
/// 環境類:用於儲存和操作需要解釋的語句,
/// 在本例項中每一個需要解釋的單詞都可以稱為一個動作標記(ActionToker)或命令
/// </summary>
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<Node> nodeList = new List<Node>();

    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)其執行效率較低。由於在直譯器模式中使用了大量的迴圈和遞迴呼叫,因此在解釋較為複雜的句子時其速度很慢,而且程式碼的除錯過程也比較麻煩。

相關文章