Создайте экземпляр класса по строке, но примите во внимание неявные преобразования — C# Reflection

Я нахожусь в ситуации, когда мне нужно создать экземпляр объекта с заданным типом (в виде строки) и массивом аргументов конструктора.

Вот как я этого добиваюсь:

public object Create(string Name, params object[] Args)
{
    return Activator.CreateInstance(Type.GetType(Name), Args);
}

В большинстве случаев это работает нормально, но с этим есть проблема; он не принимает во внимание неявные преобразования.


Позвольте мне объяснить, что я имею в виду, скажем, у нас есть простой класс с неявным преобразованием в тип int.

public class ImplicitTest
{
    public double Val { get; set; }

    public ImplicitTest(double Val)
    {
        this.Val = Val;
    }

    public static implicit operator int(ImplicitTest d)
    {
        return (int)d.Val;
    }
}

и у нас есть класс, который использует int в качестве параметра конструктора

public class TestClass
{
    public int Val { get; set; }        

    public TestClass(int Val)
    {
        this.Val = Val;
    }
}

Теперь скажем, что мы хотим создать экземпляр TestClass, мы можем сделать: new TestClass(5). В этом случае мы используем именно тот тип параметра, который указывает конструктор (int). Однако мы также можем создать экземпляр класса, используя наш класс ImplicitTest, как таковой: new TestClass(new ImplicitTest(5.1)). Это работает, потому что параметр неявно преобразуется из ImplicitTest в int. Однако Activator.CreateInstance() этого не делает.


Мы можем использовать наш предыдущий метод Create(string Name, params object[] Args), чтобы создать экземпляр TestClass как таковой: Create("ThisNamespace.TestClass", 5), это работает. Проблема, с которой я сталкиваюсь, заключается в том, что попытка использовать неявные преобразования не работает, поэтому этот фрагмент кода выдает ошибку: Create("ThisNamespace.TestClass", new ImplicitTest(5.1))

Я совершенно не знаю, как это учесть, но это важно для моего варианта использования. Может быть, какой-то параметр функции Activator.CreateInstance() мне не хватает, или, может быть, есть совершенно другой метод, который я могу использовать для достижения своей цели? Я не смог найти никаких ответов.


TL;DR

//Valid
new TestClass(5);

//Valid
new TestClass(new ImplicitTest(5.1));

//Valid
Activator.CreateInstance(Type.GetType("ThisNamespace.TestClass"), 5); 

//Invalid, throws System.MissingMethodException
Activator.CreateInstance(Type.GetType("ThisNamespace.TestClass"), new ImplicitTest(5.1)); 

Почему?


person Pema Malling    schedule 08.03.2016    source источник
comment
Аналогичная тема, которая может помочь: stackoverflow.com/questions/20932208/   -  person Steve Wong    schedule 08.03.2016
comment
Поскольку implicit operator int — это static, он не переопределяет ни один метод класса object. И с Activator.CreateInstance Args вы проходите objects   -  person Matteo Umili    schedule 08.03.2016
comment
Можно использовать динамическую инфраструктуру .NET. Сборка Microsoft.CSharp имеет правила C# для выбора перегрузки. См. ideone.com/UkGm4E . Я недостаточно опытен, чтобы перевести его на общий рабочий метод (динамический - не моя область С#)   -  person xanatos    schedule 08.03.2016
comment
@xanatos Это на самом деле кажется довольно многообещающим, большое спасибо!   -  person Pema Malling    schedule 08.03.2016


Ответы (4)


Неявное преобразование типов — это функция компилятора C#, а не CLR.

Поэтому, если вы хотите использовать Reflection, вам придется добиться этого вручную.

