C#中的动态构造函数

时间:2018-06-17 20:02:00

标签: c# dynamic constructor runtime

我正在尝试编写一个方法GetDynamicConstructor<T>,它将返回一个给定类的智能构造函数。它将接受一个字符串数组作为参数,并将它们解析为适当的类型(给定现有的构造函数数据)。

public void Init()
{
    DynamicConstructor<MyClass> ctor = GetDynamicConstructor<MyClass>();
    MyClass instance = ctor(new string[] { "123", "abc" }); // parse "123" as int
}

public delegate T DynamicConstructor<T>(string[] args);

public DynamicConstructor<T> GetDynamicConstructor<T>()
{
    ConstructorInfo originalCtor = typeof(T).GetConstructors().First();

    ParameterInfo[] paramsInfo = originalCtor.GetParameters();

    for (int i = 0; i < paramsInfo.Length; i++) {
        Type paramType = paramsInfo[i].ParameterType;

        // This is as far as I got :D
    }

    return null;
}

public class MyClass
{
    int n;
    string s;

    public MyClass(int n, string s)
    {
        this.n = n;
        this.s = s;
    }
}

我想要的,基本上是从MyClass构建一个看起来像这样的方法。

public MyClass Example(string[] args)
{
    return new MyClass(int.Parse(args[0]), args[1]);
}

这里只会有基本类型,所以我可以指望我可能遇到的类型存在Parse

如何撰写GetDynamicConstructor<T>的正文?

2 个答案:

答案 0 :(得分:2)

根据您的确切使用方式,有几种方法可以实现。 Steve16351有一种方法是创建一个方法的委托,该方法在执行时执行所有反射。另一种方法是生成一个委托,它在执行时看起来像你的Example方法,然后缓存。不同之处在于前者可以更灵活,而后者更快。在每次执行时使用反射可以在选择构造函数之前考虑哪些转换成功。虽然编译的委托必须知道在参数可用之前要选择哪个构造函数,但它的性能特征更像是在C#中本机编写的方法。下面是使用表达式树生成委托的实现。您希望为每种类型缓存此内容以获得最佳性能:

using System.Linq.Expressions;

public static DynamicConstructor<T> GetDynamicConstructor<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++)
    {
        Type paramType = paramsInfo[i].ParameterType;

        Expression 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 });
        }            

        parameterExpressions.Add(paramValue);
    }

    var newExp = Expression.New(originalCtor, parameterExpressions);
    var lambda = Expression.Lambda<DynamicConstructor<T>>(newExp, parameter);
    return lambda.Compile();
}

请注意,我添加了枚举的处理,因为Parse的调用方式与其他简单类型不同。

更新

基于评论,这里是一个扩展版本,用于发出将处理默认参数值的非泛型委托:

    public static DynamicConstructor GetDynamicConstructor(Type type)
    {
        ConstructorInfo originalCtor = type.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;

            // added check for default value on the parameter info.
            Expression defaultValueExp;
            if (paramsInfo[i].HasDefaultValue)
            {
                defaultValueExp = Expression.Constant(paramsInfo[i].DefaultValue);
            }
            else
            {
                // if there is no default value, then just provide 
                // the type's default value, but we could potentially 
                // do something else here
                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 });
            }

            // here we bounds check the array and emit a conditional expression
            // that will provide a default value if necessary. Equivalent to 
            // something like i < args.Length ? int.Parse(args[i]) : default(int);  
            // Of course if the parameter has a default value that is used instead, 
            // and if the target type is different (long, boolean, etc) then
            // we use a different parse method.
            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>(newExp, parameter);
        return lambda.Compile();
    }
}

答案 1 :(得分:1)

此方法已存在于System.Activator类:

public static object CreateInstance (Type type, params object[] args);

当然必须存在与实际参数数据相对应的构造函数重载。您可以使用Convert.ChangeType Method (Object, Type)更改参数类型。

请参阅docs.microsoft.com上的CreateInstance(Type, Object[])

Activator.CreateInstance有16种不同的重载。