TypeProviderで生成したクラスをC#で使うための方法

型プロバイダー(Type Provider)のちょっとしたアレコレ – Bug Catharsis
http://zecl.hatenablog.com/entry/TypeProvderArekore

に勝手につけたし。ちょうど「消去型と生成型」のところを考えていたところで、消去型(IsErased=true)と、生成型(IsErased=true)では、何が「プログラミング的」に違うのか?という話です。

第一には型が消去してしまうので、別のアセンブリから参照するときにインテリセンスが効かない、プロパティなどの名前解決ができない、ってところで、

  • うちうちで使うならば「消去型」のままで ok
  • 別途公開する必要があれば「生成型」を使う

って感じですね。アセンブリをファイルとして残しておくことで、Visual Studio が参照できるようになる、っていう仕組みだと思います。なので、アセンブリ自体は、こんな形でテンポラリファイルとして残しておきます。

1
2
3
4
5
6
let outerType =
    ProvidedTypeDefinition (thisAssembly, namespaceName,
        typeName, Some(typeof), IsErased = false )
// 名前を付けてアセンブリを残す
let tempAssembly = ProvidedAssembly(System.IO.Path.ChangeExtension(System.IO.Path.GetTempFileName(), ".dll"))
tempAssembly.AddTypes <| [ outerType ]

このファイルは、テンポラリファイルなので、生成された後に適当な時間で消えてしまいます。あと、TypeProvider の DLL はコードを変えるたびに生成されるので、テンポラリ名にして名前が変化するようにします。

■C# からタイププロバイダを使うときには、F# を媒介させる

型生成はテンポラリファイルとして残っているものの、これを参照できるのは F# からだけです。何故、F# からのみしか参照できないのかはわかりませんが、C# から参照しようとしても見当たりません。
といいますか、型生成の方法が、こんな風な構文になっているので、構文的に C# では書けないというのもありますね。

1
2
3
4
5
6
type AA = CSharpTypeProvider.STR<"masuda">
let a = new AA()
 
printfn "CSharpTypeProvider test in F#"
printfn "%A" a.Name
printfn "%A" a.Version

ただし、タイプビルダー自体は、

TypeBuilder クラス (System.Reflection.Emit)
http://msdn.microsoft.com/ja-jp/library/system.reflection.emit.typebuilder(v=vs.110).aspx

によって実現されているので F# しかできないものではありません。そのうちに C# や VB でも使えるようになる可能性はあります(構文さえ作ればよいので)。

さて、ここで作成した AA クラス あるいは CSharpTypeProvider.STR<“masuda”> というクラスを C# でどうやって扱えばいいのか?という話があまり書いていないのですが、結構簡単です。型生成だけした F# のライブラリを用意して、次のようにつくっておきます。

1
2
namespace FSharpLib
type AA = CSharpTypeProvider.STR<"tomoaki">

そして、C# のプロジェクトから FSharpLib プロジェクトを参照設定して、次のように書けばいいのです。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Program
{
    static void Main(string[] args)
    {
        // refer SampleType.tomoaki.dll
        var a = new FSharpLib.AA();
        Console.WriteLine("Typeprovider test in C#");
        Console.WriteLine("{0}", a.Name);
        Console.WriteLine("{0}", a.Version);
 
        System.Console.ReadKey();
    }
}

簡単ですよね。クラスの定義部分が F# になってしまうために、C# 内で閉じないのが難点ではありますが、T4 などを使わずに型生成が簡単にできるのは非常に便利です。この型生成に渡すパラメータを内部的に解釈を変えれば、外部ファイルを読み込んだり、インターネットからダウンロードして生成したりすることもできます。外部ファイルへの対応は、

ちょっと草植えときますね型言語Grass型プロバイダー
http://github.com/zecl/GrassTypeProvider

にあります。

注意したいところは、コード変えるたびに型生成の実行が発生するので、あまり重たい処理は入れられないということです。重たい処理の場合は、別プロセスにするとか遅延を許す感じでユーザーに通知するなどの工夫が必要でしょう。

■型生成済みのアセンブリを C# で直接参照する

動的に型生成をする場合には、上記のように F# プロジェクトが必要ですが、最初に生成しただけであとは全く同じ状態であるならば、C# プロジェクトから直接生成済みのアセンブリを参照することもできます。

1
2
3
4
5
6
7
let outerType =
    ProvidedTypeDefinition (thisAssembly, namespaceName,
        typeName, Some(typeof), IsErased = false )
// フルパスで指定する
// 型を残して C# から直接参照できるようにする
let tempAssembly = ProvidedAssembly("d:\temp\SampleType."+ str+".dll")
tempAssembly.AddTypes <| [ outerType ]

先に書いたテンポラリファイルへの書き込みを逆手にとって、同じファイルに出力するようにします。ここでは、渡された文字列と組み合わせてユニーク名にしたアセンブリを temp フォルダに保存しています(フルパスでないと駄目です)。タイププロバイダに渡した初期値ごとに違うのですが、同じ型であれば同じアセンブリ名になります。
型生成する F# のプロジェクトは別に作っておいて、一度だけ動かします。
そして、生成した型生成の DLL を C# プロジェクトから参照させます。

1
2
3
4
5
6
7
8
9
static void Main(string[] args)
{
    var a = new CSharpTypeProvider.AA();
    Console.WriteLine("Typeprovider test in C#");
    Console.WriteLine("{0}", a.Name);
    Console.WriteLine("{0}", a.Version);
 
    System.Console.ReadKey();
}

すると、普通の DLL のようにクラスを生成することができます。型生成されているアセンブリだから当然ですね。
この方式のメリットは、C# のプロジェクトから F# のプロジェクトを参照しなくてよいことです。生成済みの DLL だけを使えばいいので配布も簡単です。まあ、大量に型生成する必要があるかどうかは別なのですが、状況次第ではいいのではないでしょうか?

■サンプルコード

動作確認のためのサンプルコードはこちら

moonmile/SampleTypeProviderToUseCSharp
http://github.com/moonmile/SampleTypeProviderToUseCSharp

カテゴリー: C#, F# パーマリンク