4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】動的にクラスを作り上げる

Last updated at Posted at 2023-04-20

なんでそんなのが必要になったのか

システム上で設定された任意の計算式を解析して計算する必要があった。
解決するために.NET Standard2.0で利用でき、MITライセンスで、システムで利用する数式を全部カバーできてたJustEvaluateを利用することにした。

数式の埋め込みパラメーターについて、GitHubの説明にある通りの使い方だけど、どうせKeyValuePair<string, decimal>とかでも通るだろと高を括ってた。
しかし、後から気づいたのが、数式の埋め込みパラメーターとするオブジェクトは、プロパティとして定義されていないとダメだった。

つまり、特定クラスに対して、パラメーター名のプロパティが全部揃ってないといけない!
でも数式はシステム上で任意で指定するものだから、プロパティの種類と数なんてわからん!
てところから、動的にクラスを生み出すしかねぇ!!となった。

作った

あまりよく理解はできてないけど、publicで読み書き可能なプロパティを有するクラスを作り上げる!
呼び元はdynamicとかobjectで受け取る。

動的だから当然パフォーマンスは悪いんだろうけど、私が行うシチュエーションでは大量に処理することがないので、大して気にならないレベル。

PropertySet.cs
using System;

namespace Hoge
{
    /// <summary>
    /// 計算式に利用するパラメーターのプロパティセットを提供します。
    /// </summary>
    public class PropertySet
    {
        /// <summary>
        /// プロパティ名 を取得します。
        /// </summary>
        public string PropertyName { get; }

        /// <summary>
        /// プロパティのタイプ を取得します。
        /// </summary>
        public Type PropertyType { get; }

        /// <summary>
        /// プロパティの値 を取得します。
        /// </summary>
        public object Value { get; }

        /// <summary>
        /// 新しいインスタンスを生成します。
        /// </summary>
        /// <param name="propertyName">プロパティ名。</param>
        /// <param name="propertyType">プロパティのタイプ。</param>
        /// <param name="value">プロパティの値。</param>
        public PropertySet(string propertyName, Type propertyType, object value)
        {
            PropertyName = propertyName;
            PropertyType = propertyType;
            Value = value;
        }
    }
}
CalcParameterTypeBuilder.cs
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace Hoge
{
    /// <summary>
    /// 計算式に利用するパラメーターのクラスの生成を提供します。
    /// </summary>
    public static class CalcParameterTypeBuilder
    {
        private static readonly string TypeName = "CalcParameterType";

        private static readonly string ModuleName = "MainModule";

        /// <summary>
        /// 指定されたプロパティセットを有するオブジェクトを生成します。
        /// </summary>
        /// <param name="propertySets">プロパティセット。</param>
        /// <returns>指定されたプロパティセットを有するオブジェクト。</returns>
        public static object CreateNewObject(List<PropertySet> propertySets)
        {
            var myType = CompileResultType(propertySets);
            var newObj = Activator.CreateInstance(myType);

            // プロパティに値を入れる
            var type = newObj.GetType();
            foreach (var propertySet in propertySets)
            {
                var property = type.GetProperty(propertySet.PropertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty);
                if (property == null)
                {
                    continue;
                }
                property.SetValue(newObj, Convert.ChangeType(propertySet.Value, property.PropertyType));
            }

            return newObj;
        }

        /// <summary>
        /// 指定されたプロパティセットを有するタイプを生成します。
        /// </summary>
        /// <param name="propertySets">プロパティセット。</param>
        /// <returns>指定されたプロパティセットを有するタイプ。</returns>
        public static Type CompileResultType(List<PropertySet> propertySets)
        {
            var tb = GetTypeBuilder();
            tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

            foreach (var propertySet in propertySets)
            {
                CreateProperty(tb, propertySet.PropertyName, propertySet.PropertyType);
            }

            return tb.CreateType();
        }

        /// <summary>
        /// 生成するクラスのタイプビルダーの取得を行う。
        /// </summary>
        /// <returns></returns>
        private static TypeBuilder GetTypeBuilder()
        {
            var assmName = new AssemblyName(TypeName);
            var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assmName, AssemblyBuilderAccess.Run);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule(ModuleName);
            var tb = moduleBuilder.DefineType(TypeName,
                    TypeAttributes.Public |
                    TypeAttributes.Class |
                    TypeAttributes.AutoClass |
                    TypeAttributes.AnsiClass |
                    TypeAttributes.BeforeFieldInit |
                    TypeAttributes.AutoLayout,
                    null);

            return tb;
        }

        /// <summary>
        /// プロパティの生成を行う。
        /// </summary>
        /// <param name="tb">タイプビルダー。</param>
        /// <param name="propertyName">プロパティ名。</param>
        /// <param name="propertyType">プロパティのタイプ。</param>
        private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
        {
            var fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);
            var propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);

            // プロパティのGetアクセサ
            var propertyGetMethodBuilder = tb.DefineMethod("get_" + propertyName,
                MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
                propertyType, Type.EmptyTypes);
            var getIlGen = propertyGetMethodBuilder.GetILGenerator();
            getIlGen.Emit(OpCodes.Ldarg_0);
            getIlGen.Emit(OpCodes.Ldfld, fieldBuilder);
            getIlGen.Emit(OpCodes.Ret);
            propertyBuilder.SetGetMethod(propertyGetMethodBuilder);

            // プロパティのSetアクセサ
            var propertySetMethodBuilder =
                tb.DefineMethod("set_" + propertyName,
                  MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
                  null, new[] { propertyType });
            var setIlGen = propertySetMethodBuilder.GetILGenerator();
            setIlGen.MarkLabel(setIlGen.DefineLabel());
            setIlGen.Emit(OpCodes.Ldarg_0);
            setIlGen.Emit(OpCodes.Ldarg_1);
            setIlGen.Emit(OpCodes.Stfld, fieldBuilder);
            setIlGen.Emit(OpCodes.Nop);
            setIlGen.MarkLabel(setIlGen.DefineLabel());
            setIlGen.Emit(OpCodes.Ret);
            propertyBuilder.SetSetMethod(propertySetMethodBuilder);
        }
    }
}
4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?