person Matias Cicero    schedule 08.03.2016
comment
Добавлю, что это задача класса Binder (используется в ConstructorInfo GetConstructor(BindingFlags bindingAttr, Binder binder, ...). К сожалению, в Интернете нет CSharpBinder - person xanatos; 08.03.2016

Готовы ли вы реализовать IConvertible в своем классе?
Вам не нужно реализовывать все методы, а только те, которые вам нужны.
Если да, вы можете использовать что-то вроде следующего. (Вы можете сделать его более общим, чтобы вам не приходилось искать конкретный конструктор, как я сейчас)

using System;
using System.Reflection;

namespace ConsoleApplication
{
    class Program
    {
        static void Main()
        {
            var theType = Type.GetType("ConsoleApplication.TestClass");

            ConstructorInfo ctor = theType.GetConstructors()[0];
            var argumentType = ctor.GetParameters()[0].ParameterType;

            object contructorArgument = new ImplicitTest(5.1);

            object instance = ctor.Invoke(new object[] { Convert.ChangeType(contructorArgument, argumentType) });

            Console.ReadLine();
        }
    }

    public class TestClass
    {
        public int Val { get; set; }

        public TestClass(int Val)
        {
            this.Val = Val;
        }

    }

    public class ImplicitTest : IConvertible
    {
        public double Val { get; set; }

        public ImplicitTest(double Val)
        {
            this.Val = Val;
        }

        public static implicit operator int(ImplicitTest d)
        {
            return (int)d.Val;
        }

        public int ToInt32(IFormatProvider provider)
        {
            return (int)this;
        }

        //Other methods of IConvertible
    }
}
person George Vovos    schedule 08.03.2016

Я не эксперт в динамической части С#, но, похоже, это работает правильно:

using System;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.CSharp.RuntimeBinder; // Requires reference to Microsoft.CSharp

public class ImplicitTest
{
    public double Val { get; set; }

    public ImplicitTest(double val)
    {
        this.Val = val;
    }

    public static implicit operator int(ImplicitTest d)
    {
        return (int)d.Val;
    }
}

public class TestClass
{
    public int Val { get; set; }

    public TestClass(int val = 5)
    {
        this.Val = val;
    }

    public TestClass(int val1, int val2)
    {
        this.Val = val1 + val2;
    }

    public TestClass(int val1, int val2, int val3, int val4, int val5, int val6, int val7, int val8, int val9, int val10, int val11, int val12, int val13, int val14)
    {
        this.Val = val1 + val2 + val3 + val4 + val5 + val6 + val7 + val8 + val9 + val10 + val11 + val12 + val13 + val14;
    }
}

public static class DynamicFactory
{
    private static readonly CallSiteBinder callsiteBinder0 = Binder.InvokeConstructor(
        CSharpBinderFlags.None,
        typeof(DynamicFactory),
        // It is OK to have too many arguments :-)
        new CSharpArgumentInfo[] 
        {
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.IsStaticType | CSharpArgumentInfoFlags.UseCompileTimeType, null), // 0 parameters
        });

    private static readonly CallSiteBinder callsiteBinder = Binder.InvokeConstructor(
        CSharpBinderFlags.None,
        typeof(DynamicFactory),
        // It is OK to have too many arguments :-)
        // Note that this "feature" doesn't work correctly with Mono in the
        // case of 0 arguments
        new CSharpArgumentInfo[] 
        {
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.IsStaticType | CSharpArgumentInfoFlags.UseCompileTimeType, null), // 0 parameters
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), // 1 parameter
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), // 2 parameters
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), 
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), 
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), 
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), // 14 parameters
        });

    // Quirk of Mono with 0 arguments. See callsiteBinder0
    private static readonly CallSite<Func<CallSite, Type, object>> CallSite0 = CallSite<Func<CallSite, Type, object>>.Create(callsiteBinder0);

    private static readonly CallSite<Func<CallSite, Type, object, object>> CallSite1 = CallSite<Func<CallSite, Type, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object>> CallSite2 = CallSite<Func<CallSite, Type, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object>> CallSite3 = CallSite<Func<CallSite, Type, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object>> CallSite4 = CallSite<Func<CallSite, Type, object, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object, object>> CallSite5 = CallSite<Func<CallSite, Type, object, object, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object, object, object>> CallSite6 = CallSite<Func<CallSite, Type, object, object, object, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object>> CallSite7 = CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object>> CallSite8 = CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object>> CallSite9 = CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object, object>> CallSite10 = CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object, object, object>> CallSite11 = CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object, object, object, object>> CallSite12 = CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object, object, object, object, object>> CallSite13 = CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object, object, object, object, object>>.Create(callsiteBinder);
    private static readonly CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object, object, object, object, object, object>> CallSite14 = CallSite<Func<CallSite, Type, object, object, object, object, object, object, object, object, object, object, object, object, object, object, object>>.Create(callsiteBinder);

    public static object Create(string typeName, params object[] args)
    {
        return Create(Type.GetType(typeName), args);
    }

    public static object Create(Type type, params object[] args)
    {
        if (type == null)
        {
            throw new ArgumentNullException("type");
        }

        if (args == null)
        {
            args = new object[0];
        }

        object obj;

        switch (args.Length)
        {
            case 0:
                // Quirk of Mono with 0 arguments. See callsiteBinder0
                obj = CallSite0.Target(CallSite0, type);
                break;

            case 1:
                obj = CallSite1.Target(CallSite1, type, args[0]);
                break;

            case 2:
                obj = CallSite2.Target(CallSite2, type, args[0], args[1]);
                break;

            case 3:
                obj = CallSite3.Target(CallSite3, type, args[0], args[1], args[2]);
                break;

            case 4:
                obj = CallSite4.Target(CallSite4, type, args[0], args[1], args[2], args[3]);
                break;

            case 5:
                obj = CallSite5.Target(CallSite5, type, args[0], args[1], args[2], args[3], args[4]);
                break;

            case 6:
                obj = CallSite6.Target(CallSite6, type, args[0], args[1], args[2], args[3], args[4], args[5]);
                break;

            case 7:
                obj = CallSite7.Target(CallSite7, type, args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
                break;

            case 8:
                obj = CallSite8.Target(CallSite8, type, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
                break;

            case 9:
                obj = CallSite9.Target(CallSite9, type, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
                break;

            case 10:
                obj = CallSite10.Target(CallSite10, type, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);
                break;

            case 11:
                obj = CallSite11.Target(CallSite11, type, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10]);
                break;

            case 12:
                obj = CallSite12.Target(CallSite12, type, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11]);
                break;

            case 13:
                obj = CallSite13.Target(CallSite13, type, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12]);
                break;

            case 14:
                obj = CallSite14.Target(CallSite14, type, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13]);
                break;

            default:
                throw new ArgumentException("Too many parameters");
        }

        return obj;
    }
}

