February 2016

Volume 31 Number 2

C# - カスタマイズ可能な C# スクリプト

Vassili Kaplan

ここでは、外部ライブラリを一切使用しないで、C# を使ってカスタム スクリプト言語をビルドする方法を取り上げます。このスクリプト言語は、2015 年 10 月号の MSDN マガジン記事 (msdn.com/magazine/mt573716) で取り上げた C# の数式を解析する分割結合アルゴリズムを基にしています。

カスタム関数を使うことで、分割結合アルゴリズムを拡張し、数式だけでなくカスタマイズ可能なスクリプト言語も解析できるようにします。「標準」の言語フロー制御ステートメント (if、else、while、continue、break など) は、他の一般的なスクリプト言語機能 (OS コマンド、文字列操作、ファイルの検索など) と同様に、カスタム関数として追加します。

ここでは、作成する言語を「カスタマイズ可能な C# スクリプト」 (CSCS) と呼びます。スクリプト言語を新しく作成する理由は、カスタマイズを容易にするためです。任意の数のパラメーターを受け取る新しい関数や新しいフロー制御ステートメントを、わずか数行のコードを記述するだけで追加できるようにします。さらに、関数名とフロー制御ステートメントは、構成に若干の変更を加えるだけで、英語以外の任意の言語シナリオでも使用できるようになります。ここでは、その方法についても紹介します。今回の CSCS 言語の実装方法を見れば、独自のカスタムスクリプト言語を作成できるでしょう。

CSCS の対象範囲

ごく基本的なスクリプト言語の実装であれば簡単ですが、言語が高度になると難易度は格段に上がります。話の展開がわかりやすくなるように、CSCS の対象範囲を以下のように制限することにします。

  • CSCS 言語には、フロー制御ステートメントとして if、else if、else、while、continue、および break があります。フロー制御ステートメントは入れ子にすることもできます。後ほど制御ステートメントを実行時に追加する方法も取り上げます。
  • ブール値はありません。"if (a)" ではなく、"if (a == 1)" のように記述しなければなりません。
  • 論理演算子はサポートしません。"if (a ==1 and b == 2)" ではなく、if を入れ子にして "if (a == 1) { if (b == 2) { … } }" のように記述します。
  • CSCS は関数とメソッドをサポートしません。しかし、C# で関数とメソッドを記述し、分割結合パーサーに登録して CSCS で使用可能にすることはできます。
  • "//" 形式のコメントのみをサポートします。
  • 変数と 1 次元配列をサポートし、すべてグローバル レベルで定義します。変数は、数値、文字列、または他の変数のタプル (リストとして実装) を保持できます。多次元配列はサポートしません。

図 1 は、CSCS での "Hello, World!" プログラムを示しています。"print" の入力ミスにより、このプログラムでは最後に「Couldn't parse token [pint]」(トークン [pint] を解析できませんでした) というエラーが表示されています。上記のステートメントがすべて正常に実行されているのがわかります。つまり、CSCS はインタープリターです。

CSCS での "Hello, World!"
図 1 CSCS での "Hello, World!"

分割結合アルゴリズムの変更

今回、分割結合アルゴリズムの分割部分に 2 か所変更を加えました (結合部分は変更していません)。

1 つ目の変更点として、式の解析結果に数値だけでなく、文字列や値のタプル (それぞれが文字列または数値です) も使用できるようにしました。分割結合アルゴリズムの適用結果を格納するために、次の 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; }
}

2 つ目の変更点として、分割部分の実行終了の判定方法を変更しました。これまでは解析終了を示す文字、")" または "\n" を検出するまで実行していましたが、今回はこうした解析停止文字を配列として渡し、それらの文字のいずれかが検出されるまで実行するようにしました。たとえば、If ステートメントの 1 つ目の引数を解析する場合、区切り文字は <、>、または = のいずれにもなり得るので、この変更が必要になります。

変更後の分割結合アルゴリズムは、付属のソース コード ダウンロードで確認できます。

インタープリター

CSCS コードの解釈を行うクラスがインタープリターです。インタープリターはシングルトンとして実装します。つまり、そのクラスのインスタンスは 1 つしか作成できないというクラス定義です。インタープリターの 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 クラスから派生したクラスとして実装し、そのクラスの Evaluate メソッドをオーバーライドする必要があります。

インタープリターでは、スクリプトの操作の出発点として、まず、すべての空白文字 (文字列に含まれるものは除く) とコメントを削除してスクリプトを簡潔にします。したがって、空白文字や改行は、演算子の区切り文字として使用できません。演算子の区切り文字とコメント文字列も、Constants.cs で定義します。

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

変数と配列

CSCS は、数値 (Double 型)、文字列、およびタプル (C# リストとして実装する変数の配列) をサポートします。タプルの各要素には文字列または数値を使用できますが、別のタプルを要素にすることはできません。したがって、多次元配列はサポートしません。変数を定義するには、CSCS 関数の "set" を使用します。C# クラスの SetVarFunction には、変数値を設定する機能を実装します (図 2 参照)。

図 2 変数設定関数の実装

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 の変数と配列はすべて、(set や append のような) CSCS 関数を使用して作成します。これらの変数や配列は、すべてグローバル スコープで定義するため、それ以降は変数名やインデックス付きの変数を呼び出すだけで使用できます。C# では、図 3 で示す GetVarFunction でこれを実装します。

図 3 変数取得関数の実装

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());

変数取得関数は、変数設定関数内部の C# コードで登録します (図 2 の最後から 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)

フロー制御: If、Else If、Else

フロー制御ステートメントの If、Else If、Else も、パーサー関数として内部に実装します。これらのステートメントは、他の関数と同様、パーサーが登録します。

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

