Bakulog

獏の夢日記的な何か。

【C#】Roslyn for Scripting C#小ネタ集【Script】

タイトル通りちょっとしたネタの詰め合わせです。

 

Roslynの一部でありスクリプトとしてのC#をサポートするRoslyn for Scriptingの具体的な小ネタ紹介です。Roslyn for Scripting自体の基本的な使い方は前に書いた記事をご覧ください。

 

もくじ

   

基本的な準備

適当に用意します。インストール周りでうまく行かない場合は前回の記事を見て下さい。

  1.  コンソールアプリケーション作ってターゲットを.NET 4.6に設定
  2.  NuGetでMicrosoft.CodeAnalysis.CSharp.Scriptingをインストール(2015/11時点ではプレリリース版しか無い事に注意)
  3.  実行ディレクトリ(bin\Debugとか)に"test.csx"というソースファイルを配置
  4.  コンソールアプリケーション側のプログラムを下記のようにする
using System;
using System.IO;
using Microsoft.CodeAnalysis.CSharp.Scripting;

namespace HelloRoslyn
{
    class Program
    {
        static void Main(string[] args)
        {
            var script = CSharpScript.Create(File.ReadAllText("test.csx"));
            script.RunAsync().Wait();
        }
    }
}

本記事の前半では本体側のコードをコレで放置し、スクリプト側"test.csx"の中を書き換えて色々試します。

   

System.Linq使おうとしたらエラーになったんだけど

C#スクリプトで使うならLINQ使いたいのは当然ですね。そこで"test.csx"を次のように書いて実行してみます。

using System;
using System.Linq;

Enumerable.Range(0, 5)
    .ToList()
    .ForEach(Console.WriteLine);

しかしコレだけだと実行時にスクリプトコンパイルエラー(CompilerErrorException)が吐かれて止まります。

エラーが起きた原因は何かというと、デフォルトのRoslyn for Scriptingではスクリプトが参照するDLLが最低限しか設定されておらずLINQを実装してる"System.Core.dll"への参照が無いためです。解決策としてはシンプルに、スクリプトの最上部に#rディレクティブでDLL参照を追加するだけでOKです。

#r "System.Core"

using System;
using System.Linq;

Enumerable.Range(0, 5)
    .ToList()
    .ForEach(Console.WriteLine);

今度は正しく動作します。

   

async/awaitまわりの扱い

スクリプトとasync/awaitに関して唯一かつ最大のポイントは「スクリプトのベタ打ち部分でawaitが普通に使える」という点です。例えばスクリプト自身のソースファイルを非同期で読み込んで文字数を拾うコードは次のように書けます。

using System;
using System.IO;

using (var sr = new StreamReader("test.csx"))
{
    string text = await sr.ReadToEndAsync();
    Console.WriteLine(text.Length);
}

もちろんこの例だとasync/awaitのメリットがあんま無いですが、awaitが普通に使えてる事自体は分かりますね。

   

拡張メソッドって普通に定義できんの?

これは「プログラム本体側のコードからスクリプトに渡したAPIスクリプト側で拡張したい!」というケースで便利そうなので記載してみました。拡張メソッドについてはクラスで囲わずに直接メソッドを書けば作れます。クラスで囲うのはダメです。

具体例としてstringを拡張して使ってみます。以下はOKな書き方の例です。

using System;

static void ShowAsDebugMessage(this string msg)
{
    Console.WriteLine("Debug Message is: " + msg);
}

"Hello World".ShowAsDebugMessage();

一方ダメな例はこちら。

using System;

public class MyClass
{
    public static void ShowAsDebugMessage(this string msg)
    {
        Console.WriteLine("Debug Message is: " + msg);
    }
}

"Hello World".ShowAsDebugMessage();

これだとエラーで怒られます。エラーについてもうちょい詳しく知りたい方は下記の補足をどうぞ。

[expand title="補足(クリックで展開)"]

ダメの方のエラーについてですが、具体的な内容としては「入れ子クラス内で拡張メソッド定義すんじゃねえ!」という主旨のエラーが出ます。「入れ子クラスなんか書いた覚えないよ?」と思うかもしれませんが、実はスクリプトコンパイルするとスクリプト全体がクラス定義で囲まれるため、スクリプト内部でクラスを定義した場合入れ子クラスとなります。

 

更に詳しく確認したい場合、コンパイルが正しく通ったとき実際に生成されるコードをILSpyとかで見るのがオススメです。まずMain関数をちょっと書き足し、コンパイル結果をdllとして保存します。

static void Main(string[] args)
{
    var script = CSharpScript.Create(File.ReadAllText(@"test.csx"));
    script.RunAsync().Wait();

    var comp = script.GetCompilation();
    using (var ms = new MemoryStream())
    {
        comp.Emit(ms);
        File.WriteAllBytes("test.dll", ms.ToArray());
    }
}

