C# Language
Serializzazione binaria
Ricerca…
Osservazioni
Il motore di serializzazione binario fa parte del framework .NET, ma gli esempi qui riportati sono specifici di C #. Rispetto ad altri motori di serializzazione integrati nel framework .NET, il serializzatore binario è veloce ed efficiente e di solito richiede pochissimo codice aggiuntivo per farlo funzionare. Tuttavia, è anche meno tollerante alle modifiche al codice; cioè, se serializzi un oggetto e poi apporti una leggera modifica alla definizione dell'oggetto, probabilmente non lo deserializzi correttamente.
Rendere serializzabile un oggetto
Aggiungi l'attributo [Serializable]
per contrassegnare un intero oggetto per la serializzazione binaria:
[Serializable]
public class Vector
{
public int X;
public int Y;
public int Z;
[NonSerialized]
public decimal DontSerializeThis;
[OptionalField]
public string Name;
}
Tutti i membri verranno serializzati a meno che non si [NonSerialized]
esplicitamente utilizzando l'attributo [NonSerialized]
. Nel nostro esempio, X
, Y
, Z
e Name
sono tutti serializzati.
Tutti i membri devono essere presenti alla deserializzazione a meno che non siano contrassegnati con [NonSerialized]
o [OptionalField]
[NonSerialized]
[OptionalField]
. Nel nostro esempio, X
, Y
e Z
sono tutti richiesti e la deserializzazione fallirà se non sono presenti nello stream. DontSerializeThis
sarà sempre impostato su default(decimal)
(che è 0). Se Name
è presente nello stream, verrà impostato su quel valore, altrimenti verrà impostato su default(string)
(che è null). Lo scopo di [OptionalField]
è di fornire un po 'di tolleranza della versione.
Controllo del comportamento di serializzazione con attributi
Se si utilizza l'attributo [NonSerialized]
, quel membro avrà sempre il suo valore predefinito dopo la deserializzazione (ad esempio 0 per un int
, null per string
, false per un bool
, ecc.), Indipendentemente dall'inizializzazione eseguita nell'oggetto stesso (costruttori, dichiarazioni, ecc.). Per compensare, vengono [OnDeserializing]
gli attributi [OnDeserializing]
(chiamato solo BEFORE deserializing) e [OnDeserialized]
(chiamato solo AFTER deserializing) insieme alle loro controparti, [OnSerializing]
e [OnSerialized]
.
Supponiamo di voler aggiungere un "Rating" al nostro Vector e vogliamo assicurarci che il valore inizi sempre a 1. Il modo in cui è scritto qui sotto, sarà 0 dopo essere stato deserializzato:
[Serializable]
public class Vector
{
public int X;
public int Y;
public int Z;
[NonSerialized]
public decimal Rating = 1M;
public Vector()
{
Rating = 1M;
}
public Vector(decimal initialRating)
{
Rating = initialRating;
}
}
Per risolvere questo problema, possiamo semplicemente aggiungere il seguente metodo all'interno della classe per impostarlo su 1:
[OnDeserializing]
void OnDeserializing(StreamingContext context)
{
Rating = 1M;
}
Oppure, se vogliamo impostarlo su un valore calcolato, possiamo aspettare che finisca la deserializzazione e quindi impostarlo:
[OnDeserialized]
void OnDeserialized(StreamingContext context)
{
Rating = 1 + ((X+Y+Z)/3);
}
Allo stesso modo, possiamo controllare come vengono scritte le cose usando [OnSerializing]
e [OnSerialized]
.
Aggiungere più controllo implementando ISerializable
Ciò otterrebbe un maggiore controllo sulla serializzazione, su come salvare e caricare i tipi
Implementare un'interfaccia ISerializable e creare un costruttore vuoto da compilare
[Serializable]
public class Item : ISerializable
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
public Item ()
{
}
protected Item (SerializationInfo info, StreamingContext context)
{
_name = (string)info.GetValue("_name", typeof(string));
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("_name", _name, typeof(string));
}
}
Per la serializzazione dei dati, è possibile specificare il nome desiderato e il tipo desiderato
info.AddValue("_name", _name, typeof(string));
Quando i dati sono deserializzati, sarai in grado di leggere il tipo desiderato
_name = (string)info.GetValue("_name", typeof(string));
Surrogati di serializzazione (implementazione di ISerializationSurrogate)
Implementa un selettore surrogato di serializzazione che consente a un oggetto di eseguire la serializzazione e la deserializzazione di un altro
Inoltre consente di serializzare o deserializzare correttamente una classe che non è serializzabile
Implementare l'interfaccia ISerializationSurrogate
public class ItemSurrogate : ISerializationSurrogate
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
var item = (Item)obj;
info.AddValue("_name", item.Name);
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
var item = (Item)obj;
item.Name = (string)info.GetValue("_name", typeof(string));
return item;
}
}
Quindi devi informare il tuo IFormatter dei surrogati definendo e inizializzando un SurrogateSelector e assegnandolo al tuo IFormatter
var surrogateSelector = new SurrogateSelector();
surrogateSelector.AddSurrogate(typeof(Item), new StreamingContext(StreamingContextStates.All), new ItemSurrogate());
var binaryFormatter = new BinaryFormatter
{
SurrogateSelector = surrogateSelector
};
Anche se la classe non è marcata serializzabile.
//this class is not serializable
public class Item
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
La soluzione completa
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
namespace BinarySerializationExample
{
class Item
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
class ItemSurrogate : ISerializationSurrogate
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
var item = (Item)obj;
info.AddValue("_name", item.Name);
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
var item = (Item)obj;
item.Name = (string)info.GetValue("_name", typeof(string));
return item;
}
}
class Program
{
static void Main(string[] args)
{
var item = new Item
{
Name = "Orange"
};
var bytes = SerializeData(item);
var deserializedData = (Item)DeserializeData(bytes);
}
private static byte[] SerializeData(object obj)
{
var surrogateSelector = new SurrogateSelector();
surrogateSelector.AddSurrogate(typeof(Item), new StreamingContext(StreamingContextStates.All), new ItemSurrogate());
var binaryFormatter = new BinaryFormatter
{
SurrogateSelector = surrogateSelector
};
using (var memoryStream = new MemoryStream())
{
binaryFormatter.Serialize(memoryStream, obj);
return memoryStream.ToArray();
}
}
private static object DeserializeData(byte[] bytes)
{
var surrogateSelector = new SurrogateSelector();
surrogateSelector.AddSurrogate(typeof(Item), new StreamingContext(StreamingContextStates.All), new ItemSurrogate());
var binaryFormatter = new BinaryFormatter
{
SurrogateSelector = surrogateSelector
};
using (var memoryStream = new MemoryStream(bytes))
return binaryFormatter.Deserialize(memoryStream);
}
}
}
Serialization Binder
Il raccoglitore ti dà l'opportunità di verificare quali tipi vengono caricati nel dominio dell'applicazione
Creare una classe ereditata da SerializationBinder
class MyBinder : SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
if (typeName.Equals("BinarySerializationExample.Item"))
return typeof(Item);
return null;
}
}
Ora possiamo controllare quali tipi stanno caricando e su questa base per decidere cosa vogliamo veramente ricevere
Per usare un raccoglitore, devi aggiungerlo a BinaryFormatter.
object DeserializeData(byte[] bytes)
{
var binaryFormatter = new BinaryFormatter();
binaryFormatter.Binder = new MyBinder();
using (var memoryStream = new MemoryStream(bytes))
return binaryFormatter.Deserialize(memoryStream);
}
La soluzione completa
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
namespace BinarySerializationExample
{
class MyBinder : SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
if (typeName.Equals("BinarySerializationExample.Item"))
return typeof(Item);
return null;
}
}
[Serializable]
public class Item
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
class Program
{
static void Main(string[] args)
{
var item = new Item
{
Name = "Orange"
};
var bytes = SerializeData(item);
var deserializedData = (Item)DeserializeData(bytes);
}
private static byte[] SerializeData(object obj)
{
var binaryFormatter = new BinaryFormatter();
using (var memoryStream = new MemoryStream())
{
binaryFormatter.Serialize(memoryStream, obj);
return memoryStream.ToArray();
}
}
private static object DeserializeData(byte[] bytes)
{
var binaryFormatter = new BinaryFormatter
{
Binder = new MyBinder()
};
using (var memoryStream = new MemoryStream(bytes))
return binaryFormatter.Deserialize(memoryStream);
}
}
}
Alcuni trucchi in retrocompatibilità
Questo piccolo esempio mostra come si può perdere la retrocompatibilità nei propri programmi se non si presta attenzione a ciò. E i modi per ottenere un maggiore controllo del processo di serializzazione
Inizialmente, scriveremo un esempio della prima versione del programma:
Versione 1
[Serializable]
class Data
{
[OptionalField]
private int _version;
public int Version
{
get { return _version; }
set { _version = value; }
}
}
E ora, supponiamo che nella seconda versione del programma sia stata aggiunta una nuova classe. E abbiamo bisogno di memorizzarlo in un array.
Ora il codice sarà simile a questo:
Versione 2
[Serializable]
class NewItem
{
[OptionalField]
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
[Serializable]
class Data
{
[OptionalField]
private int _version;
public int Version
{
get { return _version; }
set { _version = value; }
}
[OptionalField]
private List<NewItem> _newItems;
public List<NewItem> NewItems
{
get { return _newItems; }
set { _newItems = value; }
}
}
E codice per serializzare e deserializzare
private static byte[] SerializeData(object obj)
{
var binaryFormatter = new BinaryFormatter();
using (var memoryStream = new MemoryStream())
{
binaryFormatter.Serialize(memoryStream, obj);
return memoryStream.ToArray();
}
}
private static object DeserializeData(byte[] bytes)
{
var binaryFormatter = new BinaryFormatter();
using (var memoryStream = new MemoryStream(bytes))
return binaryFormatter.Deserialize(memoryStream);
}
E quindi, cosa succederebbe quando serializzaste i dati nel programma di v2 e cerchereste di deserializzarli nel programma di v1?
Ottieni un'eccezione:
System.Runtime.Serialization.SerializationException was unhandled
Message=The ObjectManager found an invalid number of fixups. This usually indicates a problem in the Formatter.Source=mscorlib
StackTrace:
at System.Runtime.Serialization.ObjectManager.DoFixups()
at System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream)
at Microsoft.Samples.TestV1.Main(String[] args) in c:\Users\andrew\Documents\Visual Studio 2013\Projects\vts\CS\V1 Application\TestV1Part2\TestV1Part2.cs:line 29
at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()
Perché?
L'ObjectManager ha una logica diversa per risolvere le dipendenze per gli array e per i tipi di riferimento e valore. Abbiamo aggiunto una serie di nuovi il tipo di riferimento che è assente nel nostro assembly.
Quando ObjectManager tenta di risolvere le dipendenze costruisce il grafico. Quando vede la matrice, non può risolverlo immediatamente, in modo da creare un riferimento fittizio e quindi correggere l'array in un secondo momento.
E poiché questo tipo non è nell'assembly e le dipendenze non possono essere riparate. Per qualche ragione, non rimuove la matrice dall'elenco di elementi per le correzioni e alla fine genera un'eccezione "IncorrectNumberOfFixups".
Sono alcuni "trucchi" nel processo di serializzazione. Per qualche ragione, non funziona correttamente solo per le matrici di nuovi tipi di riferimento.
A Note:
Similar code will work correctly if you do not use arrays with new classes
E il primo modo per risolverlo e mantenere la compatibilità?
- Usa una collezione di nuove strutture piuttosto che classi o usa un dizionario (possibili classi), perché un dizionario è una raccolta di keyvaluepair (è la struttura)
- Usa ISerializable, se non puoi modificare il vecchio codice