パーサーに登録する必要があるキーワードは IF のみです。ELSE_IF ステートメントと ELSE ステートメントは、IfStatement 実装内部で処理します。

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();
}

上記では、If の条件が「引数 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 メソッドに実装しています。条件が true でなければ、すべてのステートメントをスキップします。この処理は、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;
    }
  }
}

while ループ内の "Continue" ステートメントと "Break" ステートメントの使い方に注意してください。これらのステートメントも関数として実装しています。以下に Continue を示します。

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

Break ステートメントの実装も同様です。他の関数と同じく、Continue と Break はどちらもパーサーに登録します。

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

入れ子になった If ブロックや while ループから抜け出すには、Break 関数を使用します。

フロー制御: While ループ

while ループも実装して関数としてパーサーに登録します。

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

while キーワードが解析されるたび、WhileStatement オブジェクトの Evaluate メソッドを呼び出します。

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 ループ内のループ変数 "i" をインクリメントし忘れています。

CSCS での無限 While ループの検出
図 7 CSCS での無限 While ループの検出

とにかく関数

CSCS でさらに役立つ機能を実行するには、新たな関数の実装が必要です。新しい関数を CSCS に追加するのは簡単です。まず、ParserFunction クラスから派生したクラスを (Evaluate メソッドをオーバーライドして) 実装し、そのクラスをパーサーに登録します。Print 関数の実装を以下に示します。

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 補助関数です。この関数については、付属のソース コードで確認できます。

2 番目かつ最後の手順は、プログラムの初期化部分で Print 関数をパーサーに登録することです。

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

Constants.PRINT 定数は、"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 式の絶対値を取得する
append 1 つの文字列または 1 つの数値 (文字列に変換) を文字列に付加する
cd ディレクトリを変更する
cd.. ディレクトリを 1 つ上の階層に変更する
dir 現在のディレクトリの内容を表示する
enc 環境変数の内容を取得する
exp 指数関数
findfiles 特定のパターンを指定してファイルを検索する
findstr 特定のパターンを持つ文字列を含むファイルを検索する
indexof 部分文字列のインデックス (見つからない場合は -1) を返す
kill 特定のプロセス ID 番号を持つプロセスを強制終了する
pi pi 定数の近似値を返す
pow 1 つ目の引数を 2 つ目の引数で累乗した値を返す
print 引数の特定のリストを出力する (数値とリストは文字列に変換)
psinfo 特定のプロセス名に関するプロセス情報を返す
pstime このプロセスの総プロセッサ時間を返す (時間測定に便利)
pwd 現在のディレクトリ パス名を表示する
run 特定の引数リストを指定してプロセスを開始し、プロセス ID を返す
setenv 環境変数の値を設定する
set 変数または配列要素の値を設定する
sin 特定の引数に対する正弦の値を返す
size 文字列の長さまたはリストのサイズを返す
sqrt 特定の数値の平方根を返す
substr 文字列の特定のインデックスから始まる部分文字列を返す
tolower 文字列を小文字に変換する
toupper 文字列を大文字に変換する

国際化

パーサーには、同じ関数に対応させて複数のラベル (関数名) を登録できます。これを利用して、他の言語をいくつでも追加できます。

翻訳した文字列を追加するには、同じ 1 つの 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 要素 (フロー制御ステートメント、変数、配列、および関数) は、ParserFunction 基本クラスから派生した C# クラスを定義し、Evaluate メソッドをオーバーライドすることで実装します。その後、実装したクラスのオブジェクトをパーサーに登録します。このアプローチには、次のようなメリットがあります。

  • モジュール性: 各CSCS 関数と各フロー制御ステートメントが個別のクラスに格納されます。そのため、新しい関数や新しいフロー制御ステートメントの定義、既存の関数やステートメントの変更などが簡単になります。
  • 柔軟性: CSCS のキーワードと関数名を任意の言語にできます。変更する必要があるのは構成ファイルのみです。他のほとんどの言語とは異なり、CSCS では、フロー制御ステートメントの関数名や変数名を ASCII 文字で指定する必要はありません。

もちろん、現段階の CSCS 言語は完ぺきには程遠い状態です。CSCS がさらに有益になる方法をいくつか以下に示しておきます。

  • 多次元配列を作成する。1 次元配列と同じ C# データ構造の List<Result> を使用できます。ただし、多次元配列の要素を取得および設定する場合は、さらに解析機能を追加する必要があります。
  • 1 行でのタプルの初期化を可能にする。
  • 論理演算子 (AND、OR、NOT など) を追加する。if ステートメントや while ステートメントを使用する場合に非常に便利になります。
  • CSCS で関数やメソッドを作成する機能を追加する。現時点では、事前に C# で作成し、コンパイルした関数しか使用できません。
  • 他のコンパイル単位からの CSCS ソース コードをインクルードする機能を追加する。
  • 代表的な OS 関連のタスクを実行する関数を追加する。このようなタスクの多くは C# で簡単に実装できるため、ほとんどの関数は C# 版の関数に対するシン ラッパーになります。
  • set(a, b) 関数のショートカット「a = b」を作成する。

今回紹介した CSCS 言語と独自のカスタム スクリプト言語の作成方法が、読者の皆様のお役に立てばさいわいです。


Vassili Kaplan は、以前は Microsoft Lync 開発者でした。C# と C++ を使用したプログラミングに熱心に取り組んでいます。現在はスイスのチューリッヒに住み、さまざまな銀行でフリーランス開発者として活動しています。連絡先は iLanguage.ch (英語) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの James McCaffrey に心より感謝いたします。