Search code examples
c#binaryfilesdowncast

Create object instance of correct subtype from raw bytes


I have a binary file format I cannot change, and in C# I need to deserialize class instances from it again.

The file contains several byte blocks which represent object instances of a base class. However, one byte in the block specifies which subclass the object once was.

For example, a byte block looks like the following:

  • General settings which the base class has (let's say Collectible)
  • A byte determining what kind of additional details follow (either Coin or ItemBox)
  • According to the byte, either details about the Coin (e.g. the coins value) or the ItemBox (e.g. which item it has and how fast it regenerates)

I currently coded a constructor for the Collectible class accepting a BinaryReader to read from. However, after I read the general settings and know which sub type follows, I can't downcast the Collectible class to Coin or ItemBox depending on the type when I'm already in the constructor.

public Collectible(BinaryReader reader)
{
    // General collectible stuff, position, synced anim...
    X = reader.ReadInt32();
    Y = reader.ReadInt32();
    MusicStartCode = reader.ReadInt32();
    MusicBpm = reader.ReadInt32();
    // ...

    // Tricky part: sub-type specific information follows
    CollectibleType type = (CollectibleType)reader.ReadByte();
    switch (type)
    {
        case CollectibleType.Coin:
            // What to do now?
            break;
        case CollectibleType.ItemBox:
            // I ran out of ideas
            break;
    }
}

How do I solve such a problem elegantly and deserialize the binary format back into object instances? I thought of some kind of seeking towards the byte determining the sub type and then create the specific sub class instances. But where would I do that, is there a typical design pattern for this problem?

On a sidenote I think that this format was written by a C++ program originally which just dumped the instances bytes into the file and had no problem casting a byte array pointer back to a class when loading it. But I can't do that in C# or can I (I want to avoid unsafe code)?


Solution

  • How about something along the lines of this:

    public class CollectionFactory
    {
        private readonly BinaryReader _reader;
        public CollectionFactory(BinaryReader reader)
        {
            if (reader == null)
            {
                throw ArgumentNullException(reader);
            }
    
            _reader = reader;
        }
    
        public CollectionType CreateInstance()
        {
            using (_reader)
            {
                // General collectible stuff, position, synced anim...
                var x = _reader.ReadInt32();
                var y = _reader.ReadInt32();
                var musicStartCode = reader.ReadInt32();
                var musicBpm = reader.ReadInt32();
                // ...
    
                // Tricky part: sub-type specific information follows
                CollectibleType collectableType = (CollectibleType)reader.ReadByte();
    
                if (collectableType is Coin)
                {
                    return new Coin 
                    {
                        X = x,      
                        Y = y,
                        MusicStartCode = musicStartCode
                        // etc..
                    };
                }
                if (collectableType is ItemBox)
                {
                    return new ItemBox
                    {
                        X = x,      
                        Y = y,
                        MusicStartCode = musicStartCode
                        // etc..
                    };
                }
    
                return null;
            }
        }
    }
    

    And then consume it:

    var collectionFactory = new CollectionFactory(reader);
    var collType = collectionFactory.CreateInstance();
    
    // cast to specific type if needed here.
    var itemBox = collType as ItemBox;
    if (itembox != null)
    {
       // Do stuff..
    }