C # Использование Activator.CreateInstance - PullRequest
50 голосов
/ 10 марта 2011

Вчера я задал вопрос об использовании отражения или шаблона стратегии для динамического вызова методов.

Однако с тех пор я решил изменить методы на отдельные классы, которые реализуют общий интерфейс.Причина в том, что каждый класс, имея некоторое сходство, также выполняет определенные методы, уникальные для этого класса.

Я использовал стратегию как таковую:

switch (method)
{
    case "Pivot":
        return new Pivot(originalData);
    case "GroupBy":
        return new GroupBy(originalData);
    case "Standard deviation":
        return new StandardDeviation(originalData);
    case "% phospho PRAS Protein":
        return new PhosphoPRASPercentage(originalData);
    case "AveragePPPperTreatment":
        return new AveragePPPperTreatment(originalData);
    case "AvgPPPNControl":
        return new AvgPPPNControl(originalData);
    case "PercentageInhibition":
        return new PercentageInhibition(originalData);
    default:
        throw new Exception("ERROR: Method " + method + " does not exist.");
}

Однако, поскольку числоПотенциальные классы растут, мне нужно будет продолжать добавлять новые, тем самым нарушая правило закрытых для модификации.

Вместо этого я использовал такое решение:

var test = Activator.CreateInstance(null, "MBDDXDataViews."+ _class);
       ICalculation instance = (ICalculation)test.Unwrap();
       return instance;

Фактически, _classПараметр - это имя класса, переданного во время выполнения.Это обычный способ сделать это, будут ли какие-либо проблемы с производительностью?

Я довольно новичок в размышлениях, поэтому ваш совет будет приветствоваться.

Ответы [ 6 ]

71 голосов
/ 10 марта 2011

При использовании рефлексии вы должны сначала задать себе пару вопросов, потому что вы можете оказаться в сложном комплексном решении, которое сложно поддерживать:

  1. Есть ли способ решитьпроблема с использованием обобщенности или наследования класса / интерфейса?
  2. Можно ли решить проблему с помощью вызовов dynamic (только для .NET 4.0 и выше)?
  3. Важна ли производительность, т. е. будет ли отражен мой методили вызов экземпляра вызывается один, два или миллион раз?
  4. Могу ли я объединить технологии, чтобы получить умное, но выполнимое / понятное решение?
  5. Я согласен с потерей безопасности времени компиляции?

Универсальность / динамичность

Из вашего описания я предполагаю, что вы не знаете типы во время компиляции, вы знаете только, что они имеют общий интерфейс ICalculation.Если это правильно, то цифры (1) и (2), приведенные выше, вероятно, не возможны в вашем сценарии.

Производительность

Это важный вопрос, который нужно задать.Затраты на использование отражения могут помешать штрафу более чем в 400 раз: это замедляет даже умеренное количество вызовов.

Разрешение относительно простое: вместо использования Activator.CreateInstance используйте фабричный метод (выуже есть), найдите MethodInfo создайте делегата, кэшируйте его и используйте делегат с этого момента.Это дает только штраф при первом вызове, последующие вызовы имеют почти нативную производительность.

Объединение технологий

Здесь многое возможно, но мне действительно нужно знать больше о вашей ситуациипомогать в этом направлении.Часто я заканчиваю тем, что комбинирую dynamic с генериками с кэшированным отражением.При использовании сокрытия информации (как обычно в ООП) вы можете получить быстрое, стабильное и все еще хорошо расширяемое решение.

Потеря безопасности при компиляции типа

Из пяти вопросов:это, пожалуй, самый важный вопрос для беспокойства.Очень важно создать свои собственные исключения, которые дают четкую информацию об ошибках отражения.Это означает: каждый вызов метода, конструктора или свойства, основанный на входной строке или другой непроверенной информации, должен быть заключен в try / catch.Поймать только конкретные исключения (как всегда, я имею в виду: никогда не поймать Exception).

Сосредоточьтесь на TargetException (метод не существует), TargetInvocationException (метод существует, но при вызове был исключен), TargetParameterCountException, MethodAccessException (не нужные привилегии, часто случаетсяв ASP.NET) InvalidOperationException (происходит с универсальными типами).Вам не всегда нужно пытаться поймать их все, это зависит от ожидаемого ввода и ожидаемых целевых объектов.

Чтобы подвести итог

Избавьтесь от Activator.CreateInstance и используйтеMethodInfo, чтобы найти метод factory-create, и используйте Delegate.CreateDelegate для создания и кэширования делегата.Просто сохраните его в статическом Dictionary, где ключ равен строке класса в вашем примере кода.Ниже приведен быстрый, но не очень грязный способ сделать это безопасно и без потери безопасности.

Пример кода

public class TestDynamicFactory
{
    // static storage
    private static Dictionary<string, Func<ICalculate>> InstanceCreateCache = new Dictionary<string, Func<ICalculate>>();

    // how to invoke it
    static int Main()
    {
        // invoke it, this is lightning fast and the first-time cache will be arranged
        // also, no need to give the full method anymore, just the classname, as we
        // use an interface for the rest. Almost full type safety!
        ICalculate instanceOfCalculator = this.CreateCachableICalculate("RandomNumber");
        int result = instanceOfCalculator.ExecuteCalculation();
    }

