Search code examples
c#serializationstaticsingletonbinary-serialization

BinarySerialization, Static and Singleton


After having my class go through binary serialization any references to static instance of another class break. The example should explain better what i mean:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace staticorsingletontest
{
    [System.Serializable]
    public class weapon 
    {
        public string name;
        public weapon (string name)
            { this.name = name; }
    }
    class Program
    {
        public static weapon sword =  new weapon("Sword");
        public static weapon axe = new weapon("Axe");
        static void Main(string[] args)
        {

            byte[] b;
            Dictionary<weapon, int> WarriorSkills = new Dictionary<weapon,int>();
            Dictionary<weapon, int> Des = new Dictionary<weapon,int>();

            WarriorSkills.Add(sword, 10);
            using (MemoryStream ms = new MemoryStream())
            {
                //Serialize
                new BinaryFormatter().Serialize(ms, WarriorSkills);
                b = ms.ToArray();
                //Deserialize
                ms.Flush();
                ms.Write(b, 0, b.Length);
                ms.Seek(0, SeekOrigin.Begin);

                Des = (Dictionary<weapon, int>)new BinaryFormatter().Deserialize(ms);
            }

            Console.WriteLine(WarriorSkills.Keys.ToArray()[0].name + " is a " + Des.Keys.ToArray()[0].name + ", but are they equal? " + (WarriorSkills.Keys.ToArray()[0] == Des.Keys.ToArray()[0]).ToString());

            Console.ReadLine();

            Console.WriteLine("Warrior's Skill with Sword is ", Des[sword]); //wonderful "KeyNotFoundException" error

            Console.ReadLine();
        }
    }    
}

Program throws an error because deserialized "sword" is not the same "sword" (its static, how that even happens?)

Making weapon class a singleton would not work because then sword and axe will be the same thing.

Is there a way to point out that both swords are the same thing, or i dont get some core logic of static classes?


Solution

  • If you deserialize an (originally singleton) object it will be a new instance anyway unless you specify that deserialization should return a "well known" instance. But you can do it by some customization:

    using System;
    using System.IO;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Formatters.Binary;
    
    public class Example
    {
        [Serializable]
        public class Weapon: IObjectReference // here is the trick, see GetRealObject method
        {
            // unless you want to allow to create any kind of weapon I suggest to use an enum for the predefined types
            private enum WeaponKind { Sword, Axe }
    
            public static Weapon Sword { get; } = new Weapon(WeaponKind.Sword);
            public static Weapon Axe { get; } = new Weapon(WeaponKind.Axe);
    
            // this is the only instance field so this will be stored on serialization
            private readonly WeaponKind kind;
    
            public string Name => kind.ToString();
    
            // make the constructor private so no one can create further weapons
            private Weapon(WeaponKind kind)
            {
                this.kind = kind;
            }
    
            // on deserialization ALWAYS a new instance will be created
            // but if you implement IObjectReference, this method will be called before returning the deserialized object
            public object GetRealObject(StreamingContext context)
            {
                // map the temporarily created new deserialized instance to the well-known static member:
                switch (kind)
                {
                    case WeaponKind.Sword:
                        return Sword;
                    case WeaponKind.Axe:
                        return Axe;
                    default:
                        throw new InvalidOperationException("Unknown weapon type");
                }
            }
        }
    }
    

    And some test:

    public static void Main()
    {
        var axe = Weapon.Axe;
        var savedContent = new MemoryStream();
        var formatter = new BinaryFormatter();
        formatter.Serialize(savedContent, axe);
        savedContent.Position = 0;
        var deserializedAxe = (Weapon)formatter.Deserialize(savedContent);
        Console.WriteLine(ReferenceEquals(axe, deserializedAxe)); // prints True
    }
    

    Update:

    If all of your weapon properties are constant (and it is not a problem if there are more instances that should be considered to be equal), then just override Equals:

    public override bool Equals(object obj)
    {
        var other = obj as Weapon;
        if (other == null)
            return base.Equals(obj);
        return other.kind == this.kind;
    }
    

    If you override Equals you must override GetHashCode as well, otherwise, you will not able to find the different instances of the same object in the dictionary:

    public override int GetHashCode()
    {
        return kind.GetHashCode();
    }
    

    Please note that == operator will still return reference equality. If you want to override this you need to overload the == and != operators:

    public static bool operator ==(Weapon w1, Weapon w2)
    {
        return Equals(w1, w2);
    }
    
    public static bool operator !=(Weapon w1, Weapon w2)
    {
        return !Equals(w1, w2);
    }