《CLR Via C#》读书笔记:24.运行时序列化
一、什么是运行时序列化
序列化的作用就是将对象图(特定时间点的对象连接图)转换为字节流,这样这些对象图就可以在文件系统/网络进行传输。
二、序列化/反序列化快速入门
一般来说我们通过 FCL 提供的 BinaryFormatter
对象就可以将一个对象序列化为字节流进行存储,或者通过该 Formatter 将一个字节流反序列化为一个对象。
FCL 的序列化与反序列化
序列化操作:
public MemoryStream SerializeObj(object sourceObj)
{
var memStream = new MemoryStream();
var formatter = new BinaryFormatter();
formatter.Serialize(memStream, sourceObj);
return memStream;
}
反序列化操作:
public object DeserializeFromStream(MemoryStream stream)
{
var formatter = new BinaryFormatter();
stream.Position = 0;
return formatter.Deserialize(stream);
}
反序列化通过 Formatter 的
Deserialize()
方法返回序列化好的对象图的根对象的一个引用。
深拷贝
通过序列化与反序列化的特性,可以实现一个深拷贝的方法,用户创建源对象的一个克隆体。
public object DeepClone(object originalObj)
{
using (var memoryStream = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(memoryStream, originalObj);
// 表明对象是被克隆的,可以安全的访问其他托管资源
formatter.Context = new StreamingContext(StreamingContextStates.Clone);
memoryStream.Position = 0;
return formatter.Deserialize(memoryStream);
}
}
另外一种技巧就是可以将多个对象图序列化到一个流当中,即调用多次 Serialize()
方法将多个对象图序列化到流当中。如果需要反序列化的时候,按照序列化时对象图的序列化顺序反向反序列化即可。
BinaryFormatter
在序列化的时候会将类型的全名与程序集定义写入到流当中,这样在反序列化的时候,格式化器会获取这些信息,并且通过 System.Reflection.Assembly.Load()
方法将程序集加载到当前的 AppDomain
。
在程序集加载完成之后,会在该程序集搜索待反序列化的对象图类型,找不到则会抛出异常。
【注意】
某些应用程序通过
Assembly.LoadFrom()
来加载程序集,然后根据程序集中的类型来构造对象。序列化该对象是没问题的,但是反序列化的时候格式化器使用的是Assembly.Load()
方法来加载程序集,这样的话就会导致无法正确加载对象。这个时候,你可以实现一个与
System.ResolveEventHandler
签名一样的委托,并且在反序列化注册到当前AppDomain
的AssemblyResolve
事件。这样当程序集加载失败的时候,你可以在该方法内部根据传入的事件参数与程序集标识自己使用
Assembly.LoadFrom()
来构造一个Assembly
对象。记得在反序列化完成之后,马上向事件注销这个方法,否则会造成内存泄漏。
三、使类型可序列化
在设计自定义类型时,你需要显式地通过 Serializable
特性来声明你的类型是可以被序列化的。如果没有这么做,在使用格式化器进行序列化的时候,则会抛出异常。
[Serializable]
public class DIYClass
{
public int x { get; set; }
public int y { get; set; }
}
【注意】
正因为这样,我们一般都会现将结果保存到
MemoryStream
之中,当没有抛出异常之后再将这些数据写入到文件/网络。
Serializable 特性
Serializable
特性只能用于值类型、引用类型、枚举类型(默认)、委托类型(默认),而且是不可被子类继承。
如果有一个 A 类与其派生类 B 类,那么 A 类没拥有 Serializable
特性,而子类拥有,一样的是无法进行序列化操作。
而且序列化的时候,是将所有访问级别的字段成员都进行了序列化,包括 private 级别成员。
四、简单控制序列化操作
禁止序列化某个字段
可以通过 System.NonSerializedAttribute
特性来确保某个字段在序列化时不被处理其值,例如下列代码:
[Serializable]
public class DIYClass
{
public DIYClass()
{
x = 10;
y = 100;
z = 1000;
}
public int x { get; set; }
public int y { get; set; }
[NonSerialized]
public int z;
}
在序列化之前,该自定义对象 z 字段的值为 1000,在序列化时,检测到了忽略特性,则不会写入该字段的值到流当中。并且在反序列化之后,z 的值为 0,而 x ,y 的值是 10 和 100。
序列化与反序列化的四个生命周期特性
通过 OnSerializing
、OnSerialized
、OnDeserializing
、OnDeserialized
这四个特性,我们可以在对象序列化与反序列化时进行一些自定义的控制。只需要将这四个特性分别加在四个方法上面即可,但是针对方法签名必须返回值为 void,同时也需要用有一个 StreamingContext
参数。
而且一般建议将这四个方法标识为 private ,防止其他对象误调用。
[Serializable]
public class DIYClass
{
[OnDeserializing]
private void OnDeserializing(StreamingContext context)
{
Console.WriteLine("反序列化的时候,会调用本方法.");
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
Console.WriteLine("反序列化完成的时候,会调用本方法.");
}
[OnSerializing]
public void OnSerializing(StreamingContext context)
{
Console.WriteLine("序列化的时候,会调用本方法.");
}
[OnSerialized]
public void OnSerialized(StreamingContext context)
{
Console.WriteLine("序列化完成的时候,会调用本方法.");
}
}
【注意】
如果 A 类型有两个版本,第 1 个版本有 5 个字段,并被序列化存储到了文件当中。后面由于业务需要,针对于 A 类型增加了 2 个新的字段,这个时候如果从文件中读取第 1 个版本的对象流信息,就会抛出异常。
我们可以通过
System.Runtime.Serialization.OptionalFieldAttribute
添加到我们新加的字段之上,这样的话在反序列化数据时就不会因为缺少字段而抛出异常。
五、格式化器的序列化原理
格式化器的核心就是 FCL 提供的 FormatterServices
的静态工具类,下列步骤体现了序列化器如何结合 FormatterServices
工具类来进行序列化操作的。
- 格式化器调用
FormatterService.GetSerializableMembers()
方法获得需要序列化的字段构成的MemberInfo
数组。 - 格式化器调用
FormatterService.GetObjectData()
方法,通过之前获取的字段MethodInfo
信息来取得每个字段存储的值数组。该数组与字段信息数组是并行的,下标一致。 - 格式化器写入类型的程序集等信息。
- 遍历两个数组,写入字段信息与其数据到流当中。
反序列化操作的步骤与上面相反。
- 首先从流头部读取程序集标识与类型信息,如果当前 AppDomain 没有加载该程序集会抛出异常。如果类型的程序集已经加载,则通过
FormatterServices.GetTypeFromAssembly()
方法来构造一个 Type 对象。 - 格式化器调用
FormatterService.GetUninitializedObject()
方法为新对象分配内存,但是 不会调用对象的构造器。 - 格式化器通过
FormatterService.GetSerializableMembers()
初始化一个MemberInfo
数组。 - 格式化器根据流中的数据创建一个 Object 数组,该数组就是字段的数据。
- 格式化器通过
FormatterService.PopulateObjectMembers()
方法,传入新分配的对象、字段信息数组、字段数据数组进行对象初始化。
六、控制序列化/反序列化的数据
一般来说通过在第四节说的那些特性控制就已经满足了大部分需求,但格式化器内部使用的是反射,反射性能开销比较大,如果你想要针对序列化/反序列化进行完全的控制,那么你可以实现 ISerializable
接口来进行控制。
该接口只提供了一个 GetObjectData()
方法,原型如下:
public interface ISerializable{
void GetObjectData(SerializationInfo info,StreamingContext context);
}
【注意】
使用了
ISerializable
接口的代价就是其集成类都必须实现它,而且还要保证子类必须调用基类的GetObjectData()
方法与其构造函数。一般来说密封类才使用ISerializable
,其他的类型使用特性控制即可满足。另外为了防止其他的代码调用
GetObjectData()
方法,可以通过一下特性来防止误操作:[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]
如果格式化器检测到了类型实现了该接口,则会忽略掉原有的特性,并且将字段值传入到 SerializationInfo
之中。
通过这个 Info 我们可以被序列化的类型,因为 Info 提供了 FullTypeName
与 AssemblyName
,不过一般推荐使用该对象提供的 SetType(Type type)
方法来进行操作。
格式化器构造完成 Info 之后,则会调用 GetObjectData()
方法,这个时候将之前构造好的 Info 传入,而该方法则决定需要用哪些数据来序列化对象。这个时候我们就可以通过 Info 的 AddValue()
方法来添加一些信息用于反序列化时使用。
在反序列化的时候,需要类型提供一个特殊的构造函数,对于密封类来说,该构造函数推荐为 private ,而一般的类型推荐为 protected,这个特殊的构造函数方法签名与 GetObjectData()
一样。
因为在反序列化的时候,格式化器会调用这个特殊的构造函数。
以下代码就是一个简单实践:
public class DIYClass : ISerializable
{
public int X { get; set; }
public int Y { get; set; }
public DIYClass() { }
protected DIYClass(SerializationInfo info, StreamingContext context)
{
X = info.GetInt32("X");
Y = 20;
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("X", 10);
}
}
该类型的对象在反序列化之后,X 的值为序列化之前的值,而 Y 的值始终都会为 20。
【注意】
如果你存储的 X 值是 Int32 ,而在获取的时候是通过 GetInt64() 进行获取。那么格式化器就会尝试使用
System.Convert
提供的方法进行转换,并且可以通过实现IConvertible
接口来自定义自己的转换。不过只有在 Get 方法转换失败的情况下才会使用上述机制。
子类与基类的 ISerializable
如果某个子类集成了基类,那么子类在其 GetObjectData()
与特殊构造器中都要调用父类的方法,这样才能够完成正确的序列化/反序列化操作。
如果基类没有实现 ISerializable
接口与特殊的构造器,那么子类就需要通过 FormatterService
来手动针对基类的字段进行赋值。
七、流上下文
流上下文 StreamingContext
只有两个属性,第一个是状态标识位,用于标识序列化/反序列化对象的来源与目的地。而第二个属性就是一个 Object 引用,该引用则是一个附加的上下文信息,由用户进行提供。
八、类型序列化为不同的类型与对象反序列化为不同的对象
在某些时候可能需要更改序列化完成之后的对象类型,这个时候只需要对象在其实现 ISerializable
接口的 GetObjectData()
方法内部通过 SerializationInfo
的 SetType()
方法变更了序列化的目标类型。
下面的代码演示了如何序列化一个单例对象:
[Serializable]
public sealed class Singleton : ISerializable
{
private static readonly Singleton _instance = new Singleton();
private Singleton() { }
public static Singleton GetSingleton() { return _instance; }
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter =true)]
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
info.SetType(typeof(SingletonHelper));
}
}
这里通过显式实现接口的 GetObjectData()
方法来将序列化的目标类型设置为 SingletonHelper
,该类型的定义如下:
[Serializable]
public class SingletonHelper : IObjectReference
{
public object GetRealObject(StreamingContext context)
{
return Singleton.GetSingleton();
}
}
这里因为 SingletonHelper
实现了 IObjectReference
接口,当格式化器尝试进行反序列化的时候,由于在 GetObjectData()
欺骗了转换器,因此反序列化的时候检测到类型有实现该接口,所以会尝试调用其 GetRealObject()
方法来进行反序列化操作。
而以上动作完成之后,SingletonHelper
会立即变为不可达对象,等待 GC 进行回收处理。
九、序列化代理
当某些时候需要对一个第三方库对象进行序列化的时候,没有其源码,但是想要进行序列化,则可以通过序列化代理来进行序列化操作。
要实现序列化代理,需要实现 ISerializationSurrogate
接口,该接口拥有两个方法,其签名分别如下:
void GetObjectData(Object obj,SerializationInfo info,StreamingContext context);
void SetObjectData(Object obj,SerializationInfo info,StreamingContext context,ISurrogateSelector selector);
GetObjectData()
方法会在对象序列化时进行调用,而 SetObjectData()
会在对象反序列化时调用。
比如说我们有一个需求是希望 DateTime
类型在序列化的时候通过 UTC 时间序列化到流中,而在反序列化时则更改为本地时间。
这个时候我们就可以自己实现一个序列化代理类 UTCToLocalTimeSerializationSurrogate
:
public sealed class UTCToLocalTimeSerializationSurrogate : ISerializationSurrogate
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
info.AddValue("Date", ((DateTime)obj).ToUniversalTime().ToString("u"));
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
return DateTime.ParseExact(info.GetString("Date"), "u", null).ToLocalTime();
}
}
并且在使用的时候,通过构造一个 SurrogateSelector
代理选择器,传入我们针对于 DateTime
类型的代理,并且将格式化器与代理选择器相绑定。那么在使用格式化器的时候,就会通过我们的代理类来处理 DateTime
类型对象的序列化/反序列化操作了。
static void Main(string[] args)
{
using (var stream = new MemoryStream())
{
var formatter = new BinaryFormatter();
// 创建一个代理选择器
var ss = new SurrogateSelector();
// 告诉代理选择器,针对于 DateTime 类型采用 UTCToLocal 代理类进行序列化/反序列化代理
ss.AddSurrogate(typeof(DateTime), formatter.Context, new UTCToLocalTimeSerializationSurrogate());
// 绑定代理选择器
formatter.SurrogateSelector = ss;
formatter.Serialize(stream,DateTime.Now);
stream.Position = 0;
var oldValue = new StreamReader(stream).ReadToEnd();
stream.Position = 0;
var newValue = (DateTime)formatter.Deserialize(stream);
Console.WriteLine(oldValue);
Console.WriteLine(newValue);
}
Console.ReadLine();
}
而一个代理选择器允许绑定多个代理类,选择器内部维护一个哈希表,通过 Type
与 StreamingContext
作为其键来进行搜索,通过 StreamintContext
地不同可以方便地为 DateTime
类型绑定不同用途的代理类。
十、反序列化对象时重写程序集/类型
通过继承 SerializationBinder
抽象类,我们可以很方便地实现类型反序列化时转化为不同的类型,该抽象类有一个 Type BindToType(String assemblyName,String typeName)
方法。
重写该方法你就可以在对象反序列化时,通过传入的两个参数来构造自己需要返回的真实类型。第一个参数是程序集名称,第二个参数是格式化器想要反序列化时转换的类型。
编写好 Binder 类重写该方法之后,在格式化器的 Binder
属性当中绑定你的 Binder 类即可。
【注意】
抽象类还有一个
BindToName()
方法,该方法是在序列化时被调用,会传入他想要序列化的类型。
《CLR Via C#》读书笔记:24.运行时序列化的更多相关文章
- 《C#高效编程》读书笔记02-用运行时常量(readonly)而不是编译期常量(const)
C#有两种类型的常量:编译期常量和运行时常量.两者有截然不同的行为,使用不当的话,会造成性能问题,如果没法确定,则使用慢点,但能保证正确的运行时常量. 运行时常量使用readonly关键字声明,编译期 ...
- 【C#进阶系列】24 运行时序列化
序列化是将对象或者对象图(一堆有包含关系的对象)转换成字节流的过程.而反序列化就是将字节流转为对象或对象图. 主要用于保存.传递数据,使得数据更易于加密和压缩. .NET内建了出色的序列化和反序列化支 ...
- 重温CLR(十八) 运行时序列化
序列化是将对象或对象图转换成字节流的过程,反序列化是将字节流转换回对象图的过程.在对象和字节流之间转换是很有用的机制. 1 应用程序的状态(对象图)可轻松保存到磁盘文件或数据库中,并在应用程序下次运行 ...
- CLR via C# 读书笔记---常量、字段、方法和参数
常量 常量是值从不变化的符号.定义常量符号时,它的值必须能在编译时确定.确定后,编译器将唱两只保存在程序集元数据中.使用const关键字声明常量.由于常量值从不变化,所以常量总是被视为类型定义的一部分 ...
- CLR via C#读书笔记一:CLR的执行模型
CLR(Common Language Runtime)公共语言进行时是一个可由多种编程语言使用的“进行时”. 将源代码编译成托管模块 可用支持CLR的任何语言创建源代码文件,然后用对应的编译器检查语 ...
- CLR via C# 读书笔记-21.托管堆和垃圾回收
前言 近段时间工作需要用到了这块知识,遂加急补了一下基础,CLR中这一章节反复看了好多遍,得知一二,便记录下来,给自己做一个学习记录,也希望不对地方能够得到补充指点. 1,.托管代码和非托管代码的区别 ...
- clr via c#读书笔记五:常量和字段
1.常量是值从不变化的符号.只能定义编译器识别的基元类型的常量.如:Boolean,Char,Byte,SByte,Int16,UInt16,Int32,UInt32,Int64,Single,Dou ...
- clr via c#读书笔记四:call、callvirt
1.嵌套类,就是定义在类中的类:嵌套类可以访问外部类的方法.属性.字段而不管访问修饰符的限制,但是外部类只能够访问修饰符为public.internal的嵌套类的字段.方法.属性: 2.CLR如何调用 ...
- CLR via c#读书笔记九:字符、字符串和文本处理
1.在.NET Framework中,字符总是表示成16位unicode代码值(关于unicode.utf8等可以到http://www.ruanyifeng.com/blog/2007/10/asc ...
随机推荐
- 根据select出来的数据进行update
update t_tbl_desc set num=b.num from t_tbl_desc a, (select distinct(name) as name,count(name) num fr ...
- 分享一个可以把 iOS/Android 应用的下载链接合成一个二维码的工具
芝麻二维码官网:https://www.hotapp.cn 1.在iOS系统设备扫描时 如果是微信扫描,因为第一步里使用了中间页面,此时无法直接跳转到App Store了,所以需要给出提示页面,提示用 ...
- hdu3307 欧拉函数
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3307 Description has only two Sentences Time Limit: 3 ...
- java多线程系列 目录
Java多线程系列1 线程创建以及状态切换 Java多线程系列2 线程常见方法介绍 Java多线程系列3 synchronized 关键词 Java多线程系列4 线程交互(wait和 ...
- 修改oracle的字符集操作方法
cmd环境下进行以下命令行的操作--连接sqlplus / as sysdba--命令行shutdown immediate; startup mount ALTER SYSTEM ENABLE RE ...
- Maven学习 六 pom.xml文件
java jar包的搜索网址:http://mvnrepository.com/ pom作为项目对象模型.通过xml表示maven项目,使用pom.xml来实现.主要描述了项目:包括配置文件:开发者需 ...
- 爬取baidu的明星的名称及头像
#!/1111111111usr/bin/env python# -*- encoding: utf-8 -*-# Created on 2018-11-15 15:24:12# Project: d ...
- (26)A delightful way to teach kids about computers
https://www.ted.com/talks/linda_liukas_a_delightful_way_to_teach_kids_about_computers/transcript00:1 ...
- 在idea中,mavne项目使用mybatis-generator-maven-plugin自动生成实体了的时候,在maven插件里面始终不显示
最近想学习mybatis的知识,自己搭了个简单的ssm框架,想通过插件自动生成实体类,发现想要的插件一直都没显示出来,着实很郁闷: pom.xm中的配置: <!--mybatis-generat ...
- CDR锁定方式
每个通道的PMA包括一个通道PLL可以配置成接收器CDR.还可以把通道1和4的PLL配置成CMU PLL用于发送器. CDR有两种锁定方式 1.Lock-to-Reference Mode(LTR) ...