    // searches for the class, initiates it (calls factory method) and returns the instance
    // TODO: add a lot of error handling!
    ICalculate CreateCachableICalculate(string className)
    {
        if(!InstanceCreateCache.ContainsKey(className))
        {
            // get the type (several ways exist, this is an eays one)
            Type type = TypeDelegator.GetType("TestDynamicFactory." + className);

            // NOTE: this can be tempting, but do NOT use the following, because you cannot 
            // create a delegate from a ctor and will loose many performance benefits
            //ConstructorInfo constructorInfo = type.GetConstructor(Type.EmptyTypes);

            // works with public instance/static methods
            MethodInfo mi = type.GetMethod("Create");

            // the "magic", turn it into a delegate
            var createInstanceDelegate = (Func<ICalculate>) Delegate.CreateDelegate(typeof (Func<ICalculate>), mi);

            // store for future reference
            InstanceCreateCache.Add(className, createInstanceDelegate);
        }

        return InstanceCreateCache[className].Invoke();

    }
}

// example of your ICalculate interface
public interface ICalculate
{
    void Initialize();
    int ExecuteCalculation();
}

// example of an ICalculate class
public class RandomNumber : ICalculate
{
    private static Random  _random;

    public static RandomNumber Create()
    {
        var random = new RandomNumber();
        random.Initialize();
        return random;
    }

    public void Initialize()
    {
        _random = new Random(DateTime.Now.Millisecond);
    }

    public int ExecuteCalculation()
    {
        return _random.Next();
    }
}
17 голосов
/ 10 марта 2011

Я предлагаю вам дать вашей фабричной реализации метод RegisterImplementation. Таким образом, каждый новый класс - это просто вызов этого метода, и вы не меняете свой код фабрики.

UPDATE:
Я имею в виду что-то вроде этого:

Создать интерфейс, который определяет расчет. Согласно вашему коду, вы уже сделали это. Ради завершенности я собираюсь использовать следующий интерфейс в оставшейся части моего ответа:

public interface ICalculation
{
    void Initialize(string originalData);
    void DoWork();
}

Ваш завод будет выглядеть примерно так:

public class CalculationFactory
{
    private readonly Dictionary<string, Func<string, ICalculation>> _calculations = 
                        new Dictionary<string, Func<string, ICalculation>>();

    public void RegisterCalculation<T>(string method)
        where T : ICalculation, new()
    {
        _calculations.Add(method, originalData =>
                                  {
                                      var calculation = new T();
                                      calculation.Initialize(originalData);
                                      return calculation;
                                  });
    }

    public ICalculation CreateInstance(string method, string originalData)
    {
        return _calculations[method](originalData);
    }
}

В этом простом фабричном классе отсутствует проверка ошибок по причине простоты.

ОБНОВЛЕНИЕ 2:
Вы могли бы инициализировать это где-нибудь в подпрограмме инициализации приложений:

CalculationFactory _factory = new CalculationFactory();

public void RegisterCalculations()
{
    _factory.RegisterCalculation<Pivot>("Pivot");
    _factory.RegisterCalculation<GroupBy>("GroupBy");
    _factory.RegisterCalculation<StandardDeviation>("Standard deviation");
    _factory.RegisterCalculation<PhosphoPRASPercentage>("% phospho PRAS Protein");
    _factory.RegisterCalculation<AveragePPPperTreatment>("AveragePPPperTreatment");
    _factory.RegisterCalculation<AvgPPPNControl>("AvgPPPNControl");
    _factory.RegisterCalculation<PercentageInhibition>("PercentageInhibition");
}
7 голосов
/ 10 марта 2011

Как пример, как добавить инициализацию в конструкторе:

Что-то похожее на:

Activator.CreateInstance(Type.GetType("ConsoleApplication1.Operation1"), initializationData);
, но записано с выражением Linq, часть кода взята здесь :
public class Operation1 
{
    public Operation1(object data)
    { 
    }
}

public class Operation2 
{
    public Operation2(object data)
    {
    }
}

public class ActivatorsStorage
{
    public delegate object ObjectActivator(params object[] args);

    private readonly Dictionary<string, ObjectActivator> activators = new Dictionary<string,ObjectActivator>();

    private ObjectActivator CreateActivator(ConstructorInfo ctor)
    {
        Type type = ctor.DeclaringType;

        ParameterInfo[] paramsInfo = ctor.GetParameters();
        ParameterExpression param = Expression.Parameter(typeof(object[]), "args");

        Expression[] argsExp = new Expression[paramsInfo.Length];

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

            Expression paramAccessorExp = Expression.ArrayIndex(param, index);

            Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType);