public class Program
{
    public static void Main()
    {
        try
        {
            Type monoRuntime = Type.GetType("Mono.Runtime");

            if (monoRuntime != null)
            {
                System.Reflection.MethodInfo displayName = monoRuntime.GetMethod("GetDisplayName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

                if (displayName != null)
                {
                    Console.WriteLine("Mono version {0}", displayName.Invoke(null, null));
                }
            }

            TestClass tc0 = (TestClass)DynamicFactory.Create("TestClass");
            TestClass tc1 = (TestClass)DynamicFactory.Create("TestClass", new ImplicitTest(1.0));
            TestClass tc1b = (TestClass)DynamicFactory.Create("TestClass", 1);
            TestClass tc2 = (TestClass)DynamicFactory.Create("TestClass", new ImplicitTest(1.0), new ImplicitTest(2.0));
            TestClass tc14 = (TestClass)DynamicFactory.Create("TestClass", Enumerable.Range(0, 14).Select(x => new ImplicitTest((double)x)).ToArray());

            Console.WriteLine(tc0.Val);
            Console.WriteLine(tc1.Val);
            Console.WriteLine(tc1b.Val);
            Console.WriteLine(tc2.Val);
            Console.WriteLine(tc14.Val);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
    }
}

Он использует «динамическую» часть .NET. Он использует Microsoft.CSharp.RuntimeBinder, который "знает" правила привязки C#. Как заметил кто-то другой, определяемые пользователем неявные и явные приведения типов относятся к С#, а не к .NET, поэтому вам нужен связующий элемент, который «знает» о них. Идеальным решением было бы иметь подкласс класса System.Reflection.Binder, который «знает» правила C#. Тогда вы могли бы передать его Type.GetConstructor и жить счастливо. Жаль, что нет такой реализации.

Обратите внимание, что, к сожалению, делегат хранится в общем подклассе CallSite, CallSite<T>, где T является типом делегата. Именно поэтому большая switch инструкция.

Идея кода: https://ideone.com/NoQ67H. Обратите внимание, что в Mono есть особенность, поэтому конструкторы с нулевым параметром имеют специальную обработку.

person xanatos    schedule 09.03.2016

Вы можете воспользоваться фреймворком Expressions, вот пример, который должен помочь:

class Program
{
    static object Create(string Name, params object[] Args)
    {
        Type theType = Type.GetType(Name);
        object toReturn = null;
        // Code wrote quickly, I just try to call all the type constructors...
        foreach (var constructor in Type.GetType(Name).GetConstructors())
        {
            try
            {
                string paramPrefix = "p";
                int pIdx = 0;
                var expParamsConsts = new List<Expression>();
                var ctrParams = constructor.GetParameters();
                for (int i = 0; i < constructor.GetParameters().Length; i++)
                {
                    var param = ctrParams[i];
                    var tmpParam = Expression.Variable(param.ParameterType, paramPrefix + pIdx++);
                    var expConst = Expression.Convert(Expression.Constant(Args[i]), param.ParameterType);
                    expParamsConsts.Add(expConst);
                }
                // new Type(...);
                var expConstructor = Expression.New(constructor, expParamsConsts);
                // return new Type(...);
                var expLblRetTarget = Expression.Label(theType);
                var expReturn = Expression.Return(expLblRetTarget, expConstructor, theType);
                var expLblRet = Expression.Label(expLblRetTarget, Expression.Default(theType));
                // { return new Type(...); }
                var expBlock = Expression.Block(expReturn, expLblRet);
                // Build the expression and run it
                var expFunc = Expression.Lambda<Func<dynamic>>(expBlock);
                toReturn = expFunc.Compile().Invoke();
            }
            catch (Exception ex)
            {
                ex.ToString();
            }
        }
        return toReturn;
    }

    static void Main(string[] args)
    {
        var tmpTestClass = Create(TYPE_NAME, 5);
        tmpTestClass = Create(TYPE_NAME, new ImplicitTest(5.1));
    }
}

public class ImplicitTest
{
    public double Val { get; set; }

    public ImplicitTest(double Val)
    {
        this.Val = Val;
    }

    public static implicit operator int(ImplicitTest d)
    {
        return (int)d.Val;
    }
}

public class TestClass
{
    public int Val { get; set; }

    public TestClass(int Val)
    {
        this.Val = Val;
    }
}

Возможно, есть более быстрый способ сделать это, но я не очень разбираюсь в выражениях...

person Matteo Umili    schedule 15.03.2016
comment
Да, легко увидеть, что вы не очень часто используете Expressions - большинство людей просто строят дерево выражений, а не сохраняют все промежуточные значения в локальных переменных :D Очевидно, что функциональное программирование на вас пока не сильно повлияло. В любом случае, это отличное решение практически в любое время, когда вам нужно отражение, а также использование того, что обычно обрабатывается компилятором (например, упаковка, неявные преобразования, разрешение перегрузки...). Хотя я бы действительно использовал это только тогда, когда есть только один конструктор на выбор, поскольку выбор правильного конструктора, подобного этому, неоднозначен. - person Luaan; 15.03.2016
comment
Спасибо, в данном случае я решил использовать промежуточные переменные, потому что мне это показалось более читаемым - person Matteo Umili; 15.03.2016
comment
Да, это связано с тем, что вы не привыкли к функциональному программированию. Попробуйте немного поэкспериментировать с FP, и вам станет намного удобнее. Что очень важно, когда C# со временем становится все более и более функциональным (делегаты, деревья выражений, LINQ,...). Ключевым моментом является то, что чистые функции позволяют вам рассуждать об отдельных вызовах функций изолированно, поэтому пропуск промежуточных звеньев не обязательно ухудшает читабельность. Но на самом деле это просто стилистический вопрос, ничего особенно важного. Однако FP — отличный инструмент для вашего набора инструментов :) - person Luaan; 15.03.2016
comment
@Luaan Да, легко заметить, что вы не очень часто используете выражения Я пишу много Expression и сохраняю все в отдельной переменной. Альтернативой являются строки кода, которые идут слева от моей таблицы справа от таблицы :-) - person xanatos; 15.03.2016
comment
@xanatos Ну, типичным решением для этого является использование многострочных выражений :) Но да, это стилистический выбор. Я часто вижу это у программистов, которые начали под влиянием FP, а не у императивных программистов старой школы. Вероятно, помогает то, что функциональная программа all представляет собой всего лишь одно выражение (теоретически) — использование промежуточных звеньев просто выглядит попыткой придать коду императивный вид, хотя на самом деле это не так. Вы также храните частичные (но не динамические) запросы LINQ в локальных файлах? Или вы используете свободный синтаксис? Это в основном эквивалентно. - person Luaan; 15.03.2016
comment
@Luaan Я склонен переходить на новую строку непосредственно перед точечным вызовом ... (newline).Where(...)(newline).Select и так далее. - person xanatos; 15.03.2016
comment
@xanatos Да, именно это я и делаю (если это не очень короткий запрос). И я использую тот же подход с деревьями выражений, у вас просто круглые скобки вместо ., по сути. Существует множество причин, по которым функциональные языки избегают круглых скобок вокруг аргументов — если вы посмотрите, как выглядел бы код без точек и круглых скобок, вы увидите, что в конечном итоге они выглядят одинаково. - person Luaan; 15.03.2016