Monday, May 07, 2007

Fun With Reflection... Reference vs Value Types

So here is an interesting little quirk I found regarding Reflection in .NET.  I was writing a serialization library that was capable of reading and writing to a CSV file format, and also to a fixed width file format.  The project I was working on had various CSV and Fixed Width formats to deal with, so we wanted a nice and generic library to read and write with.  Moreover, the code I was replacing basically just parsed everything into a string, and then there would be tons of logic that simply indexed into a string using a constant to represent the position in the row.  We wanted each record in the file to be read into a strongly typed structure.

I decided to make use of Reflection so that you could create a data structure that looked like this:

[TextSerializable]
public class Person
{
    [TextField(0)]
    public string Name;

    [TextField(1)]
    public int Age;

    [TextField(2)]
    public DateTime DateOfBirth;
  }

And then easily read it in by doing this:

TextReader reader = new StreamReader( "TestFile.csv" );
CSVSerializer<Person> ser = new CSVSerializer<Person>();
Person p = ser.Deserialize( reader.ReadLine() );

Sounds pretty easy right?  But what if you want your target data type to be a struct instead of a class?  Should be pretty easy right?  As it turns out, there is a little known quirk in how you use reflection to set Property and Field values that makes a big difference due to boxing.

The serialization class that I wrote uses reflection to find all the Fields and Properties that have been marked with the TextField attribute.  Then during the deserialization process, it uses the PropertyInfo.SetValue (or FieldInfo.SetValue) method to set the value on the newly created target type.

Here is the trick.  You have to know whether the target type if a class or a struct.  If it's a class, then you can pass in an object reference.  If it's a class, then you have to store the variable in a ValueType variable.  Otherwise the structure will be boxed, and during the boxing/unboxing process, the value will be lost!

It's weird... you call the exact same SetValue method... there is not even a special overload that takes a ValueType vs Object type.  However, it makes all the difference in the world.  Here is part of the code from the Deserialize method.  TargetType is the generic type that gets passed in during the declaration (in the above example it was Person).  I stored the Type variable in _type.

TargetType returnObj = new TargetType();
            
ValueType  returnStruct = null;
if ( _type.IsValueType )
{
    object tempObj = returnObj;
    returnStruct = (ValueType)tempObj;
}
// Parsing code here
if ( _type.IsValueType )
    AssignToStruct( returnStruct, fieldObj, attr.Member );
else
    AssignToClass( returnObj, fieldObj, attr.Member );

And here is AssignToClass and AssignToStruct:

private void AssignToClass( object obj, object val, MemberInfo member )
{
    if ( member is PropertyInfo )
        ( (PropertyInfo)member ).SetValue( obj, val, null );
    else if ( member is FieldInfo )
        ( (FieldInfo)member ).SetValue( obj, val );
    else
        throw new TextSerializationException( "Invalid MemberInfo type encountered" );
}

private void AssignToStruct( ValueType obj, object val, MemberInfo member )
{
    if ( member is PropertyInfo )
        ( (PropertyInfo)member ).SetValue( obj, val, null );
    else if ( member is FieldInfo )
        ( (FieldInfo)member ).SetValue( obj, val );
    else
        throw new TextSerializationException( "Invalid MemberInfo type encountered" );
}

Notice how they are identical, except for the type being passed in?  It's absolutely crazy making to have this copy and paste code, but it's necessary.  The other crazy making part is that FieldInfo and PropertyInfo don't have a common base which has SetValue in it.  For whatever reason, all languages in .NET treat Properties and Fields as identical syntactically, but they are completely different reflected.  More copy and paste madness.

#    9:20 PM by Nick | 1 Comment |