二进制序列化、静态和单例
BinarySerialization, Static and Singleton
在我的 class 通过二进制序列化后,任何对另一个 class 静态实例的引用中断。
这个例子应该能更好地解释我的意思:
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();
}
}
}
程序抛出错误,因为反序列化的 "sword" 与 "sword" 不同(它的 static
,这是怎么发生的?)
将 weapon
class 变成 singleton
是行不通的,因为那样剑和斧头就是一回事了。
有没有办法指出这两把剑是同一回事,或者我没有得到static
classes的一些核心逻辑?
也许最好的解决方法是完全避免序列化武器。
您可以改为序列化一个武器密钥,并拥有一个类型安全的枚举,您可以从中查找您的武器。
但是,在使用 .net 二进制序列化作为持久性机制之前,您应该考虑所有其他选项 - 它的性能非常差,并且有大量陷阱可能会真正伤害你 - 特别是如果你正在尝试保持版本向后兼容性。
[Serializable]
public sealed class Warrior
{
private readonly Dictionary<int, int> weaponSkills = new Dictionary<int, int>();
public void SetWeaponSkill(Weapon weapon, int skill)
{
weaponSkills[weapon.Key] = skill;
}
public int GetWeaponSkill(Weapon weapon)
{
int skill;
if (!weaponSkills.TryGetValue(weapon.Key, out skill))
{
throw new ArgumentException("Warrior doesn't have weapon");
}
return skill;
}
}
public static class Weapons
{
public static readonly Weapon Sword = new Weapon(1, "Sword");
public static readonly Weapon Dagger = new Weapon(2, "Dagger");
}
public sealed class Weapon
{
public Weapon(int key, string text)
{
Key = key;
Text = text;
}
public int Key { get; }
public string Text { get; }
}
如果您反序列化一个(最初是单例的)对象,它将是一个新实例,除非您指定反序列化应该 return 一个 "well known" 实例。但是你可以通过一些定制来做到这一点:
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");
}
}
}
}
还有一些测试:
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
}
更新:
如果你所有的武器属性都是不变的(如果有更多的实例应该被认为是相等的,这不是问题),那么只需覆盖 Equals
:
public override bool Equals(object obj)
{
var other = obj as Weapon;
if (other == null)
return base.Equals(obj);
return other.kind == this.kind;
}
如果覆盖Equals
,则必须同时覆盖GetHashCode
,否则,您将无法在字典中找到同一对象的不同实例:
public override int GetHashCode()
{
return kind.GetHashCode();
}
请注意,==
运算符仍将 return 引用相等性。如果你想覆盖它,你需要重载 ==
和 !=
运算符:
public static bool operator ==(Weapon w1, Weapon w2)
{
return Equals(w1, w2);
}
public static bool operator !=(Weapon w1, Weapon w2)
{
return !Equals(w1, w2);
}
在我的 class 通过二进制序列化后,任何对另一个 class 静态实例的引用中断。 这个例子应该能更好地解释我的意思:
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();
}
}
}
程序抛出错误,因为反序列化的 "sword" 与 "sword" 不同(它的 static
,这是怎么发生的?)
将 weapon
class 变成 singleton
是行不通的,因为那样剑和斧头就是一回事了。
有没有办法指出这两把剑是同一回事,或者我没有得到static
classes的一些核心逻辑?
也许最好的解决方法是完全避免序列化武器。
您可以改为序列化一个武器密钥,并拥有一个类型安全的枚举,您可以从中查找您的武器。
但是,在使用 .net 二进制序列化作为持久性机制之前,您应该考虑所有其他选项 - 它的性能非常差,并且有大量陷阱可能会真正伤害你 - 特别是如果你正在尝试保持版本向后兼容性。
[Serializable]
public sealed class Warrior
{
private readonly Dictionary<int, int> weaponSkills = new Dictionary<int, int>();
public void SetWeaponSkill(Weapon weapon, int skill)
{
weaponSkills[weapon.Key] = skill;
}
public int GetWeaponSkill(Weapon weapon)
{
int skill;
if (!weaponSkills.TryGetValue(weapon.Key, out skill))
{
throw new ArgumentException("Warrior doesn't have weapon");
}
return skill;
}
}
public static class Weapons
{
public static readonly Weapon Sword = new Weapon(1, "Sword");
public static readonly Weapon Dagger = new Weapon(2, "Dagger");
}
public sealed class Weapon
{
public Weapon(int key, string text)
{
Key = key;
Text = text;
}
public int Key { get; }
public string Text { get; }
}
如果您反序列化一个(最初是单例的)对象,它将是一个新实例,除非您指定反序列化应该 return 一个 "well known" 实例。但是你可以通过一些定制来做到这一点:
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");
}
}
}
}
还有一些测试:
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
}
更新:
如果你所有的武器属性都是不变的(如果有更多的实例应该被认为是相等的,这不是问题),那么只需覆盖 Equals
:
public override bool Equals(object obj)
{
var other = obj as Weapon;
if (other == null)
return base.Equals(obj);
return other.kind == this.kind;
}
如果覆盖Equals
,则必须同时覆盖GetHashCode
,否则,您将无法在字典中找到同一对象的不同实例:
public override int GetHashCode()
{
return kind.GetHashCode();
}
请注意,==
运算符仍将 return 引用相等性。如果你想覆盖它,你需要重载 ==
和 !=
运算符:
public static bool operator ==(Weapon w1, Weapon w2)
{
return Equals(w1, w2);
}
public static bool operator !=(Weapon w1, Weapon w2)
{
return !Equals(w1, w2);
}