第 24 章 运行时序列化
序列化是什么?
对象转化为字节流的过程,字节流转化为对象叫反序列化
- 应用程序状态可以很轻松保存在磁盘文件或数据库中,并在下次运行应用程序时恢复
- 一组对象可以复制到系统粘贴板,并复制到同一个或其他应用程序中
- 一组对象可以通过网络发送到另一台机器运行的进程中
下面是简单的序列化和反序列化
class MainClass
{
public static void Main()
{
var objs = new List<string> { "a", "b", "c" };
Stream stream = SerializeToMemory(objs);
objs = null;
Console.WriteLine($"{stream.Position}");
var dObj = DeserializeFromMemory(stream);
foreach (var item in (List<string>)dObj)
{
Console.WriteLine(item);
}
}
static MemoryStream SerializeToMemory(Object objectGraph)
{
MemoryStream memoryStream = new MemoryStream();
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(memoryStream, objectGraph);
return memoryStream;
}
static Object DeserializeFromMemory(Stream stream)
{
stream.Seek(0, SeekOrigin.Begin);
BinaryFormatter bf = new BinaryFormatter();
return bf.Deserialize(stream);
}
}
Stream
对象标识序列化的字节放在哪里BinaryFormatter
实现了IFormatter
使其知道如何序列化和反序列化对象图DeserializeFromMemory
检查流的内容,构造出所有对象的实例,并且初始化所有字段,使其与序列化前对象拥有相同的值,可以利用此特性进行深拷贝,之前List<T>
进行深拷贝就利用过这个特性
private static Object DeepClone(Object original) {
using (MemoryStream stream = new MemoryStream()) {
BinaryFormatter formatter = new BinaryFormatter();
formatter.Context = new StreamingContext(StreamingContextStates.Clone);
formatter.Serialize(stream, original);
stream.Position = 0;
return formatter.Deserialize(stream);
}
}
- 可以将多个对象图序列化到一个流中,并根据相同顺序反序列化
[Serializable] public class A
{
public int val;
}
[Serializable]
public class B
{
public string val;
}
public sealed class Program
{
public static void Main()
{
var aList = new List<A>();
aList.Add(new A() { val = 1 });
aList.Add(new A() { val = 2 });
var bList = new List<B>();
bList.Add(new B() { val = "a" });
bList.Add(new B() { val = "b" });
MemoryStream ms = new MemoryStream();
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(ms, aList);
bf.Serialize(ms, bList);
ms.Seek(0, SeekOrigin.Begin);
var daList = (List<A>)bf.Deserialize(ms);
var dbList = (List<B>)bf.Deserialize(ms);
foreach (var item in daList)
{
Console.WriteLine(item.val);
}
foreach (var item in dbList)
{
Console.WriteLine(item.val);
}
Console.WriteLine(Assembly.GetEntryAssembly().FullName);
}
}
使类型可序列化
- 对象图可能一部分可以序列化、一部分不能序列化,当序列化抛出异常,字节流中可能存在一部分序列化的数据,所以应该应该序列化到
MemoryStream
中,然后再将字节复制到目标字节流
,可以使用CopyTo
,CopyTo拷贝的字节是剩余字节(从Position开始算) SerializableAttribute
默认序列化枚举和委托类型
,并且是不可被派生类继承的
控制序列化和反序列化
- 如果某字段很容易计算,可以使用
[NonSerialized]
将字段不序列化,减少传输量和性能 - 应用
[NonSerialized]
,当进行反序列化后,对象这个字段将为默认值,可以利用[OnDeserialized]
标识,将会在序列化完成后调用 [NonSerialized]和[OnDeserialized]会被派生类继承(如果B继承了A,那么当B反序列化后同样会执行[OnDeserialized]标记的方法),基类被[NonSerialized]字段还是不纳入序列化
- 最后除了上面的标识还有很多控制序列化和反序列化的标识 e.g.
OnDeserializing、OnSerializing、OnSerialized
[Serializable]
public class A
{
public int a;
public int b;
[NonSerialized]
public int sum;
public A(int a, int b)
{
this.a = a;
this.b = b;
this.sum = a + b;
}
[OnDeserialized]
void OnDeserialized_ClassA(StreamingContext context)
{
sum = a + b;
}
}
public sealed class Program
{
public static void Main()
{
A a = new A(1, 2);
MemoryStream ms = new MemoryStream();
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(ms, a);
ms.Seek(0, SeekOrigin.Begin);
A da = (A) bf.Deserialize(ms);
Console.WriteLine($"Deserialize sum {da.sum}");
Console.WriteLine("...");
}
}
格式化器如何序列化类型实例
-
如何序列化对象的字段
– FormatterServices.GetSerializableMembers方法
通过反射可以获得所有可序列化字段(除了[NonSerialized]
标记的字段)
– FormatterServices.GetObjectData方法
获得被序列化的字段的值(这个和上面方法返回的MemberInfo是一一对应的)
– 格式化器将程序集标识和类型的完整名称
写入流中
– 格式化器然后遍历两个数组中的元素,将每个成员的名称和值写入流中
-
如何反序列化对象的字段
– 首先读取程序集标识和类型完整名称
,如果程序集没有加载到AppDomain中,那么就去加载,无法加载就抛出异常,程序集存在就使用FormatterServices.GetTypeFromAssembly
获得反序列化类型
– 然后将类型传入FormatterServices.GetUninitializedObject方法
,这个方法分配一个新对象,但是不调用构造方法,对象所有字节为Null或0
– 格式化器创建一个MemberInfo数组
,利用FormatterServices.GetSerializableMembers方法
获得,然后根据流中的数据初始化一个Object数组
– 最后利用FormatterServices.PopulateObjectMembers
一一对应填充字段
控制序列化/反序列化的数据
- 前面提到的例如
OnSerialized、OnDeserialized
不能满足对序列化全部控制,并且在上面发现序列化和反序化使用了反射
,反射速度是很慢的 后面用到再看
流上下文
- 一组序列化好的对象有很多目的地,使用时只需设置
IFormatter
的Context
序列化代理等剩余内容,用到再看