クラスのコンストラクタを動的に呼び出し、動的にクラスを作成するコードを紹介します。
クラスのコンストラクタを動的に呼び出す方法は、リフレクションを利用する方法など、何通りかありますが、この記事ではラムダ式を利用するコードを紹介します。
コンストラクタの引数が無い場合のコードを紹介します。
Windowsフォームアプリケーションを作成し、下図のUIを作成します。ボタンとテキストボックスをひとつずつ配置します。
下記のコードを記述します。フォームのbutton1 のClickイベントを実装します。
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Forms;
namespace DynamicDelegate
{
public partial class FormDynamicConstructor : Form
{
public FormDynamicConstructor()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
NewExpression body = Expression.New(typeof(DemoSimpleClass));
LambdaExpression lambda = Expression.Lambda<Func<DemoSimpleClass>>(body);
Func<DemoSimpleClass> func = (Func<DemoSimpleClass>)lambda.Compile();
DemoSimpleClass cls = func();
textBox1.Text = cls.Data;
}
}
}
フォームから参照するDemoSimpleClassのコードは下記になります。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DynamicDelegate
{
public class DemoSimpleClass
{
public string Data;
public DemoSimpleClass()
{
Data = "Penguin";
}
}
}
button1がクリックされた際に実行されるコードは下記のコードと同等になります。
private void button1_Click(object sender, EventArgs e)
{
Func<DemoSimpleClass> func = () => new DemoSimpleClass();
DemoSimpleClass cls = func();
textBox1.Text = cls.Data;
}
今回のプログラムのコードは上記のコードのラムダ式の部分の Func<DemoSimpleClass> func = () => new DemoSimpleClass();
を動的に作成しています。ラムダ式の動的な作成についてはこちらの記事も参照して下さい。
下記のコードでラムダ式の=>
の右辺の new DemoSimpleClass()
の処理を作成しています。
NewExpression body = Expression.New(typeof(DemoSimpleClass));
Expression.Labbda
メソッドを呼び出しラムダ式を作成します。詳細はこちらの記事を参照して下さい。~
LambdaExpression lambda = Expression.Lambda<Func<DemoSimpleClass>>(body);
Func<DemoSimpleClass>
型のデリゲートになります。 Func<DemoSimpleClass> func = (Func<DemoSimpleClass>)lambda.Compile();
ラムダ式のデリゲートを呼び出し、DemoSimpleClassのインスタンスを作成します。作成されたインスタンスのDataメンバ変数の値をテキストボックスに表示します。
DemoSimpleClass cls = func();
textBox1.Text = cls.Data;
プロジェクトを実行します。下図のウィンドウが表示されます。
[button1]をクリックします。"Penguin"の文字列がテキストボックスに表示されます。DemoSimpleClass のインスタンスが作成されメンバ変数に設定された値を取得できていることが確認できます。
コンストラクタに引数が1つある場合のコードを紹介します。
下図のフォームを作成します。フォームにボタンとテキストボックスを配置します。(下図のフォームのにはボタンが2つありますが、button2 のみを利用します。)
下記のコードを記述します。
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Forms;
namespace DynamicDelegate
{
public partial class FormDynamicConstructor : Form
{
public FormDynamicConstructor()
{
InitializeComponent();
}
private void button2_Click(object sender, EventArgs e)
{
ParameterExpression arg = Expression.Parameter(typeof(string), "value");
ConstructorInfo ctor = typeof(DemoParamClass).GetConstructor(new[] { typeof(string) });
NewExpression instance = Expression.New(ctor, arg);
LambdaExpression lambda = Expression.Lambda<Func<string, DemoParamClass>>(instance, arg);
Func<string, DemoParamClass> func = (Func<string, DemoParamClass>)lambda.Compile();
DemoParamClass cls = func("Duck");
textBox1.Text = cls.Data;
}
}
}
namespace DynamicDelegate
{
public class DemoParamClass
{
public string Data;
public DemoParamClass(string value)
{
Data = value;
}
}
}
基本的な仕組みは引数が無い場合と同様です。コンストラクタに引数がある場合は、引数のパラメータをラムダ式中(ラムダ式の左辺)に準備する必要があります。
今回作成するラムダ式は以下になります。
下記のコードでパラメーターを作成します。
ParameterExpression arg = Expression.Parameter(typeof(string), "value");
DemoParamClass
クラスの ConstructorInfo を取得します。GetConstructorの引数には、引数に与えたコンストラクタの引数と一致するコンストラクタを探して取得します。下記コードでは new[] { typeof(string) }
を与えていますので、string型の引数が一つあるコンストラクタを取得します。取得したコンストラクターのConstructoInfo オブジェクトをメソッドの戻り値とて返します。~
ConstructorInfo ctor = typeof(DemoParamClass).GetConstructor(new[] { typeof(string) });
ラムダ式の右側を作成します。Expression.New()
メソッドでクラスのインスタンスを作成する式を作成します。コンストラクタの引数が無い例ではクラスのTypeを与えましたが、コンストラクタの引数がある場合は、先に取得したコンストラクタのCoinstructorInfo を与えます。第二引数にコンストラクタの引数パラメータの ParameterExpression を与えます。
Expression.Lambda()
メソッドでラムダ式を作成します。Lambdaメソッドもコンストラクタの引数が無い場合は、NewExpressionオブジェクトのみを与えましたが、引数がある場合は、第二引数にコンストラクタの引数パラメータの ParameterExpression を与えます。
その後、作成したラムダ式のオブジェクト LambdaExpression
のCompile() メソッドを呼び出しデリゲートを作成します。
NewExpression instance = Expression.New(ctor, arg);
LambdaExpression lambda = Expression.Lambda<Func<string, DemoParamClass>>(instance, arg);
Func<string, DemoParamClass> func = (Func<string, DemoParamClass>)lambda.Compile();
コンストラクタのデリゲートを呼び出します。インスタンスが生成されますので、メンバ変数の値をテキストボックスに表示します。
DemoParamClass cls = func("Duck");
textBox1.Text = cls.Data;
プロジェクトを実行します。下図のウィンドウが表示されます。
[button2]をクリックします。テキストボックスに "Duck" の文字列が表示されます。DemoParamClass のコンストラクタに与えた文字列と同じ文字列が表示されており、コンストラクタにパラメータを渡してクラスのインスタンスが生成できていることが確認できます。
Windows フォームアプリケーションで下図のUIを作成します。フォームにボタンとテキストボックスを配置します。
下記のコードを記述します。フォームの[button3]のClickイベントを実装します。
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Forms;
namespace DynamicDelegate
{
public partial class FormDynamicConstructor : Form
{
public FormDynamicConstructor()
{
InitializeComponent();
}
private void button3_Click(object sender, EventArgs e)
{
ParameterExpression arg1 = Expression.Parameter(typeof(int), "x");
ParameterExpression arg2 = Expression.Parameter(typeof(int), "y");
ParameterExpression arg3 = Expression.Parameter(typeof(string), "name");
ParameterExpression arg4 = Expression.Parameter(typeof(string), "code");
ConstructorInfo ctor = typeof(DemoParamCompClass).GetConstructor(new[] { typeof(int), typeof(int), typeof(string), typeof(string) });
NewExpression instance = Expression.New(ctor, new ParameterExpression[] { arg1, arg2, arg3, arg4 });
LambdaExpression lambda = Expression.Lambda<Func<int, int, string, string, DemoParamCompClass>>(instance, new ParameterExpression[] { arg1, arg2, arg3, arg4 });
Func<int, int, string, string, DemoParamCompClass> func = (Func<int, int, string, string, DemoParamCompClass>)lambda.Compile();
DemoParamCompClass cls = func(8,6,"Penguin","P-101");
textBox1.Text = string.Format("{0:d},{1:d} / {2} : {3}", cls.value_x, cls.value_y, cls.value_name, cls.value_code);
}
}
}
namespace DynamicDelegate
{
public class DemoParamCompClass
{
public int value_x;
public int value_y;
public string value_name;
public string value_code;
public DemoParamCompClass(int x, int y, string name, string code)
{
value_x = x;
value_y = y;
value_name = name;
value_code = code;
}
}
}
仕組みは引数が1つの場合と同様です。コンストラクタに引数がある場合は、引数のパラメータをラムダ式中(ラムダ式の左辺)に準備する必要があります。
今回作成するラムダ式は以下になります。
下記のコードでパラメーターを作成します。今回は引数が4つあるのでパラメーターも4つ作成します。
ParameterExpression arg1 = Expression.Parameter(typeof(int), "x");
ParameterExpression arg2 = Expression.Parameter(typeof(int), "y");
ParameterExpression arg3 = Expression.Parameter(typeof(string), "name");
ParameterExpression arg4 = Expression.Parameter(typeof(string), "code");
DemoParamCompClass
クラスの ConstructorInfo を取得します。GetConstructorの引数には、引数に与えたコンストラクタの引数と一致するコンストラクタを探して取得します。下記コードでは new[] { typeof(int), typeof(int), typeof(string), typeof(string) }
を与えていますので、int, int, string, string型の引数の並びになっているコンストラクタを取得します。取得したコンストラクターのConstructoInfo オブジェクトをメソッドの戻り値とて返します。~
ConstructorInfo ctor = typeof(DemoParamCompClass).GetConstructor(new[] { typeof(int), typeof(int), typeof(string), typeof(string) });
ラムダ式の右側を作成します。Expression.New()
メソッドでクラスのインスタンスを作成する式を作成します。先に取得したコンストラクタのCoinstructorInfo を与えます。第二引数にコンストラクタの引数パラメータの ParameterExpression を与えます。
Expression.Lambda()
メソッドでラムダ式を作成します。Lambdaメソッドも第二引数にコンストラクタの引数パラメータの ParameterExpression を与えます。
その後、作成したラムダ式のオブジェクト LambdaExpression
のCompile() メソッドを呼び出しデリゲートを作成します。
NewExpression instance = Expression.New(ctor, new ParameterExpression[] { arg1, arg2, arg3, arg4 });
LambdaExpression lambda = Expression.Lambda<Func<int, int, string, string, DemoParamCompClass>>(instance, new ParameterExpression[] { arg1, arg2, arg3, arg4 });
Func<int, int, string, string, DemoParamCompClass> func = (Func<int, int, string, string, DemoParamCompClass>)lambda.Compile();
コンストラクタのデリゲートを呼び出します。インスタンスが生成されますので、メンバ変数の値をテキストボックスに表示します。
DemoParamCompClass cls = func(8,6,"Penguin","P-101");
textBox1.Text = string.Format("{0:d},{1:d} / {2} : {3}", cls.value_x, cls.value_y, cls.value_name, cls.value_code);
プロジェクトを実行します。下図のウィンドウが表示されます。
コードを実装した[button3]をクリックします。テキストボックスに下図のテキストが表示されます。コードのコンストラクタで与えた 8
6
"Penguin"
"P-101"
の値が表示できていることが確認できます。
コンストラクタの引数が不定の場合のコード例を紹介します。
Windows Formアプリケーションを作成します。
下図のフォームを作成します。
テキストボックスとボタンを1つ配置します。
下記のコードを記述します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DynamicDelegate
{
public class DemoClass
{
public int value_n;
public string value_s;
public DemoClass(int n, string s)
{
value_n = n;
value_s = s;
}
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Reflection;
namespace DynamicDelegate
{
public partial class FormDynamicMethod : Form
{
public FormDynamicMethod()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
DynamicConstructor<DemoClass> ctor = GetDynamicConstructor<DemoClass>();
DemoClass instance = ctor(new string[] { "123", "abc", "211", "ghy" });
textBox1.Text = string.Format("{0:d} {1}",instance.value_n, instance.value_s);
}
public delegate T DynamicConstructor<T>(string[] args);
public DynamicConstructor<T> GetDynamicConstructor<T>()
{
// (a[1],a[2],a[3],a[4]) => T.Constructor(a[1],a[2],a[3],a[4]);
ConstructorInfo originalCtor = typeof(T).GetConstructors().First();
var parameter = Expression.Parameter(typeof(string[]), "args");
var parameterExpressions = new List<Expression>();
ParameterInfo[] paramsInfo = originalCtor.GetParameters();
for (int i = 0; i < paramsInfo.Length; i++) {
Type paramType = paramsInfo[i].ParameterType;
Expression defaultValueExp;
if (paramsInfo[i].HasDefaultValue) {
defaultValueExp = Expression.Constant(paramsInfo[i].DefaultValue);
}
else {
defaultValueExp = Expression.Default(paramType);
}
Expression paramValue;
paramValue = Expression.ArrayIndex(parameter, Expression.Constant(i));
if (paramType.IsEnum) {
var enumParse = typeof(Enum).GetMethod("Parse", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(Type), typeof(string) }, null);
var call = Expression.Call(null, enumParse, new[] { Expression.Constant(paramType), paramValue });
paramValue = Expression.Convert(call, paramType);
}
else if (paramType != typeof(string)) {
var parseMethod = paramType.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(string) }, null);
if (parseMethod == null) {
throw new Exception($"Cannot find Parse method for type {paramType} (parameter index:{i})");
}
paramValue = Expression.Call(null, parseMethod, new[] { paramValue });
}
Expression boundsCheck = Expression.LessThan(Expression.Constant(i), Expression.ArrayLength(parameter));
paramValue = Expression.Condition(boundsCheck, paramValue, defaultValueExp);
parameterExpressions.Add(paramValue);
}
var newExp = Expression.New(originalCtor, parameterExpressions);
var lambda = Expression.Lambda<DynamicConstructor<T>>(newExp, parameter);
return lambda.Compile();
}
}
}
動的コンストラクタのオブジェクトを宣言します。GetDynamicConstructor()
メソッドでコンストラクタを動的に取得します。
DynamicConstructor<DemoClass> ctor = GetDynamicConstructor<DemoClass>();
コンストラクタを返す、GetDynamicConstructor
メソッドで次のラムダ式を作成します。
(a[1],a[2],a[3],a[4], ... a[n]) => T.Constructor(a[1],a[2],a[3],a[4], ... a[m]);
T
のクラスのコンストラクタの最初のコンストラクタを取得します。今回のコードでは GetDynamicConstructor
呼び出し元のDemoClass
がT
になります。
ConstructorInfo originalCtor = typeof(T).GetConstructors().First();
パラメーターオブジェクト、ラムダ式オブジェクトの宣言と、先に取得したコンストラクターのパラメータ情報を取得します。
var parameter = Expression.Parameter(typeof(string[]), "args");
var parameterExpressions = new List<Expression>();
ParameterInfo[] paramsInfo = originalCtor.GetParameters();
コンストラクタのパラメーターの個数ループします。
for (int i = 0; i < paramsInfo.Length; i++) {
// (中略)
}
パラメータの型は、ParameterInfo
オブジェクトの ParameterType
で取得します。
デフォルト値がある場合は、デフォルト値に関する設定します。
Type paramType = paramsInfo[i].ParameterType;
Expression defaultValueExp;
if (paramsInfo[i].HasDefaultValue) {
defaultValueExp = Expression.Constant(paramsInfo[i].DefaultValue);
}
else {
defaultValueExp = Expression.Default(paramType);
}
コンストラクタのパラメーターに対応したラムダ式を作成します。パラメーター部ジェクトを作成し、ラムダ式のオブジェクトparameterExpressions
に追加していきます。
Expression paramValue;
paramValue = Expression.ArrayIndex(parameter, Expression.Constant(i));
if (paramType.IsEnum) {
var enumParse = typeof(Enum).GetMethod("Parse", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(Type), typeof(string) }, null);
var call = Expression.Call(null, enumParse, new[] { Expression.Constant(paramType), paramValue });
paramValue = Expression.Convert(call, paramType);
}
else if (paramType != typeof(string)) {
var parseMethod = paramType.GetMethod("Parse", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(string) }, null);
if (parseMethod == null) {
throw new Exception($"Cannot find Parse method for type {paramType} (parameter index:{i})");
}
paramValue = Expression.Call(null, parseMethod, new[] { paramValue });
}
Expression boundsCheck = Expression.LessThan(Expression.Constant(i), Expression.ArrayLength(parameter));
paramValue = Expression.Condition(boundsCheck, paramValue, defaultValueExp);
parameterExpressions.Add(paramValue);
}
ラムダ式のオブジェクトを作成します。Compile()
メソッド呼び出し、ラムダ式のオブジェクトを作成します。
var newExp = Expression.New(originalCtor, parameterExpressions);
var lambda = Expression.Lambda<DynamicConstructor<T>>(newExp, parameter);
return lambda.Compile();
取得したコンストラクタのオブジェクトを呼び出します。引数にはstring型の配列を与えます。
先に parameter = Expression.Parameter(typeof(string[]), "args")
のコードでラムダ式のパラメーターを定義しているため、
コンストラクタのパラメータにはint, string のものがありますが、ラムダ式ではすべてstring型で記述します。
呼び出した結果として、クラスインスタンスが戻ります。
DemoClass instance = ctor(new string[] { "123", "abc", "211", "ghy" });
クラスインスタンスオブジェクトのメンバ変数にアクセスし、メンバ変数の内容をテキストボックスに表示します。
textBox1.Text = string.Format("{0:d} {1}",instance.value_n, instance.value_s);
プロジェクトを実行します。下図のフォームウィンドウが表示されます。
[button1]をクリックします。インスタンスオブジェクトが生成され、value_n
value_s
の値がテキストボックスに表示されます。インスタンス生成時に与えたstring配列の先頭から2つの値が
メンバ変数に設定されていることが確認できます。
複雑なコードですが、このコードでは、コンストラクタの引数の個数と、呼び出し元のパラメータ個数が違っていても動作するメリットがあります。
今回のコードでもコンストラクタには{ "123", "abc", "211", "ghy" }
を与えていますが、最初の2つの項目のみが利用されており、コンパイルエラーにはなりません。
また、呼び出し元のコードを下記に変更します。
private void button1_Click(object sender, EventArgs e)
{
DynamicConstructor<DemoParamCompClass> ctor = GetDynamicConstructor<DemoParamCompClass>();
DemoParamCompClass instance = ctor(new string[] { "123", "211", "abc", "ghy" }); // parse "123" as int
textBox1.Text = string.Format("{0:d} {1:d} {2} {3}", instance.value_x, instance.value_y, instance.value_code, instance.value_name);
}
namespace DynamicDelegate
{
public class DemoParamCompClass
{
public int value_x;
public int value_y;
public string value_name;
public string value_code;
public DemoParamCompClass(int x, int y, string name, string code)
{
value_x = x;
value_y = y;
value_name = name;
value_code = code;
}
}
}
上記の変更したコードを実行し、[button1]をクリックした結果が下図です。インスタンス生成時に与えた引数が4つともメンバ変数に設定されていることが確認できます。