2016 年 2 月

第 31 卷,第 2 期

本文章是由機器翻譯。

C# - C# 的可自訂指令碼

Vassili Kaplan

在本文我將告訴您如何建置自訂指令碼語言使用 C#,而不使用任何外部程式庫。指令碼語言為基礎的分割合併演算法剖析 C# 我曾介紹在 2015 年 10 月號的 MSDN Magazine 中的數學運算式 (msdn.com/magazine/mt573716)。

我可以透過使用自訂函式,來擴充剖析不只是數學運算式,但要剖析的可自訂的指令碼語言的分割合併演算法。「 標準 」 語言的控制流程陳述式 (如果,否則,雖然、 繼續、 中斷,依此類推) 可以加入自訂函式可能是其他一般指令碼語言功能 (作業系統命令,字串管理、 搜尋的檔案,依此類推)。

我要呼叫我可以自訂指令碼的 C# 或 CSCS 的語言。為什麼要建立另一個指令碼語言? 因為它是自訂的簡單語言。加入新的函式或新的控制流程陳述式可以採用任意數目的參數,會在幾行程式碼。此外,函式名稱和控制流程陳述式可在任何非英文語言案例中有一些組態變更,另外還會示範在這篇文章。並藉由查看 CSCS 語言實作的方式,您就能夠建立自己的自訂指令碼語言。

CSCS 的範圍

它是相當簡單的實作非常基本的指令碼語言,但要實作五顆星語言進行困難。我要限制的範圍 CSCS 這裡,就會知道有何影響:

  • CSCS 語言具有如果,否則,雖然、 繼續和中斷控制流程陳述式。巢狀陳述式,也支援。您將學習如何立即加入其他控制陳述式。
  • 沒有任何布林值。反而比撰寫 「 如果 (a),「 您必須撰寫 「 如果 (= = 1)。 」
  • 不支援邏輯運算子。反而比撰寫 」 如果 (= = 1,b = = 2),"撰寫巢狀的 ifs: 「 如果 (= = 1) {如果 (b = = 2) {…}}。 」
  • 函式和方法中,不支援 CSCS,但可以以 C# 撰寫並向分割合併剖析器可與 CSCS。
  • 只有 「 //"-style 支援註解。
  • 支援變數和一維陣列,所有在全域層級定義。變數可以保留數字、 字串或 tuple (實作為清單) 的其他變數。不支援多維陣列。

[圖 1 CSCS 中會顯示"Hello,World!"的程式。由於 「 列印 」 的輸入錯誤,程式會顯示錯誤訊息結尾: 「 無法剖析語彙基元 [pint] 」。 請注意,所有先前的陳述式執行成功。亦即,CSCS 是解譯器。

"Hello,World!"中 CSCS
圖 1"Hello,World!"中 CSCS

修改分割合併演算法

我做了兩個變更的分割合併演算法中分割的一部分。(合併組件會保持相同。)

第一個變更就剖析運算式的結果是現在數字、 字串或 tuple 的值 (每一個都可以是字串或數字),而不是數字。我建立下列 Parser.Result 類別來儲存套用分割合併演算法的結果:

public class Result
{
  public Result(double dRes = Double.NaN, 
    string sRes = null, 
    List<Result> tRes = null)
  {
    Value  = dResult;
    String = sResult;
    Tuple  = tResult;
  }
  public double
       Value  { get; set; }
  public string
       String { get; set; }
  public List<Result> Tuple  { get; set; }
}

第二處修改是現在會執行分割的一部分,不只是直到停止剖析的字元 —) 或 \n—is 找不到,但直到任何字元,在找到的停止剖析的字元傳遞的陣列。這是必要的比方說,如果第一個引數的剖析時陳述式,其中分隔符號可以是任何 <>、、 或 = 字元。

您可以看一下修改過的分割合併演算法伴隨原始程式碼下載中。

解譯器

類別負責解譯 CSCS 程式碼會呼叫解譯器。它已實作為單一值,也就是類別定義,其中可以有只有一個執行個體的類別。在其 Init 方法中,剖析器 (請參閱先前提及的原始文章),解譯器所使用的所有函式以初始化:

