Динамическое приведение неизвестных типов для сериализации - PullRequest
6 голосов
/ 15 ноября 2011

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

if      (fi.FieldType.Name == "Int16")   bw.Write((Int16)fi.GetValue(obj));
else if (fi.FieldType.Name == "UInt16")  bw.Write((UInt16)fi.GetValue(obj));
else if (fi.FieldType.Name == "Int32")   bw.Write((Int32)fi.GetValue(obj));
else if (fi.FieldType.Name == "UInt32")  bw.Write((UInt32)fi.GetValue(obj));
else if (fi.FieldType.Name == "Int64")   bw.Write((Int64)fi.GetValue(obj));
else if (fi.FieldType.Name == "UInt64")  bw.Write((UInt64)fi.GetValue(obj));
else if (fi.FieldType.Name == "Single")  bw.Write((float)fi.GetValue(obj));
else if (fi.FieldType.Name == "Double")  bw.Write((double)fi.GetValue(obj));
else if (fi.FieldType.Name == "Decimal") bw.Write((decimal)fi.GetValue(obj));
else if (fi.FieldType.Name == "Byte")    bw.Write((byte)fi.GetValue(obj));
else if (fi.FieldType.Name == "SByte")   bw.Write((sbyte)fi.GetValue(obj));
else if (fi.FieldType.Name == "String")  bw.Write((string)fi.GetValue(obj));

Очевидно, что это некрасиво, и становится еще страшнее, когда я хочу сделать то же самое с массивами этих типов.

Что было бы здорово, если бы я мог сделать что-то вроде этого:

bw.Write( (fi.FieldType) fi.GetValue(obj) );

Затем выполните аналогичные действия для массивов.

Есть идеи?

Ответы [ 6 ]

4 голосов
/ 15 ноября 2011

Вы можете использовать отражение, чтобы вызвать правильную версию Write

public static void WriteField(BinaryWriter bw, object obj, FieldInfo fieldInfo)
{
    typeof(BinaryWriter)
        .GetMethod("Write", new Type[] { fieldInfo.FieldType })
        .Invoke(bw, new object[] { fieldInfo.GetValue(obj) });
}
2 голосов
/ 15 ноября 2011

Я делаю очень похожий код для protobuf-net; Type.GetTypeCode(...) - благо, позволяющее switch:

switch(Type.GetTypeCode(fi.FieldType)) {
    case TypeCode.Int16: bw.Write((Int16)fi.GetValue(obj)); break
    case TypeCode.UInt32: bw.Write((UInt16)fi.GetValue(obj)); break;
        ... etc lots and lots
}

все еще немного повторяется, но вы смотрите на Type только один раз - остальное - switch.

Если вы используете 4.0, другой трюк может быть:

dynamic value = fi.GetValue(obj);
bw.Write(value);

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

Последняя мысль: использование метапрограммирования (например, ILGenerator) для создания кода во время выполнения - более сложное, но более быстрое и не имеющее ни одной из этих проверок при выполнении время (только при подготовке модели).

2 голосов
/ 15 ноября 2011

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

//Cache the generated method for re-use later, say as a static field of dictionary. It shouldn't grow too-big given the number of overloads of Write.
private static Dictionary<Type, Action<BinaryWriter, object>> _lambdaCache = new Dictionary<Type, Action<BinaryWriter, object>>();

//...

if (!_lambdaCache.ContainsKey(fi.FieldType))
{
    var binaryWriterParameter = Expression.Parameter(typeof(BinaryWriter));
    var valueParameter = Expression.Parameter(typeof(object));
    var call = Expression.Call(binaryWriterParameter, "Write", null, Expression.Convert(valueParameter, fi.FieldType));
    var lambda = Expression.Lambda<Action<BinaryWriter, object>>(call, binaryWriterParameter, valueParameter).Compile();
    _lambdaCache.Add(fi.FieldType, lambda);
}
var write = _lambdaCache[fi.FieldType];
write(bw, fi.GetValue(obj));

То, что мы здесь делаем, - это динамическая генерация кода для вызова того, что вам нужно для бинарной записи. Это звучит сложнее, чем на самом деле, но мы создаем выражение для метода «Write» BinaryWriter. Мы также динамически приводим его с использованием Expression.Convert, поэтому вызывается правильная перегрузка Write. Мы берем два параметра BinaryWriter и значение для записи. Наконец, мы компилируем лямбду и кешируем ее для этого типа для последующего использования.

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

2 голосов
/ 15 ноября 2011

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

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

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

Иногда нам нравится брать слишком простые задачи и делать их более сложными.Но часто хороший бизнес-код - это просто обыденный, скучный код.

1 голос
/ 15 ноября 2011

Я могу придумать три варианта:

1) BinaryFormatter - это может быть в состоянии выполнить вашу задачу очень просто, методом Serialize.
2) Как вы предлагаете, используяотражение.Код будет выглядеть примерно так:

// sample source data
object src = (uint)234;

var bwType = typeof(BinaryWriter);
var argTypes = new Type[] { src.GetType() };
var m = bwType.GetMethod("Write", argTypes);
var args = new object[] { src };
m.Invoke(bw, args);

3) Используйте шаблон T4 для быстрой генерации кода.Код все еще уродлив, но, по крайней мере, требует гораздо меньше усилий для его обслуживания.Я часто использую этот шаблон в некоторых своих проектах, потому что он лучший в обоих мирах - без потери производительности из-за размышлений, но со всеми преимуществами динамически генерируемого кода.

0 голосов
/ 15 ноября 2011

Даже если вы ничего не делаете, switch работает со строками, и это значительно облегчит чтение.

Учитывая, что работает явное приведение:

Type t = Type.GetType(String.Concat("System.", fi.FieldType.Name));

Тогда используйте

MethodInfo m = typeof(BinaryWriter).GetMethod("Write", new type[] { t });

Если не ноль

m.Invoke(bw, new object[] { fi.GetValue(obj) });

Предполагается, что FieldType.Name соответствует типу, который находится в области видимости. Не сказал, что было бы там для массива, но если это Int16[], это всего лишь небольшая часть покерных проблем, и может быть подкласс BinaryWriter и добавление еще нескольких перегрузок для типов, которые не указаны в коробке иметь дело с. Если вы делаете это много, какой-то кэш Name, Type и MethodInfo, вероятно, будет полезен.

...