lap
lap

Reputation: 125

Dynamic constructor in C#

I am trying to write a method GetDynamicConstructor<T> which will return, essentially, a smart constructor for the given class. It will accept an array of strings as parameters and parse them as the appropriate type (given existing constructor data).

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

What I want, basically, is to construct from MyClass a method which looks like this.

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

There will only be basic types here so I can count on a Parse existing for the types I might run into.

How can I write the body of GetDynamicConstructor<T>?

Upvotes: 3

Views: 7420

Answers (2)

Olivier Jacot-Descombes
Olivier Jacot-Descombes

Reputation: 112632

This method already exists in the System.Activator class:

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

Of course a constructor overload corresponding to the actual parameter data must exist. You can use Convert.ChangeType Method (Object, Type) to change the type of the parameters.

See: CreateInstance(Type, Object[]) on learn.microsoft.com.

Activator.CreateInstance has 16 different overloads.

Upvotes: 3

Mike Zboray
Mike Zboray

Reputation: 40838

Depending on how exactly you want use this there are a couple ways to do it. Steve16351 has one way which is to create a delegate to a method that does all the reflection at execution time. Another way would be to generate a delegate which looks like your Example method at execution time, which is then cached. The difference would be that the former can be more flexible, while the latter would be faster. Using reflection on each execution can take into account which conversions are successful before selecting the constructor. While a compiled delegate has to know which constructor to select before arguments are available, it will have performance characteristics more like a method that had been written natively in C#. Below is in an implementation to generate the delegate using expression trees. You'd want to cache this for each type for maximum performance:

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

Note that I added handling of enums since Parse can't be called the same way as other simple types.

Update:

Based on comments here is an expanded version to emit a non-generic delegate that will handle default parameter values:

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

Upvotes: 2

Related Questions