C#をスクリプト言語的に実行したり、インタラクティブ実行したりできるようになりました。 その実行方法や、通常の(コンパイルして使う)C#にはないスクリプト専用機能などについて説明します。

目次

概要

2015年末頃、ついにC#をスクリプト言語的に実行したり、インタラクティブに実行したりできるようになりました。すなわち、以下のようなことができるようになりました。

  • アプリへの組み込み
    • アプリに組み込んで、そのアプリ用のマクロ言語としてC#を使う
    • アプリを実行したままC#スクリプトを読み直して、動的にアプリの挙動を変える
  • REPL(Read Eval Print Loop)実行
    • 1行1行、都度(インタラクティブに)結果を見ながらC#を書く
  • スクリプト実行
    • コマンド ライン ツールにC#スクリプト ファイルを渡して実行する
    • class Program { static void Main() { } }みたいなノイズなしに、1行目から式やステートメントを書ける

以下、これらを総称して「スクリプト実行」と呼びます。

通常の(コンパイルして使う)C#で書けるものは大体はスクリプト実行できます。また、スクリプト実行時にのみ許される構文や、スクリプト実行時特有の動作がいくつかあります。

いくつかの実行形態

概要で一覧を出したように、いくつかの方法でC#スクリプト実行できます。

アプリへの組み込み

Microsoft.CodeAnalysis.CSharp.Scriptingライブラリを参照することで、自作のアプリにC#スクリプトを組み込めます。 例えば、以下のようなコードが書けます。

サンプル コード: https://github.com/ufcpp/UfcppSample/tree/master/Chapters/Scripting/src/Scripting

using Microsoft.CodeAnalysis.CSharp.Scripting;
using System;
using System.Threading.Tasks;

public class Program
{
    public static void Main(string[] args)
    {
        MainAsync().Wait();
    }

    private static async Task MainAsync()
    {
        var result = await CSharpScript.EvaluateAsync<int>(@"
var x = 1;
var y = 2;
x + y
");
        Console.WriteLine(result);
    }
}

スクリプトとアプリとのやり取り

アプリに組み込む以上は、アプリに対する命令みたいなものをスクリプトに対して公開する必要があるわけですが、 それはこのEvaluateAsyncなどのメソッドの引数のglobalsに対してオブジェクトを渡すことで実現できます。

例えば、以下のようなクラスを用意します。

/// <summary>
/// コマンド発行クラス。
/// C# スクリプトのglobalsとして渡して、スクリプトからコマンドを発行するのに使う。
/// </summary>
public class Commander
{
    // 中略

