С# Использование Activator.CreateInstance
Вчера я задал вопрос относительно использования либо отражения, либо шаблона стратегии для динамически вызывающих методов.
Однако с тех пор я решил изменить методы на отдельные классы, реализующие общий интерфейс. Причина в том, что каждый класс, хотя и имеет некоторые сходства, также выполняет определенные методы, уникальные для этого класса.
Я использовал стратегию как таковую:
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 - это имя класса, переданного во время выполнения.
Это общий способ сделать это, будут ли какие-либо проблемы с производительностью?
Я новичок в размышлениях, поэтому ваш совет будет приветствоваться.
Ответы
Ответ 1
При использовании рефлексии сначала вы должны задать себе пару вопросов, потому что вы можете оказаться в сложном комплексном решении, которое трудно поддерживать:
- Есть ли способ решить проблему с использованием универсальности или наследования класса/интерфейса?
- Могу ли я решить проблему с помощью
dynamic
invocations (только .NET 4.0 и выше)?
- Важна ли производительность, т.е. будет ли мой отраженный метод или вызов-экземпляр вызываться один, два или два миллиона раз?
- Могу ли я объединить технологии, чтобы получить умное, но работоспособное/понятное решение?
- Я в порядке с потерей безопасности типа времени компиляции?
Общая/динамическая
Из вашего описания я предполагаю, что вы не знаете типы во время компиляции, вы знаете, что они используют интерфейс ICalculation
. Если это правильно, то число (1) и (2) выше, вероятно, не возможно в вашем сценарии.
Производительность
Это важный вопрос. Накладные расходы на использование отражения могут препятствовать более чем 400-кратным штрафам: это замедляет даже умеренное количество вызовов.
Разрешение относительно просто: вместо использования Activator.CreateInstance
используйте метод factory (у вас уже есть), найдите MethodInfo
, создайте делегат, кешируйте его и затем используйте делегат. Это дает только штраф за первый вызов, последующие вызовы имеют почти собственную производительность.
Комбинированные технологии
Здесь много, но мне действительно нужно знать больше о вашей ситуации, чтобы помочь в этом направлении. Часто я заканчиваю комбинирование dynamic
с generics, с кэшированным отражением. При использовании скрытия информации (как это обычно бывает в ООП) вы можете получить быстрое, стабильное и все еще хорошо расширяемое решение.
Потеря безопасности времени компиляции
Из пяти вопросов это, пожалуй, самый важный, о котором нужно беспокоиться. Очень важно создавать свои собственные исключения, которые дают четкую информацию о рефлексивных ошибках. Это означает, что каждый вызов метода, конструктора или свойства на основе входной строки или другой неконтролируемой информации должен быть завернут в 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();
}
}
Ответ 2
Я предлагаю вам реализовать ваш factory метод RegisterImplementation
. Таким образом, каждый новый класс является просто вызовом этого метода, и вы не меняете код своей фабрики.
UPDATE:
Я имею в виду что-то вроде этого:
Создайте интерфейс, определяющий расчет. Согласно вашему коду, вы уже это сделали. Чтобы закончить, я останусь в следующем ответе:
public interface ICalculation
{
void Initialize(string originalData);
void DoWork();
}
Ваш factory будет выглядеть примерно так:
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);
}
}
В этом простом классе factory отсутствует проверка ошибок по причине простоты.
ОБНОВЛЕНИЕ 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");
}
Ответ 3
Одна из стратегий, которые я использую в таких случаях, - это указать мои различные реализации специальным атрибутом для указания его ключа и отсканировать активные сборки для типов с этим ключом:
[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;
}
}
Таким образом, просто создав новый класс с новой ключевой строкой, вы можете автоматически "подключиться" к factory, не изменяя код factory вообще.
Вы также заметите, что вместо того, чтобы в зависимости от каждой реализации предоставить конкретный конструктор, я создал метод Initialize на интерфейсе, который, как я ожидаю, реализует классы. Пока они реализуют интерфейс, я смогу отправить им "originalData" без какой-либо отразительной странности.
Я также предлагаю использовать инфраструктуру инъекций зависимостей, такую как Ninject, вместо использования Activator.CreateInstance. Таким образом, реализация вашей операции может использовать инъекцию конструктора для различных зависимостей.
Ответ 4
Как пример добавления инициализации в конструкторе:
Нечто похожее на:
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.
Кроме того, классы не должны наследоваться от одного и того же интерфейса или базового класса.
Спасибо, Виталий
Ответ 5
По существу, похоже, что вы хотите шаблон factory. В этой ситуации вы определяете отображение входных данных для типов вывода, а затем создаете экземпляр типа во время выполнения, как вы делаете.
Пример:
У вас есть 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;
}
}
Ответ 6
- Здесь нет ошибок. Вы абсолютно уверены, что _class решит действительный класс? Вы контролируете все возможные значения или эта строка каким-то образом заполняется конечным пользователем?
- Отражение обычно является наиболее дорогостоящим, чем его устранение. Проблемы с производительностью пропорциональны количеству объектов, которые вы планируете создать таким образом.
- Прежде чем убежать и использовать инфраструктуру инъекций зависимостей, прочитайте ее критику. =)