これで実行ディレクトリに"test.dll"が出力されるのでILSpyで中を見た結果が下記です。これは上に記載されている拡張メソッドを用いたスクリプトのうち、ビルドが通ってる方のケースです。(※ちなみにビルドが通らないスクリプトでdllを保存すると容量ゼロのファイルになります)

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

public sealed class Submission#0
{
    public sealed class <<Initialize>>d__0 : IAsyncStateMachine
    {
        public int <>1__state;

        public AsyncTaskMethodBuilder<object> <>t__builder;

        public Submission#0 <>4__this;

        void IAsyncStateMachine.MoveNext()
        {
            int num = this.<>1__state;
            object result;
            try
            {
                "Hello World".ShowAsDebugMessage();
                result = null;
            }
            catch (Exception exception)
            {
                this.<>1__state = -2;
                this.<>t__builder.SetException(exception);
                return;
            }
            this.<>1__state = -2;
            this.<>t__builder.SetResult(result);
        }

        [DebuggerHidden]
        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
        {
        }
    }

    public Task<object> <Initialize>()
    {
        Submission#0.<<Initialize>>d__0 <<Initialize>>d__ = new Submission#0.<<Initialize>>d__0();
        <<Initialize>>d__.<>4__this = this;
        <<Initialize>>d__.<>t__builder = AsyncTaskMethodBuilder<object>.Create();
        <<Initialize>>d__.<>1__state = -1;
        AsyncTaskMethodBuilder<object> <>t__builder = <<Initialize>>d__.<>t__builder;
        <>t__builder.Start<Submission#0.<<Initialize>>d__0>(ref <<Initialize>>d__);
        return <<Initialize>>d__.<>t__builder.Task;
    }

    public static void ShowAsDebugMessage(this string msg)
    {
        Console.WriteLine("Debug Message is: " + msg);
    }

    public Submission#0(object[] submissionArray)
    {
        submissionArray[1] = this;
    }

    public static Task<object> <Factory>(object[] submissionArray)
    {
        return new Submission#0(submissionArray).<Initialize>();
    }
}

[/expand]

   

スクリプトの中でstatic変数の宣言したらどうなるの?

先に注意しておきますが、本節の内容は他の節と異なり「アレ、この仕様って大丈夫なの…?」と疑問に思ったので書いたネタです。まずスクリプトをこのように書きます。

using System;

static int SomeValue = 0;

SomeValue++;

Console.WriteLine(SomeValue);

次に本体側のMain関数を次のように書き換えます。

static void Main(string[] args)
{
    for (int i = 0; i < 5; i++)
    {
        var script = CSharpScript.Create(File.ReadAllText("test.csx"));
        script.RunAsync().Wait();
    }

    var script2 = CSharpScript.Create(File.ReadAllText("test.csx"));
    for (int i = 0; i < 5; i++)
    {
        script2.RunAsync().Wait();
    }
}

ふたつの方法でスクリプトを実行しています。前者ではScriptのインスタンスを毎回作り、後者では同じインスタンスを使いまわすのが違いですね。実行結果はこうなります。

1
1
1
1
1
1
2
3
4
5

結果から分かる通り、同じScriptのインスタンスを使いまわせばstatic変数のデータが保持できることが分かります。

これは便利そうにも見えますが、Roslyn for Scriptingでは色々なクラスがImmutableに作られてる事を考えると、上の例は「Scriptインスタンスにstatic変数の中身が付きまとう事で不変性にヒビが入りました」と解釈した方がいいように見えてしまい、なんだか居心地が悪いです。特に理由が無いなら避けるべき書き方だと言えます。

   

dynamicキーワード使うとスクリプト感が高そう

小ネタ集のトリを飾るのはスクリプト感の高いキーワードであるdynamicです。C#とはいえスクリプトで使う以上は静的型付けしたくないシチュエーションもあるので、さっそくdynamicが使えるか試してみます。

本体側のコードをこんな風に書き足します。

using System;
using System.IO;
using Microsoft.CodeAnalysis.CSharp.Scripting;

namespace HelloRoslyn
{
    class Program
    {
        static void Main(string[] args)
        {
            CSharpScript.RunAsync(
                File.ReadAllText("test.csx"),
                globals: new ApiHolder(),
                globalsType: typeof(ApiHolder)                
                ).Wait();

        }
    }

    public class ApiHolder
    {
        public SomeApiForScript Hoge { get; } = new SomeApiForScript();
    }

    public class SomeApiForScript
    {
        public double Value { get; set;} = 4.0;

        public void SomeMethod()
        {
            Console.WriteLine("This is Some Method");
        }
    }
}

スクリプト側はこうします。

#r "System.Core"
#r "Microsoft.CSharp"

dynamic api = Hoge;
System.Console.WriteLine(api.SomeValue);
api.SomeMethod();

dynamicキーワードを使うためにはSystem.CoreとMicrosoft.CSharpの参照が必要なことに注意してください。これで実行すると普通に動作し、dynamicキーワードが正しく使えることが確認できます。

 

まとめ

安定版が欲しいです。