    public void walk(double distance) => _queue.Enqueue(Command.Walk(distance));
    public void turn(double angle) => _queue.Enqueue(Command.Turn(angle));
    public void speed(double speedDotPerSecond) => _queue.Enqueue(Command.Speed(speedDotPerSecond));
    public void clear() => _queue.Enqueue(Command.Clear());
}

これを、EvaluateAsyncRunAsyncなどのスクリプトAPIのglobals引数に渡すことで、 C#スクリプト側から、walk, turn, speed, clearなどのメソッドを呼び出せるようになります。

_state = await CSharpScript.RunAsync(s, globals: ViewModel.Commander);

ちなみに、このコードは、C#スクリプトを使ってタートル グラフィックスをやってみるサンプル プログラムの一部です。 コード全体は、GitHubで公開しています。

サンプルコード: https://github.com/ufcpp/UfcppSample/tree/master/Chapters/Scripting/TurtleGraphics

実際に動かしている様子は以下の通りです。

C# インタラクティブ ウィンドウ

Visual Studio 2015 Update 1から、C#をREPL実行できる「C# インタラクティブ」というウィンドウが追加されました。

Visual Studioのメニューから下図のようにたどるか、 Visual Studio右上にある「クイック起動」欄に下図のように「C#」と打って検索することでウィンドウを開けます。

メニューから、C#インタラクティブ ウィンドウを開く

クイック起動から、C#インタラクティブ ウィンドウを開く

C#インタラクティブ ウィンドウ内では、下図のようにコード ハイライトやコード補完が効きます。

コードのハイライトや補完

REPL(Read Eval Print Loop)なので、1行1行コードを読んで(read)、評価して(eval)、その結果を出力(print)することができます。

C#インタラクティブ ウィンドウを使ったREPL実行

dotnetコマンド

dotnetコマンドの1機能として、REPL実行やスクリプト実行ができます。

下図のように、dotnet replというサブコマンドを使うことでREPLが起動します。

dotnet replサブコマンド

ちなみに、dotnet replは、既定動作がC# REPLの起動というだけで、引数で他の.NET言語も選べます。 (といっても、2016年1月時点ではC#のみに対応。計画としてはVisual BasicとF#への対応も考えている模様。Visual Basicはその作業真っ最中。)

1行1行書けるコードはC#インタラクティブ ウィンドウと同じです。 ただ、C#インタラクティブ ウィンドウと違って、コード補完などは掛かりません。 Visual Studio 2015 Update 1以降を使えるのであれば、C#インタラクティブ ウィンドウを使う方が便利でしょう。 dotnetコマンドはクロスプラットフォームなコマンド ライン ツールなので、GUIのない環境でも使えるという利点はあります。

REPLで1行1行実行する他に、スクリプト ファイルを与えて実行するモードがあります。 下図のように、dotnet replサブコマンドの引数にファイル名を指定します。

dotnet replサブコマンドにスクリプト ファイルを与えて実行

ちなみに第1引数はどの言語を使うかを指定します。(前述の通り2016年1月時点ではC#のみ。csiかcsharpを入力。) そして、第2引数が実行したいC#スクリプトのファイル名です。

この例では、以下のようなC#スクリプトを与えています。

using System;

Console.WriteLine(DateTime.Now);

見てのとおり、通常の(コンパイルして使う)C#と違って、トップ レベルにステートメントを書いて実行できます。 class Programstatic void Main()などのクラス/メソッドは必ずしも必要ありません。

スクリプト実行用の構文

通常の(コンパイルして使う)C#の機能はほぼ全て使えます。 例えば以下のように、通常のC#コードをそのままC#インタラクティブ ウィンドウに張り付けて実行できます。

> using System;
. 
. public class Program
. {
.     public static void Main()
.     {
.         Console.WriteLine("Hello World!");
.     }
. }
. 
> Program.Main()
Hello World!

一方で、スクリプト実行でだけできる書き方がいくつかあります。

結果の出力

式を1つだけ書いて、;も入力せずに改行すると、その式の結果を出力します。 例えば、以下のようなコードでは、1行目は普通のC#と同じく代入ステートメント、2行目はx * xという式の計算結果の出力になります。

> var x = 10;
> x * x
100

一方で、例えば以下のような書き方はできません。 ; を付けたことで通常のC#構文として解釈されますが、式 + ; という構文はC#にはないのでエラーになります。

> x * x;
(1,1): error CS0201: Only assignment, call, increment, decrement, and new object expressions can be used as a statement

トップ レベル

通常のC#では、トップ レベル(ソースコードの一番上)に書けるものがかなり限られています。

このうち、名前空間とアセンブリに対する属性は、スクリプト実行では使えません。

> namespace Sample { }
(1,1): error CS7021: スクリプト コードで名前空間を宣言することはできません
> [assembly:System.Reflection.AssemblyTitle("test")]
(1,2): error CS7026: アセンブリ属性とモジュール属性は、このコンテキストでは許可されていません。

一方、スクリプト実行時には、トップ レベルに以下のようなものが書けます。

  • ステートメント
  • 式(結果の値が出力される)
  • クラスのメンバー(メソッド、プロパティなど)

例えば以下のようなコードが書けます。

> var x = 10;
> var y = 20;
> int Product => x * y;
> Product
200
> x = 15;
> y = 25;
> Product
375

トップ レベルで定義した変数は特殊なスコープを持ちます。 上記の例のように、トップ レベルに書いたメンバー内では参照(この例だとProductプロパティ内で、変数x, yを参照)できますが、 クラスを書くと、その中からは参照できません。

> var x = 10;
> int X => x; // これはOK
> class C { int X => x; } // クラス内からは x を使えない
(1,20): error CS0120: 静的でないフィールド、メソッド、またはプロパティ 'x' で、オブジェクト参照が必要です

ちなみに、トップ レベルに拡張メソッドも書けます。

> static int Square(this int x) => x * x;
> 10.Square()
100

また、トップ レベルは、通常のC#でいうところの非同期メソッドと同じ状態になっていて、常にawait演算子が使えます。

> #r "System.Net.Http"
> using System.Net.Http;
> var c = new HttpClient();
> var res = await c.GetAsync("http://ufcpp.net");
> var content = await res.Content.ReadAsStringAsync();
> content.Substring(0, 50)
"\r\n<!DOCTYPE html>\r\n<html lang=\"ja\" xmlns=\"http://w"

スクリプト用ディレクティブ

スクリプト実行時にだけ使えるものとして、プリプロセス ディレクティブと同じ # から始まるいくつかのディレクティブがあります。

現状では以下のようなものがあります。

ディレクティブ 説明
#help ヘルプを表示します。
#cls, #clear ウィンドウ内のテキストをクリアします。
#reset コンテキスト(定義した変数やメンバーなど)をクリアします。
#r アセンブリを読み込みます。
#load スクリプト ファイルを読み込みます。

例えば、a.csxという名前で以下のようなファイルがあったとします。

var x = 10;

この状況下で、以下のようなスクリプトを実行できます。

> #load "a.csx"
> x
10

ディレクティブは、これからいくつか追加も予定されています。 #helpと打つことでヘルプが表示されるので詳しくはそれを読んでみてください。

更新履歴

ブログ