C# Language
Serializacja binarna
Szukaj…
Uwagi
Binarny silnik serializacji jest częścią frameworka .NET, ale podane tutaj przykłady są specyficzne dla C #. W porównaniu z innymi silnikami serializacji wbudowanymi w platformę .NET, binarny serializator jest szybki i wydajny i zwykle wymaga bardzo mało dodatkowego kodu, aby go uruchomić. Jest jednak mniej tolerancyjny na zmiany kodu; to znaczy, jeśli serializujesz obiekt, a następnie dokonasz niewielkiej zmiany w definicji obiektu, prawdopodobnie nie zdezrializuje poprawnie.
Uczynienie obiektu możliwym do serializacji
Dodaj atrybut [Serializable]
aby oznaczyć cały obiekt do binarnej serializacji:
[Serializable]
public class Vector
{
public int X;
public int Y;
public int Z;
[NonSerialized]
public decimal DontSerializeThis;
[OptionalField]
public string Name;
}
Wszyscy członkowie zostaną zserializowani, chyba że jednoznacznie zrezygnujemy z użycia atrybutu [NonSerialized]
. W naszym przykładzie X
, Y
, Z
i Name
są serializowane.
Wszyscy członkowie muszą być obecni podczas deserializacji, chyba że są oznaczone [NonSerialized]
lub [OptionalField]
. W naszym przykładzie wszystkie X
, Y
i Z
są wymagane, a deserializacja zakończy się niepowodzeniem, jeśli nie będą obecne w strumieniu. DontSerializeThis
zawsze będzie ustawione na default(decimal)
(czyli 0). Jeśli Name
jest obecna w strumieniu, zostanie ustawiona na tę wartość, w przeciwnym razie zostanie ustawiona default(string)
(która jest równa null). Celem [OptionalField]
jest zapewnienie odrobiny tolerancji wersji.
Kontrolowanie zachowania serializacji za pomocą atrybutów
Jeśli [NonSerialized]
atrybutu [NonSerialized]
, wówczas ten element członkowski będzie zawsze miał domyślną wartość po deserializacji (np. 0 dla int
, null dla string
, false dla bool
itp.), Niezależnie od jakiejkolwiek inicjalizacji wykonanej w samym obiekcie (konstruktory, deklaracje itp.). Aby to zrekompensować, [OnDeserializing]
atrybuty [OnDeserializing]
(nazywane po prostu PRZED deserializacją) i [OnDeserialized]
(nazywane po prostu PO deserializowaniu) wraz z ich odpowiednikami, [OnSerializing]
i [OnSerialized]
.
Załóżmy, że chcemy dodać „ocenę” do naszego wektora i chcemy mieć pewność, że wartość zawsze zaczyna się od 1. Sposób, w jaki jest zapisany poniżej, będzie wynosił 0 po deserializacji:
[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;
}
}
Aby rozwiązać ten problem, możemy po prostu dodać następującą metodę w klasie, aby ustawić ją na 1:
[OnDeserializing]
void OnDeserializing(StreamingContext context)
{
Rating = 1M;
}
Lub, jeśli chcemy ustawić wartość obliczoną, możemy poczekać, aż zakończy się deserializacja, a następnie ustawić:
[OnDeserialized]
void OnDeserialized(StreamingContext context)
{
Rating = 1 + ((X+Y+Z)/3);
}
Podobnie możemy kontrolować sposób, w jaki rzeczy są spisywane za pomocą [OnSerializing]
i [OnSerialized]
.
Dodanie większej kontroli poprzez wdrożenie ISerializable
Zapewniłoby to większą kontrolę nad serializacją, sposobem zapisywania i ładowania typów
Zaimplementuj interfejs ISerializable i utwórz pusty konstruktor do kompilacji
[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));
}
}
W przypadku serializacji danych można określić żądaną nazwę i żądany typ
info.AddValue("_name", _name, typeof(string));
Gdy dane zostaną przekształcone w postaci szeregowej, będziesz w stanie odczytać pożądany typ
_name = (string)info.GetValue("_name", typeof(string));
Zastępstwa serializacji (Implementing ISerializationSurrogate)
Implementuje selektor zastępowania serializacji, który pozwala jednemu obiektowi na serializację i deserializację innego
Pozwala również na prawidłowe serializowanie lub deserializację klasy, która sama nie jest serializowalna
Zaimplementuj interfejs 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;
}
}
Następnie musisz poinformować swój IFormatter o surogatach poprzez zdefiniowanie i zainicjowanie SurrogateSelector i przypisanie go do swojego IFormatter
var surrogateSelector = new SurrogateSelector();
surrogateSelector.AddSurrogate(typeof(Item), new StreamingContext(StreamingContextStates.All), new ItemSurrogate());
var binaryFormatter = new BinaryFormatter
{
SurrogateSelector = surrogateSelector
};
Nawet jeśli klasa nie jest oznaczona jako możliwa do serializacji.
//this class is not serializable
public class Item
{
private string _name;
public string Name
{
get { return _name; }
set { _name = value; }
}
}
Kompletne rozwiązanie
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
Spoiwo daje możliwość sprawdzenia, jakie typy są ładowane w domenie aplikacji
Utwórz klasę odziedziczoną z SerializationBinder
class MyBinder : SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
if (typeName.Equals("BinarySerializationExample.Item"))
return typeof(Item);
return null;
}
}
Teraz możemy sprawdzić, jakie typy ładują się i na tej podstawie zdecydować, co naprawdę chcemy otrzymać
Aby użyć spoiwa, musisz dodać go do BinaryFormatter.
object DeserializeData(byte[] bytes)
{
var binaryFormatter = new BinaryFormatter();
binaryFormatter.Binder = new MyBinder();
using (var memoryStream = new MemoryStream(bytes))
return binaryFormatter.Deserialize(memoryStream);
}
Kompletne rozwiązanie
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);
}
}
}
Niektóre problemy z kompatybilnością wsteczną
Ten mały przykład pokazuje, w jaki sposób można utracić kompatybilność wsteczną w swoich programach, jeśli nie podejmie się tego z wyprzedzeniem. I sposoby uzyskania większej kontroli nad procesem serializacji
Najpierw napiszemy przykład pierwszej wersji programu:
Wersja 1
[Serializable]
class Data
{
[OptionalField]
private int _version;
public int Version
{
get { return _version; }
set { _version = value; }
}
}
A teraz załóżmy, że w drugiej wersji programu dodano nową klasę. I musimy przechowywać go w tablicy.
Teraz kod będzie wyglądał następująco:
Wersja 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; }
}
}
I kod do serializacji i deserializacji
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);
}
A więc, co by się stało, gdy serializujesz dane w programie v2 i spróbujesz deserializować je w programie v1?
Otrzymujesz wyjątek:
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()
Dlaczego?
ObjectManager ma inną logikę do rozwiązywania zależności dla tablic oraz typów referencyjnych i wartościowych. Dodaliśmy tablicę nowego typu odniesienia, którego nie ma w naszym zestawie.
Kiedy ObjectManager próbuje rozwiązać zależności, tworzy wykres. Kiedy widzi tablicę, nie może jej natychmiast naprawić, więc tworzy fałszywe odniesienie, a następnie naprawia tablicę później.
A ponieważ tego typu nie ma w zestawie, a zależności nie można naprawić. Z jakiegoś powodu nie usuwa tablicy z listy elementów dla poprawek, a na końcu zgłasza wyjątek „IncorrectNumberOfFixups”.
To pewne „gotchas” w procesie serializacji. Z jakiegoś powodu nie działa poprawnie tylko dla tablic nowych typów referencyjnych.
A Note:
Similar code will work correctly if you do not use arrays with new classes
A pierwszy sposób, aby to naprawić i zachować zgodność?
- Użyj kolekcji nowych struktur zamiast klas lub użyj słownika (możliwe klasy), ponieważ słownik jest zbiorem pary kluczy-wartości (jego struktura)
- Użyj ISerializable, jeśli nie możesz zmienić starego kodu