public void Init()
{
  ParserFunction.AddFunction(Constants.IF,
        new IfStatement(this));
  ParserFunction.AddFunction(Constants.WHILE,
     new WhileStatement(this));
  ParserFunction.AddFunction(Constants.CONTINUE,
  new ContinueStatement());
  ParserFunction.AddFunction(Constants.BREAK,
     new BreakStatement());
  ParserFunction.AddFunction(Constants.SET,
       new SetVarFunction());
...
}

在 Constants.cs 檔案中,定義 CSCS 中使用的實際名稱:

...
public const string IF          = "if";
public const string ELSE        = "else";
public const string ELSE_IF     = "elif";
public const string WHILE       = "while";
public const string CONTINUE    = "continue";
public const string BREAK       = "break";
public const string SET         = "set";

剖析器與已註冊的任何函數必須實作為衍生自 ParserFunction 類別的類別,而且必須覆寫其評估方法。

第一件事,解譯器時啟動指令碼處理指令碼將移除以簡化所有泛空白字元 (除非它們在字串內),和所有註解。因此,空格或新行不能做為運算子分隔符號。運算子分隔字元和註解字串會定義在 Constants.cs,以及:

public const char END_STATEMENT = ';';
public const string COMMENT     = "//";

變數和陣列

CSCS 支援數字 (類型 double)、 字串或多個 tuple (變數陣列,實作為 C# 清單)。Tuple 的每個項目可以是字串或數字,但不是其他的 tuple。因此,不支援多維陣列。若要定義變數,會使用 CSCS 函式 「 set 」。C# 類別 SetVarFunction 實作的功能設定變數的值,如所示 [圖 2

[圖 2 實作的 Set 變數函式

class SetVarFunction : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    string varName = Utils.GetToken(data, ref from, Constants.NEXT_ARG_ARRAY);
    if (from >= data.Length)
    {
      throw new ArgumentException("Couldn't set variable before end of line");
    }
    Parser.Result varValue = Utils.GetItem(data, ref from);
    // Check if the variable to be set has the form of x(i),
    // meaning that this is an array element.
    int arrayIndex = Utils.ExtractArrayElement(ref varName);
    if (arrayIndex >= 0)
    {
      bool exists = ParserFunction.FunctionExists(varName);
      Parser.Result  currentValue = exists ?
            ParserFunction.GetFunction(varName).GetValue(data, ref from) :
            new Parser.Result();
      List<Parser.Result> tuple = currentValue.Tuple == null ?
                                  new List<Parser.Result>() :
                                  currentValue.Tuple;
      if (tuple.Count > arrayIndex)
      {
        tuple[arrayIndex] = varValue;
      }
      else
      {
        for (int i = tuple.Count; i < arrayIndex; i++)
        {
          tuple.Add(new Parser.Result(Double.NaN, string.Empty));
        }
        tuple.Add(varValue);
      }
      varValue = new Parser.Result(Double.NaN, null, tuple);
    }
    ParserFunction.AddFunction(varName, new GetVarFunction(varName, varValue));
    return new Parser.Result(Double.NaN, varName);
  }
}

以下是一些 CSCS 中定義變數的範例:

set(a, "2 + 3");  // a will be equal to the string "2 + 3"
set(b, 2 + 3);    // b will be equal to the number 5
set(c(2), "xyz"); // c will be initialized as a tuple of size 3 with c(0) = c(1) = ""

請注意,沒有任何特殊陣列的宣告: 只定義索引的變數會初始化陣列,如果它尚未初始化,並加入空白項目,如有必要。在上述範例中,項目 c(0) 和 c(1) 已加入,兩者都初始化為空字串。如此,在檢視中,不必要的步驟所需的大多數指令碼語言的宣告陣列的第一次。

所有 CSCS 變數陣列會都建立和使用 CSCS 函式 (例如集或後端)。它們定義具有全域領域,而可供稍後只呼叫變數的名稱或索引的變數。在 C# 中,這實作中所示的 GetVarFunction [圖 3

