Polynomial
Polynomial

Reputation: 28316

Dynamic casting of unknown types for serialization

I'm writing a custom serializer for struct types for interop with a protocol I can't alter. I'm using reflection to pull out structure member values and write them to a BinaryWriter. It's only designed to support basic types and arrays of them.

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));

Obviously this is ugly, and it gets even more ugly when I want to do the same thing with arrays of these types too.

What would be really nice is if I could do something like this:

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

Then do a similar kind of thing for arrays.

Any ideas?

Upvotes: 6

Views: 908

Answers (6)

Tony Hopkinson
Tony Hopkinson

Reputation: 20320

Even if you don't do anything else, switch works with strings, and it would make what you have a lot easier to read.

Given the explicit cast is working:

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

Then use

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

If it's not null

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

This is assuming FieldType.Name corresponds to a type that's in scope. Didn't say what would be in there for an array, but if it's Int16[], it's just a bit of jiggery pokery, and may be subclassing BinaryWriter and adding some more overloads for types the one in the box doesn't deal with. If you are doing a lot of this, some sort of cache Name, Type and MethodInfo, would probably be useful.

Upvotes: 0

vcsjones
vcsjones

Reputation: 141588

If you want to simplify it, you could use an expression to dynamically make the right call.

//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));

What we are doing here is dynamically generating the code to make the call that you need to the binary writer. This sounds more complicated than it is, but what we are doing is creating an expression to the "Write" method of a BinaryWriter. We also dynamically cast it using Expression.Convert so the correct overload of Write is called. We take in two parameters of the BinaryWriter and value to write. Finally, we compile the lambda and cache it for that Type for re-use later.

Depending on your needs, this will be a lot faster than using reflection over BinaryWriter.

Upvotes: 2

mellamokb
mellamokb

Reputation: 56769

I can think of three options:

1) BinaryFormatter - this might be able to accomplish your task very simply, by the Serialize method.
2) As you suggest, using reflection. Code would look something like this:

// 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) Use a T4 template to generate code quickly. The code is still ugly, but at least takes a lot less work to maintain. I use this pattern often in some of my projects because it's the best of both worlds - no performance penalty from reflection, but all the benefits of dynamically-generated code.

Upvotes: 1

Jacob
Jacob

Reputation: 78850

You can use reflection to invoke the right version of 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) });
}

Upvotes: 4

Marc Gravell
Marc Gravell

Reputation: 1062502

I do some very similar code for protobuf-net; Type.GetTypeCode(...) is a boon, allowing a 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
}

still a bit repetitive, but you only look at the Type once - the rest is a switch.

If you are using 4.0, another trick might be:

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

which will try to pick the most appropriate overload at runtime. However, in my view, this is not reason-enough to use dynamic here.

A final thought would be: use meta-programming (such as ILGenerator) to create the code at runtime - more complex, but faster, and doesn't have any of these checks at execution time (just when preparing the model).

Upvotes: 2

Brandon Moore
Brandon Moore

Reputation: 8780

This code isn't really ugly at all... it's just repetitive. But it's actually pretty clean, short and very easy to understand. If you had a million different types to account for that would be one thing, but there are only a limited number.

If you are able to do what you're wanting to do, it will be hard to maintain if there's ever a problem with it or it needs to do something more and another programmer may not understand it... or you may have forgotten what the heck you did and have to relearn it.

By doing this you will have: -added additional development time -reduced readability -reduced speed -increased maintenance

Sometimes we like to take problems that are too simple and make them more challenging. But often good business code is just mundane, boring code.

Upvotes: 2

Related Questions