Генерировать динамический метод для установки поля структуры вместо использования отражения
Скажем, у меня есть следующий код, который обновляет поле struct
с использованием отражения. Поскольку экземпляр struct скопирован в метод DynamicUpdate
, он должен быть помещен в бокс для объекта перед передачей.
struct Person
{
public int id;
}
class Test
{
static void Main()
{
object person = RuntimeHelpers.GetObjectValue(new Person());
DynamicUpdate(person);
Console.WriteLine(((Person)person).id); // print 10
}
private static void DynamicUpdate(object o)
{
FieldInfo field = typeof(Person).GetField("id");
field.SetValue(o, 10);
}
}
Код работает нормально. Теперь, скажем, я не хочу использовать рефлексию, потому что она медленная. Вместо этого я хочу сгенерировать некоторый CIL, напрямую изменяющий поле id
и преобразовывая этот CIL в многократно используемый делегат (скажем, используя функцию Dynamic Method). В частности, я хочу заменить указанный выше код на s/t следующим образом:
static void Main()
{
var action = CreateSetIdDelegate(typeof(Person));
object person = RuntimeHelpers.GetObjectValue(new Person());
action(person, 10);
Console.WriteLine(((Person)person).id); // print 10
}
private static Action<object, object> CreateSetIdDelegate(Type t)
{
// build dynamic method and return delegate
}
Мой вопрос: есть ли способ реализовать исключения CreateSetIdDelegate
из одного из следующих методов?
- Сгенерировать CIL, который вызывает сеттер с использованием отражения (как 1-й сегмент кода в этом сообщении). Это не имеет смысла, поскольку требование состоит в том, чтобы избавиться от рефлексии, но это возможная реализация, поэтому я просто упоминаю.
- Вместо использования
Action<object, object>
используйте пользовательский делегат, подпись которого public delegate void Setter(ref object target, object value)
.
- Вместо
Action<object, object>
используйте Action<object[], object>
, когда 1-й элемент массива является целевым объектом.
Причина, по которой мне не нравятся 2 и 3, состоит в том, что я не хочу иметь разных делегатов для установщика объекта и сеттера структуры (а также не хочу, чтобы делегировать делегат объекта set-object чем необходимо, например, Action<object, object>
). Я полагаю, что реализация CreateSetIdDelegate
создавала бы другой CIL в зависимости от того, является ли целевой тип структурой или объектом, но я хочу, чтобы он возвращал тому же делегату, предлагающему пользователю тот же API.
Ответы
Ответ 1
РЕДАКТИРОВАТЬ снова. Теперь это создает структуру.
Там великолепный способ сделать это на С# 4, но вам придется написать свой собственный код ILGenerator
emit для чего-либо до этого. Они добавили ExpressionType.Assign
в .NET Framework 4.
Это работает в С# 4 (проверено):
public delegate void ByRefStructAction(ref SomeType instance, object value);
private static ByRefStructAction BuildSetter(FieldInfo field)
{
ParameterExpression instance = Expression.Parameter(typeof(SomeType).MakeByRefType(), "instance");
ParameterExpression value = Expression.Parameter(typeof(object), "value");
Expression<ByRefStructAction> expr =
Expression.Lambda<ByRefStructAction>(
Expression.Assign(
Expression.Field(instance, field),
Expression.Convert(value, field.FieldType)),
instance,
value);
return expr.Compile();
}
Изменить: Вот мой тестовый код.
public struct SomeType
{
public int member;
}
[TestMethod]
public void TestIL()
{
FieldInfo field = typeof(SomeType).GetField("member");
var setter = BuildSetter(field);
SomeType instance = new SomeType();
int value = 12;
setter(ref instance, value);
Assert.AreEqual(value, instance.member);
}
Ответ 2
Я столкнулся с подобной проблемой, и мне потребовалось большую часть выходных, но я, наконец, понял это после многого поиска, чтения и демонстрации проектов тестирования С#. И для этой версии требуется только .NET 2, а не 4.
public delegate void SetterDelegate(ref object target, object value);
private static Type[] ParamTypes = new Type[]
{
typeof(object).MakeByRefType(), typeof(object)
};
private static SetterDelegate CreateSetMethod(MemberInfo memberInfo)
{
Type ParamType;
if (memberInfo is PropertyInfo)
ParamType = ((PropertyInfo)memberInfo).PropertyType;
else if (memberInfo is FieldInfo)
ParamType = ((FieldInfo)memberInfo).FieldType;
else
throw new Exception("Can only create set methods for properties and fields.");
DynamicMethod setter = new DynamicMethod(
"",
typeof(void),
ParamTypes,
memberInfo.ReflectedType.Module,
true);
ILGenerator generator = setter.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldind_Ref);
if (memberInfo.DeclaringType.IsValueType)
{
#if UNSAFE_IL
generator.Emit(OpCodes.Unbox, memberInfo.DeclaringType);
#else
generator.DeclareLocal(memberInfo.DeclaringType.MakeByRefType());
generator.Emit(OpCodes.Unbox, memberInfo.DeclaringType);
generator.Emit(OpCodes.Stloc_0);
generator.Emit(OpCodes.Ldloc_0);
#endif // UNSAFE_IL
}
generator.Emit(OpCodes.Ldarg_1);
if (ParamType.IsValueType)
generator.Emit(OpCodes.Unbox_Any, ParamType);
if (memberInfo is PropertyInfo)
generator.Emit(OpCodes.Callvirt, ((PropertyInfo)memberInfo).GetSetMethod());
else if (memberInfo is FieldInfo)
generator.Emit(OpCodes.Stfld, (FieldInfo)memberInfo);
if (memberInfo.DeclaringType.IsValueType)
{
#if !UNSAFE_IL
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldloc_0);
generator.Emit(OpCodes.Ldobj, memberInfo.DeclaringType);
generator.Emit(OpCodes.Box, memberInfo.DeclaringType);
generator.Emit(OpCodes.Stind_Ref);
#endif // UNSAFE_IL
}
generator.Emit(OpCodes.Ret);
return (SetterDelegate)setter.CreateDelegate(typeof(SetterDelegate));
}
Обратите внимание на материал #if UNSAFE_IL. Я на самом деле придумал два способа сделать это, но первый из них действительно... хакерский. Чтобы привести цитату из Ecma-335, документ стандартов для IL:
"В отличие от поля, которое требуется для создания копии типа значения для использования в объекте, unbox не требуется для копирования типа значения из объекта. Обычно он просто вычисляет адрес типа значения, который уже присутствовать внутри объекта в штучной упаковке."
Итак, если вы хотите играть опасно, вы можете использовать OpCodes.Unbox для изменения дескриптора объекта в указатель на вашу структуру, который затем можно использовать в качестве первого параметра Stfld или Callvirt. Выполнение этого способа фактически завершает модификацию структуры на месте, и вам даже не нужно передавать целевой объект по ссылке.
Однако обратите внимание, что стандарт не гарантирует, что Unbox даст вам указатель на коробку. В частности, это говорит о том, что Nullable < > может вызвать Unbox для создания копии. В любом случае, если это произойдет, вы, вероятно, получите молчаливый сбой, когда он устанавливает значение поля или свойства в локальной копии, которая затем сразу же отбрасывается.
Таким образом, безопасный способ сделать это - передать свой объект по ref, сохранить адрес в локальной переменной, внести изменения, а затем снова добавить результат и поместить его обратно в свой параметр объекта ByRef.
Я сделал некоторые грубые тайминги, назвав каждую версию 10 000 000 раз, с двумя разными структурами:
Структура с 1 полем:
.46 s "Небезопасный" делегат
.70 s "Безопасный" делегат
4.5 s FieldInfo.SetValue
Структура с 4 полями:
.46 s "Небезопасный" делегат
.88 s "Безопасный" делегат.
4.5 s FieldInfo.SetValue
Обратите внимание, что бокс делает скорость "безопасной" версии уменьшенной с размером структуры, тогда как другие два метода не зависят от размера структуры. Думаю, в какой-то момент стоимость бокса будет превышать стоимость отражения. Но я не буду доверять "Небезопасной" версии в любой важной емкости.
Ответ 3
После некоторых экспериментов:
public delegate void ClassFieldSetter<in T, in TValue>(T target, TValue value) where T : class;
public delegate void StructFieldSetter<T, in TValue>(ref T target, TValue value) where T : struct;
public static class FieldSetterCreator
{
public static ClassFieldSetter<T, TValue> CreateClassFieldSetter<T, TValue>(FieldInfo field)
where T : class
{
return CreateSetter<T, TValue, ClassFieldSetter<T, TValue>>(field);
}
public static StructFieldSetter<T, TValue> CreateStructFieldSetter<T, TValue>(FieldInfo field)
where T : struct
{
return CreateSetter<T, TValue, StructFieldSetter<T, TValue>>(field);
}
private static TDelegate CreateSetter<T, TValue, TDelegate>(FieldInfo field)
{
return (TDelegate)(object)CreateSetter(field, typeof(T), typeof(TValue), typeof(TDelegate));
}
private static Delegate CreateSetter(FieldInfo field, Type instanceType, Type valueType, Type delegateType)
{
if (!field.DeclaringType.IsAssignableFrom(instanceType))
throw new ArgumentException("The field is declared it different type");
if (!field.FieldType.IsAssignableFrom(valueType))
throw new ArgumentException("The field type is not assignable from the value");
var paramType = instanceType.IsValueType ? instanceType.MakeByRefType() : instanceType;
var setter = new DynamicMethod("", typeof(void),
new[] { paramType, valueType },
field.DeclaringType.Module, true);
var generator = setter.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Stfld, field);
generator.Emit(OpCodes.Ret);
return setter.CreateDelegate(delegateType);
}
}
Основное отличие от дерева дерева выражений состоит в том, что поля readonly также могут быть изменены.
Ответ 4
Этот код работает для структур без использования ссылки:
private Action<object, object> CreateSetter(FieldInfo field)
{
var instance = Expression.Parameter(typeof(object));
var value = Expression.Parameter(typeof(object));
var body =
Expression.Block(typeof(void),
Expression.Assign(
Expression.Field(
Expression.Unbox(instance, field.DeclaringType),
field),
Expression.Convert(value, field.FieldType)));
return (Action<object, object>)Expression.Lambda(body, instance, value).Compile();
}
Вот мой тестовый код:
public struct MockStruct
{
public int[] Values;
}
[TestMethod]
public void MyTestMethod()
{
var field = typeof(MockStruct).GetField(nameof(MockStruct.Values));
var setter = CreateSetter(field);
object mock = new MockStruct(); //note the boxing here.
setter(mock, new[] { 1, 2, 3 });
var result = ((MockStruct)mock).Values;
Assert.IsNotNull(result);
Assert.IsTrue(new[] { 1, 2, 3 }.SequenceEqual(result));
}
Ответ 5
Возможно, вам захочется взглянуть на динамические методы (отражение не должно быть медленным!)...
У Герхарда хорошая статья об этом: http://jachman.wordpress.com/2006/08/22/2000-faster-using-dynamic-method-calls/
Ответ 6
Вы можете довольно легко изменить это, чтобы работать с structs. В настоящее время он основан на словах, но ваша ситуация проще.
http://www.damonpayne.com/2009/09/07/TwoWayBindingToNameValuePairs.aspx