[C#][.NET][CodeDOM] メタプログラミング入門 – CodeDOM によるクラスの生成

Metasequoia

※ 「[C#][.NET][CodeDOM] メタプログラミング入門 – CodeDOM による Hello world!」の続き。

CodeDOM によるクラスの動的生成

前回は、CodeDOM を使って Hello world! を表示するプログラムを動的に生成した。

もう少し CodeDOM を使ってみよう。
今回は、CodeDOM を使ってクラスを作ってみる。

次のような Item クラスを動的に作ることにする。

namespace CodeDomClassDemo
{
public class Item
{
private int price;
public int Price
{
get
{
return this.price;
}
set
{
this.price = value;
}
}
public override string ToString()
{
return (this.Price + "円");
}
}
}

次のような手順だ。

  1. CodeDomClassDemo 名前空間の生成
    ― CodeNamespace を new
  2. Item クラスの生成
    ― CodeTypeDeclaration を new
  3. Item クラスを CodeDomClassDemo 名前空間へ追加
  4. price フィールドを生成
    ― CodeMemberField を new
  5. price フィールドを Item クラスに追加
  6. Price プロパティを生成
    ― CodeMemberProperty を new
  7. Price プロパティの Get を追加
    ― price フィールドへの参照を return する文を生成して追加
  8. Price プロパティの Set を追加
    ― price フィールドへの参照に value を代入する文を生成して追加
  9. Price プロパティを Item クラスに追加
  10. ToString メソッドを生成
    ― CodeMemberMethod を new
  11. ToString メソッドの中身を追加
    ― Price プロパティへの参照に &qout;円&qout; を + して return する文を生成して追加
  12. ToString メソッドを Item クラスに追加

では、やってみよう。
前回同様、System.CodeDom 名前空間を使用する。

using System.CodeDom;
class Program
{
// Item クラスの Code DOM
static CodeNamespace ItemClassCodeDom()
{
// CodeDomHelloWorldDemo 名前空間
var nameSpace              = new CodeNamespace(name: "CodeDomClassDemo");
//// System 名前空間のインポート
//nameSpace.Imports.Add(new CodeNamespaceImport(nameSpace: "System"));
// Item クラス
var itemClass              = new CodeTypeDeclaration(name: "Item");
// CodeDomHelloWorldDemo 名前空間に Item クラスを追加
nameSpace.Types.Add(itemClass);
// price フィールド
var priceField             = new CodeMemberField(type: typeof(int), name: "price") {
Attributes = MemberAttributes.Private // private
};
// Item クラスに price フィールドを追加
itemClass.Members.Add(priceField);
// Price プロパティ
var priceProperty          = new CodeMemberProperty {
Name       = "Price"                                         , // 名前は、Price
Attributes = MemberAttributes.Public | MemberAttributes.Final, // public で virtual じゃない
Type       = new CodeTypeReference(typeof(int))                // 型は int
};
// price フィールドへの参照
var priceFieldReference    = new CodeFieldReferenceExpression(
targetObject: new CodeThisReferenceExpression(), // this.
fieldName   : "price"                            // price
);
// Price プロパティの Get を追加
priceProperty.GetStatements.Add(
new CodeMethodReturnStatement(priceFieldReference) // price フィールドへの参照を return する文
);
// Price プロパティの Set を追加
priceProperty.SetStatements.Add(
new CodeAssignStatement(                                  // 代入文
left : priceFieldReference                          , // 左辺は、price フィールドへの参照
right: new CodePropertySetValueReferenceExpression()  // 右辺は、value
)
);
// Item クラスに Price プロパティを追加
itemClass.Members.Add(priceProperty);
// ToString メソッド
var toStringMethod         = new CodeMemberMethod {
Name       = "ToString"                                         , // 名前は、ToString
Attributes = MemberAttributes.Public | MemberAttributes.Override, // public で override
ReturnType = new CodeTypeReference(typeof(string))                // 戻り値の型は、string
};
// Price プロパティへの参照
var pricePropertyReference = new CodePropertyReferenceExpression(
targetObject: new CodeThisReferenceExpression(), // this.
propertyName: "Price"                            // Price
);
// ToString メソッドの中身
toStringMethod.Statements.Add(
new CodeMethodReturnStatement(                    // return 文
new CodeBinaryOperatorExpression(             // 二項演算子の式
left : pricePropertyReference           , // Price プロパティへの参照
op   : CodeBinaryOperatorType.Add       , // +
right: new CodePrimitiveExpression("円")  // "円"
)
)
);
// Item クラスに ToString メソッドを追加
itemClass.Members.Add(toStringMethod);
return nameSpace;
}
static void Main()
{
// Item クラスを含む名前空間を CodeDOM で生成
var itemClassCodeDom = ItemClassCodeDom();
}
}

この ItemClassCodeDom メソッドで、Item クラスを含む名前空間を生成したことになる。

CodeDOM によるソースコードの生成

前回同様、上で作った名前空間から CodeDOM を使って C# のソースコードを生成してみる。
手順は前回と全く同じで、System.CodeDom.Compiler 名前空間を使用する。

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Text;
class Program
{
// Item クラスの Code DOM
static CodeNamespace ItemClassCodeDom()
{
…… 同じなので省略 ……
}
// 名前空間からソースコードを生成
static string GenerateCode(CodeNamespace codeNamespace)
{
// コンパイル オプション
var compilerOptions = new CodeGeneratorOptions { IndentString = "    ", BracingStyle = "C" };
var codeText        = new StringBuilder();
using (var codeWriter = new StringWriter(codeText)) {
// 名前空間からソースコードを生成
CodeDomProvider.CreateProvider("C#").GenerateCodeFromNamespace(codeNamespace, codeWriter, compilerOptions);
}
return codeText.ToString(); // 生成されたソースコード
}
static void Main()
{
// Item クラスを含む名前空間を CodeDOM で生成
var itemClassCodeDom = ItemClassCodeDom();
// Item クラスのソースコードを生成
var code             = GenerateCode(itemClassCodeDom);
Console.WriteLine(code);
}
}

実行してみると、次のように C# のソースコードが表示される。

namespace CodeDomClassDemo
{
public class Item
{
private int price;
public int Price
{
get
{
return this.price;
}
set
{
this.price = value;
}
}
public override string ToString()
{
return (this.Price + "円");
}
}
}

GenerateCode メソッド中の "C#" の部分を "VB" に置き換えて実行した場合は次の通り。

Namespace CodeDomClassDemo
Public Class Item
Private price As Integer
Public Property Price() As Integer
Get
Return Me.price
End Get
Set
Me.price = value
End Set
End Property
Public Overrides Function ToString() As String
Return (Me.Price + "円")
End Function
End Class
End Namespace

CodeDOM によるアセンブリの生成

次に、CodeDOM で生成した名前空間をコンパイルしてアセンブリを生成してみよう。

前回は、アセンブリをファイルに出力したが、今回はオンメモリで生成してみる。

生成したアセンブリの中から Item クラスを取り出してインスタンスを生成し、Price プロパティや ToString メソッドを使ってみよう。

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
class Program
{
// Item クラスの Code DOM
static CodeNamespace ItemClassCodeDom()
{
…… 同じなので省略 ……
}
// 名前空間からソースコードを生成
static string GenerateCode(CodeNamespace codeNamespace)
{
…… 同じなので省略 ……
}
// 名前空間をアセンブリへコンパイル
static Assembly CompileAssembly(CodeNamespace codeNamespace)
{
var codeCompileUnit = new CodeCompileUnit();   // コンパイル単位
codeCompileUnit.Namespaces.Add(codeNamespace); // コンパイル単位に名前空間を追加
// アセンブリへコンパイル
var compilerResults = CodeDomProvider.CreateProvider("C#").CompileAssemblyFromDom(
options         : new CompilerParameters { // コンパイル オプション
GenerateInMemory = true                // アセンブリをメモリ内で生成
},
compilationUnits: codeCompileUnit          // コンパイル単位
);
return compilerResults.CompiledAssembly; // コンパイルされたアセンブリを返す
}
static void Main()
{
// Item クラスを含む名前空間を CodeDOM で生成
var itemClassCodeDom = ItemClassCodeDom();
// Item クラスのソースコードを生成
var code             = GenerateCode(itemClassCodeDom);
Console.WriteLine(code);
// Item クラスを、アセンブリとしてコンパイル
var itemAssembly     = CompileAssembly(codeNamespace: itemClassCodeDom);
// Item クラス アセンブリのテスト
TestItemAssembly(itemAssembly);
}
// Item クラス アセンブリのテスト
static void TestItemAssembly(Assembly itemAssembly)
{
// Item クラスを、アセンブリから取得
var     itemType = itemAssembly.GetTypes().First();
// Item クラスのインスタンスを動的に生成
dynamic item     = Activator.CreateInstance(itemType);
// Item クラスの Price プロパティのテスト
item.Price       = 2980;
Console.WriteLine(item.Price);
// Item クラスの ToString メソッドのテスト
Console.WriteLine(item);
}
}

実行してみると、次のように表示され、Price プロパティとToString メソッドが正常に使えるのが分かる。

2980
2980円

まとめ

今回は、CodeDOM を使った動的にクラスを生成した。CodeDOM を使うことで、クラスを実行時に作ることができる。