            argsExp[i] = paramCastExp;
        }

        NewExpression newExp = Expression.New(ctor, argsExp);

        LambdaExpression lambda = Expression.Lambda(typeof(ObjectActivator), newExp, param);

        return (ObjectActivator)lambda.Compile();
    }

    private ObjectActivator CreateActivator(string className)
    {
        Type type = Type.GetType(className);
        if (type == null)
            throw new ArgumentException("Incorrect class name", "className");

        // Get contructor with one parameter
        ConstructorInfo ctor = type.GetConstructors()
            .SingleOrDefault(w => w.GetParameters().Length == 1 
                && w.GetParameters()[0].ParameterType == typeof(object));

        if (ctor == null)
                throw new Exception("There is no any constructor with 1 object parameter.");

        return CreateActivator(ctor);
    }

    public ObjectActivator GetActivator(string className)
    {
        ObjectActivator activator;

        if (activators.TryGetValue(className, out activator))
        {
            return activator;
        }
        activator = CreateActivator(className);
        activators[className] = activator;
        return activator;
    }
}

Используется следующее:

ActivatorsStorage ast = new ActivatorsStorage();
var a = ast.GetActivator("ConsoleApplication1.Operation1")(initializationData);
var b = ast.GetActivator("ConsoleApplication1.Operation2")(initializationData);

То же самое можно реализовать с помощью DynamicMethods.

Кроме того, классы не обязаны наследоваться от того же интерфейса или базыкласс.

Спасибо, Виталий

6 голосов
/ 10 марта 2011

Одна из стратегий, которую я использую в подобных случаях, - помечать мои различные реализации специальным атрибутом для указания его ключа и сканировать активные сборки на наличие типов с этим ключом:

[AttributeUsage(AttributeTargets.Class)]
public class OperationAttribute : System.Attribute
{ 
    public OperationAttribute(string opKey)
    {
        _opKey = opKey;
    }

    private string _opKey;
    public string OpKey {get {return _opKey;}}
}

[Operation("Standard deviation")]
public class StandardDeviation : IOperation
{
    public void Initialize(object originalData)
    {
        //...
    }
}

public interface IOperation
{
    void Initialize(object originalData);
}

public class OperationFactory
{
    static OperationFactory()
    {
        _opTypesByKey = 
            (from a in AppDomain.CurrentDomain.GetAssemblies()
             from t in a.GetTypes()
             let att = t.GetCustomAttributes(typeof(OperationAttribute), false).FirstOrDefault()
             where att != null
             select new { ((OperationAttribute)att).OpKey, t})
             .ToDictionary(e => e.OpKey, e => e.t);
    }
    private static IDictionary<string, Type> _opTypesByKey;
    public IOperation GetOperation(string opKey, object originalData)
    {
        var op = (IOperation)Activator.CreateInstance(_opTypesByKey[opKey]);
        op.Initialize(originalData);
        return op;
    }
}

Таким образом, просто создав новый класс с новой ключевой строкой, вы можете автоматически «подключиться» к фабрике, без необходимости вообще изменять код фабрики.

Вы также заметите, что вместо того, чтобы полагаться на каждую реализацию для обеспечения конкретного конструктора, я создал метод Initialize в интерфейсе, который, как я ожидаю, реализуют классы. Пока они реализуют интерфейс, я смогу отправлять им «оригинальные данные» без каких-либо странностей отражения.

Я бы также предложил использовать среду внедрения зависимостей, такую ​​как Ninject, вместо использования Activator.CreateInstance. Таким образом, ваши реализации операций могут использовать инжектор конструктора для их различных зависимостей.

1 голос
/ 10 марта 2011
  1. Здесь нет проверки ошибок. Вы абсолютно уверены, что _class разрешит допустимый класс? Вы контролируете все возможные значения или эта строка каким-то образом заполняется конечным пользователем?
  2. Отражение обычно обходится дороже, чем его избегать. Проблемы с производительностью пропорциональны количеству объектов, которые вы планируете создать таким образом.
  3. Перед тем как убежать и использовать инфраструктуру внедрения зависимостей, прочтите ее критику. =)
1 голос
/ 10 марта 2011

По сути, это звучит так, как будто вам нужен заводской шаблон.В этой ситуации вы определяете сопоставление ввода с типами вывода, а затем создаете экземпляр типа во время выполнения, как вы делаете.

Пример:

У вас есть X классов, и все они имеют общий интерфейс IDoSomething.

public interface IDoSomething
{
     void DoSomething();
}

public class Foo : IDoSomething
{
    public void DoSomething()
    {
         // Does Something specific to Foo
    }
}

public class Bar : IDoSomething
{
    public void DoSomething()
    {
        // Does something specific to Bar
    }
}

public class MyClassFactory
{
     private static Dictionary<string, Type> _mapping = new Dictionary<string, Type>();

     static MyClassFactory()
     {
          _mapping.Add("Foo", typeof(Foo));
          _mapping.Add("Bar", typeof(Bar));
     }

     public static void AddMapping(string query, Type concreteType)
     {
          // Omitting key checking code, etc. Basically, you can register new types at runtime as well.
          _mapping.Add(query, concreteType);
     }

     public IDoSomething GetMySomething(string desiredThing)
     {
          if(!_mapping.ContainsKey(desiredThing))
              throw new ApplicationException("No mapping is defined for: " + desiredThing);

          return Activator.CreateInstance(_mapping[desiredThing]) as IDoSomething;
     }
}
...