[圖 3 實作 Get 變數函式

class GetVarFunction : ParserFunction
{
  internal GetVarFunction(Parser.Result value)
  {
    m_value = value;
  }
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    // First check if this element is part of an array:
    if (from < data.Length && data[from - 1] == Constants.START_ARG)
    {
      // There is an index given - it may be for an element of the tuple.
      if (m_value.Tuple == null || m_value.Tuple.Count == 0)
      {
        throw new ArgumentException("No tuple exists for the index");
      }
      Parser.Result index = Utils.GetItem(data, ref from, true /* expectInt */);
      if (index.Value < 0 || index.Value >= m_value.Tuple.Count)
      {
        throw new ArgumentException("Incorrect index [" + index.Value +
          "] for tuple of size " + m_value.Tuple.Count);
      }
      return m_value.Tuple[(int)index.Value];
    }
    // This is the case for a simple variable, not an array:
    return m_value;
  }
  private Parser.Result m_value;
}

設定變數函數必須向剖析器:

ParserFunction.AddFunction(Constants.SET, new SetVarFunction());

取得變數的函式會註冊內 set 變數函式 C# 程式碼 (請參閱中的下一個到最後一個陳述式 [圖 2):

ParserFunction.AddFunction(varName, new GetVarFunction(varName, varValue));

取得變數 CSCS 中的部分範例如下:

append(a, "+ 5"); // a will be equal to the string "2 + 3 + 5"
set(b, b * 2);    // b will be equal to the number 10 (if it was 5 before)

控制流程: 如果 Else If,其他

If Else 如果和 Else 控制流程陳述式會以剖析器函式,以及在內部實作。就像任何其他函式的剖析器會登錄:

ParserFunction.AddFunction(Constants.IF, new IfStatement(this));

IF 關鍵字必須向剖析器。IfStatement 實作內部,將會處理 ELSE_IF 和 ELSE 陳述式:

class IfStatement : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    m_interpreter.CurrentChar = from;
    Parser.Result result = m_interpreter.ProcessIf();
    return result;
  }
  private Interpreter m_interpreter;
}

