プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

メタプログラミング入門 - 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 を使うことで、クラスを実行時に作ることができる。