實際的實作的 If 陳述式是在解譯器類別中,所顯示的 [圖 4

[圖 4 實作的 If 陳述式

internal Parser.Result ProcessIf()
{
  int startIfCondition = m_currentChar;
  Parser.Result result = null;
  Parser.Result arg1 = GetNextIfToken();
  string comparison  = Utils.GetComparison(m_data, ref m_currentChar);
  Parser.Result arg2 = GetNextIfToken();
  bool isTrue = EvalCondition(arg1, comparison, arg2);
  if (isTrue)
  {
    result = ProcessBlock();
    if (result is Continue || result is Break)
    {
      // Got here from the middle of the if-block. Skip it.
      m_currentChar = startIfCondition;
      SkipBlock();
    }
    SkipRestBlocks();
    return result;
  }
  // We are in Else. Skip everything in the If statement.
  SkipBlock();
  int endOfToken = m_currentChar;
  string nextToken = Utils.GetNextToken(m_data, ref endOfToken);
  if (ELSE_IF_LIST.Contains(nextToken))
  {
    m_currentChar = endOfToken + 1;
    result = ProcessIf();
  }
  else if (ELSE_LIST.Contains(nextToken))
  {
    m_currentChar = endOfToken + 1;
    result = ProcessBlock();
  }
  return result != null ? result : new Parser.Result();
}

它已明確指定,如果條件的形式: 引數 1、 比較號、 引數 2:

Parser.Result arg1 = GetNextIfToken();
string comparison  = Utils.GetComparison(m_data, ref m_currentChar);
Parser.Result arg2 = GetNextIfToken();
bool isTrue = EvalCondition(arg1, comparison, arg2);

這是選擇性的 AND、 OR 或 NOT 陳述式可以在何處加入。

EvalCondition 函式只會比較根據比較正負號的語彙基元:

internal bool EvalCondition(Parser.Result arg1, string comparison, Parser.Result arg2)
{
  bool compare = arg1.String != null ? CompareStrings(arg1.String, comparison, arg2.String) :
                                       CompareNumbers(arg1.Value, comparison, arg2.Value);
  return compare;
}

以下是數值比較的實作:

internal bool CompareNumbers(double num1, string comparison, double num2)
{
  switch (comparison) {
    case "==": return num1 == num2;
    case "<>": return num1 != num2;
    case "<=": return num1 <= num2;
    case ">=": return num1 >= num2;
    case "<" : return num1 <  num2;
    case ">" : return num1 >  num2;
    default: throw new ArgumentException("Unknown comparison: " + comparison);
  }
}

字串比較類似,可在隨附的程式碼下載中,因為是 GetNextIfToken 函式的簡單實作。

當 if 時、 else if 或 else 條件為 true,處理完所有的區塊內的陳述式。這實作在 [圖 5 ProcessBlock 方法中。如果條件不符合,則會略過的所有陳述式。這被實作在 SkipBlock 方法 (請參閱伴隨的原始程式碼)。

[圖 5 ProcessBlock 方法實作

internal Parser.Result ProcessBlock()
{
  int blockStart = m_currentChar;
  Parser.Result result = null;
  while(true)
  {
    int endGroupRead = Utils.GoToNextStatement(m_data, ref m_currentChar);
    if (endGroupRead > 0)
    {
      return result != null ? result : new Parser.Result();
    }
    if (m_currentChar >= m_data.Length)
    {
      throw new ArgumentException("Couldn't process block [" +
                                   m_data.Substring(blockStart) + "]");
    }
    result = Parser.LoadAndCalculate(m_data, ref m_currentChar,
      Constants.END_PARSE_ARRAY);
    if (result is Continue || result is Break)
    {
      return result;
    }
  }
}

請注意,「 繼續 」 和 「 中斷 」 陳述式內時的使用方式迴圈。這些陳述式會做為函式,以及實作。以下是繼續:

class Continue : Parser.Result  { }
class ContinueStatement : ParserFunction
{
  protected override Parser.Result
    Evaluate(string data, ref int from)
  {
    return new Continue();
  }
}

Break 陳述式的實作很相似。兩者都進行登錄與任何其他函式剖析器:

ParserFunction.AddFunction(Constants.CONTINUE,  new ContinueStatement());
ParserFunction.AddFunction(Constants.BREAK,     new BreakStatement());

您可以使用符號函式來發揮巢狀 If 區塊或來發揮 while 迴圈。

控制流程: While 迴圈

While 迴圈也實作並向剖析器做為函式:

ParserFunction.AddFunction(Constants.WHILE,     new WhileStatement(this));

每當關鍵字會經過剖析,則稱為 Evaluate 方法的 WhileStatement 物件:

class WhileStatement : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    string parsing = data.Substring(from);
    m_interpreter.CurrentChar = from;
    m_interpreter.ProcessWhile();
    return new Parser.Result();
  }
  private Interpreter m_interpreter;
}

因此實際的實作的 while 迴圈會在解譯器類別中,顯示 [圖 6

[圖 6 實作的 While 迴圈

internal void ProcessWhile()
{
  int startWhileCondition = m_currentChar;
  // A heuristic check against an infinite loop.
  int cycles = 0;
  int START_CHECK_INF_LOOP = CHECK_AFTER_LOOPS / 2;
  Parser.Result argCache1 = null;
  Parser.Result argCache2 = null;
  bool stillValid = true;
  while (stillValid)
  {
    m_currentChar = startWhileCondition;
    Parser.Result arg1 = GetNextIfToken();
    string comparison = Utils.GetComparison(m_data, ref m_currentChar);
    Parser.Result arg2 = GetNextIfToken();
    stillValid = EvalCondition(arg1, comparison, arg2);
    int startSkipOnBreakChar = m_currentChar;
    if (!stillValid)
    {
      break;
    }
    // Check for an infinite loop if same values are compared.
    if (++cycles % START_CHECK_INF_LOOP == 0)
    {
      if (cycles >= MAX_LOOPS || (arg1.IsEqual(argCache1) &&
        arg2.IsEqual(argCache2)))
      {
        throw new ArgumentException("Looks like an infinite loop after " +
          cycles + " cycles.");
      }
      argCache1 = arg1;
      argCache2 = arg2;
    }
    Parser.Result result = ProcessBlock();
    if (result is Break)
    {
      m_currentChar = startSkipOnBreakChar;
      break;
    }
  }
  // The while condition is not true anymore: must skip the whole while
  // block before continuing with next statements.
  SkipBlock();
}

請注意,while 迴圈主動 CHECK_AFTER_LOOPS 常數所定義的組態設定中的反覆運算次數後檢查無限迴圈。啟發學習法是,如果完全相同值的 while 條件會比較數個迴圈,這可能表示無限的迴圈。[圖 7 顯示一段迴圈忘記遞增週期變數 內部的 while 迴圈。

偵測無限 While 迴圈中 CSCS
[圖 7 偵測無限的 While 迴圈 CSCS

函式,函式,函式

為了讓 CSCS 來做更有用的事情,更增加細節要加入的需求也就是必須實作多個函式。將新的函式加入至 CSCS 很簡單: 先實作衍生自 ParserFunction 類別 (覆寫評估方法),然後再將它登錄的剖析器。以下是列印函式的實作:

class PrintFunction : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    List<string> args = Utils.GetFunctionArgs(data, ref from);
    m_interpreter.AppendOutput(string.Join("", args.ToArray()));
    return new Parser.Result();
  }
  private Interpreter m_interpreter;
}

函式會列印任何數目的逗號分隔的引數傳遞給它。實際讀取的引數是在 GetFunctionArgs 輔助函數,傳回所有傳遞的引數為字串的清單。您可以看一下函式中隨附的原始程式碼。

第二個和最後一個步驟是向程式初始化部分中的剖析器的列印函式:

ParserFunction.AddFunction(Constants.PRINT,     new PrintFunction(this));

Constants.PRINT 常數定義為 「 列印 」。

[圖 8 顯示啟動新的處理序的函式的實作。

[圖 8 執行程序函式實作

class RunFunction : ParserFunction
{
  internal RunFunction(Interpreter interpreter)
  {
    m_interpreter = interpreter;
  }
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    string processName = Utils.GetItem(data, ref from).String;
    if (string.IsNullOrWhiteSpace(processName))
    {
      throw new ArgumentException("Couldn't extract process name");
    }
    List<string> args = Utils.GetFunctionArgs(data, ref from);
    int processId = -1;
    try
    {
      Process pr = Process.Start(processName, string.Join("", args.ToArray()));
      processId = pr.Id;
    }
    catch (System.ComponentModel.Win32Exception exc)
    {
      throw new ArgumentException("Couldn't start [" + processName + "]:
        " + exc.Message);
    }
    m_interpreter.AppendOutput("Process " + processName + " started, id:
      " + processId);
    return new Parser.Result(processId);
  }
  private Interpreter m_interpreter;
}

以下是如何尋找檔案、 啟動和終止處理序,以及列印 CSCS 中的某些值:

 

set(b, findfiles("*.cpp", "*.cs"));
set(i, 0);
while(i < size(b)) {
  print("File ", i, ": ", b(i));
  set(id, run("notepad", b(i)));
  kill(id);
  set(i, i+ 1);
}

[圖 9 列出可下載的原始程式碼,以及簡短的描述中所實作的函式。大部分的函式是透過對應的 C# 函式的包裝函式。

[圖 9 CSCS 函式

abs 取得運算式的絕對值
附加 將字串或數字 (以字串再轉換) 附加至字串
cd 變更目錄
cd... 變更目錄向上一個層級
目錄 顯示目前目錄的內容
enc 取得環境變數的內容
exp 指數函式
findfiles 尋找具有特定模式的檔案
findstr 尋找檔案,其中包含具有特定模式的字串
indexof 傳回的索引子字串或-1,如果找不到
kill 刪除具有指定的處理序 id 編號的程序
pi 傳回 pi 常數的近似值
pow 傳回第一個引數的第二個引數
print 列印指定的引數清單 (數字和清單會轉換為字串)
psinfo 傳回處理特定處理序名稱的資訊
pstime 傳回此處理程序; 的處理器總處理時間適用於測量時間
pwd 顯示目前的目錄路徑名稱
執行 開始具有指定的引數清單的程序,並傳回處理序識別碼
setenv 設定環境變數的值
設定 設定變數或陣列元素的值
sin 傳回指定的引數的正弦函數值
大小 傳回字串的長度或清單的大小
sqrt 傳回給定數字的平方根
substr 傳回從指定的索引字串的子字串
tolower 將字串轉換成小寫
toupper 將字串轉換成大寫

國際化

請注意,您可以註冊多個標籤 (函式名稱) 對應至相同的函式,以剖析器。如此一來,就可以加入任意數目的其他語言。

加入翻譯註冊相同的 C# 物件與另一個字串所組成。對應的 C# 程式碼如下:

var languagesSection =
  ConfigurationManager.GetSection("Languages") as NameValueCollection;
string languages = languagesSection["languages"];
foreach(string language in languages.Split(",".ToCharArray());)
{
  var languageSection =
    ConfigurationManager.GetSection(language) as NameValueCollection;
  AddTranslation(languageSection, Constants.IF);
  AddTranslation(languageSection, Constants.WHILE);
...
}

AddTranslation 方法就會將現有的函式的同義字:

public void AddTranslation(NameValueCollection languageDictionary, string originalName)
{
  string translation = languageDictionary[originalName];
  ParserFunction originalFunction =
    ParserFunction.GetFunction(originalName);
  ParserFunction.AddFunction(translation, originalFunction);
}

由於 C# 支援 Unicode 的大部分語言可以加入這種方式。請注意,變數名稱可以是 Unicode,以及。

在組態檔中已指定所有的翻譯。這是組態檔西班牙文的外觀:

<Languages>
  <add key="languages" value="Spanish" />
</Languages>
<Spanish>
    <add key="if"    value ="si" />
    <add key="else"  value ="sino" />
    <add key="elif"  value ="sinosi" />
    <add key="while" value ="mientras" />
    <add key="set"   value ="asignar" />
    <add key="print" value ="imprimir" />
 ...
</Spanish>

以下是以西班牙文 CSCS 程式碼範例:

asignar(a, 5);
mientras(a > 0) {
  asignar(expr, 2*(10 – a*3));
  si (expr > 0) {
    imprimir(expr, " es mayor que cero");
  }
  sino {
    imprimir(expr, " es cero o menor que cero");
  }
  asignar(a, a - 1);
}

請注意,剖析器可以立即處理控制陳述式和英文和西班牙文中的函式。沒有任何限制,您可以新增的語言數目。

總結

所有 CSCS 項 — 控制流程陳述式、 變數、 陣列和函式,由定義為 C# 類別衍生自 ParserFunction 基底類別並覆寫其評估方法。接著您會使用剖析器註冊此類別的物件。此方法會提供下列優點:

  • 模組化: 每個 CSCS 函式和控制流程陳述式都位於自己的類別,所以很容易用來定義新的函式或控制流程陳述式,或修改現有。
  • 彈性: 它是可以在任何語言的 CSCS 關鍵字和函式名稱。必須修改組態檔。不同於大多數其他語言,CSCS 控制流程陳述式中的函式和變數名稱不一定要以 ASCII 字元。

當然,在這個階段 CSCS 語言是列並不完整。以下是一些讓它更有用的方法:

  • 建立多維陣列。可用的相同 C# 資料結構的一維陣列,清單 < 結果 >。不過,取得及設定多維陣列的項目時,必須加入更多的剖析功能。
  • 啟用要在同一行上初始化的 tuple。
  • 新增邏輯運算子 (AND、 OR、 不等),就可能非常有用,如 if 和 while 陳述式。
  • 新增的功能來撰寫 CSCS 函式和方法。目前,您可以使用僅先前撰寫和編譯 C# 中的函式。
  • 新增要包含其他單位 CSCS 原始碼的功能。
  • 加入多個執行作業系統相關的一般工作的函式。因為可以輕鬆地實作大部分這類工作,在 C# 中,大部分是只是精簡型包裝函式 C# 對應項目。
  • 建立的捷徑,set (a、 b) 函式做為 「 = b。 」

我希望您喜歡這個初探 CSCS 語言,以及查看如何建立您自己的自訂指令碼語言。


Vassili Kaplan是先前的 Microsoft Lync 開發人員。他是熱衷於在 C# 和 c + + 程式設計。他目前住在蘇黎世,瑞士並針對各種銀行 freelancer 擔任。您可以透過他的連絡網址 iLanguage.ch

感謝以下的微軟技術專家對本文的審閱: James McCaffrey