一、什么是运行时序列化

序列化的作用就是将对象图(特定时间点的对象连接图)转换为字节流,这样这些对象图就可以在文件系统/网络进行传输。

二、序列化/反序列化快速入门

一般来说我们通过 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 签名一样的委托,并且在反序列化注册到当前 AppDomainAssemblyResolve 事件。

这样当程序集加载失败的时候,你可以在该方法内部根据传入的事件参数与程序集标识自己使用 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。

序列化与反序列化的四个生命周期特性

通过 OnSerializingOnSerializedOnDeserializingOnDeserialized 这四个特性,我们可以在对象序列化与反序列化时进行一些自定义的控制。只需要将这四个特性分别加在四个方法上面即可,但是针对方法签名必须返回值为 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 工具类来进行序列化操作的。

  1. 格式化器调用 FormatterService.GetSerializableMembers() 方法获得需要序列化的字段构成的 MemberInfo 数组。
  2. 格式化器调用 FormatterService.GetObjectData() 方法,通过之前获取的字段 MethodInfo 信息来取得每个字段存储的值数组。该数组与字段信息数组是并行的,下标一致。
  3. 格式化器写入类型的程序集等信息。
  4. 遍历两个数组,写入字段信息与其数据到流当中。

反序列化操作的步骤与上面相反。

  1. 首先从流头部读取程序集标识与类型信息,如果当前 AppDomain 没有加载该程序集会抛出异常。如果类型的程序集已经加载,则通过 FormatterServices.GetTypeFromAssembly() 方法来构造一个 Type 对象。
  2. 格式化器调用 FormatterService.GetUninitializedObject() 方法为新对象分配内存,但是 不会调用对象的构造器
  3. 格式化器通过 FormatterService.GetSerializableMembers() 初始化一个 MemberInfo 数组。
  4. 格式化器根据流中的数据创建一个 Object 数组,该数组就是字段的数据。
  5. 格式化器通过 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 提供了 FullTypeNameAssemblyName,不过一般推荐使用该对象提供的 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() 方法内部通过 SerializationInfoSetType() 方法变更了序列化的目标类型。

下面的代码演示了如何序列化一个单例对象:

[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();
}

而一个代理选择器允许绑定多个代理类,选择器内部维护一个哈希表,通过 TypeStreamingContext 作为其键来进行搜索,通过 StreamintContext 地不同可以方便地为 DateTime 类型绑定不同用途的代理类。

十、反序列化对象时重写程序集/类型

通过继承 SerializationBinder 抽象类,我们可以很方便地实现类型反序列化时转化为不同的类型,该抽象类有一个 Type BindToType(String assemblyName,String typeName) 方法。

重写该方法你就可以在对象反序列化时,通过传入的两个参数来构造自己需要返回的真实类型。第一个参数是程序集名称,第二个参数是格式化器想要反序列化时转换的类型。

编写好 Binder 类重写该方法之后,在格式化器的 Binder 属性当中绑定你的 Binder 类即可。

【注意】

抽象类还有一个 BindToName() 方法,该方法是在序列化时被调用,会传入他想要序列化的类型。

《CLR Via C#》读书笔记:24.运行时序列化的更多相关文章

  1. 《C#高效编程》读书笔记02-用运行时常量(readonly)而不是编译期常量(const)

    C#有两种类型的常量:编译期常量和运行时常量.两者有截然不同的行为,使用不当的话,会造成性能问题,如果没法确定,则使用慢点,但能保证正确的运行时常量. 运行时常量使用readonly关键字声明,编译期 ...

  2. 【C#进阶系列】24 运行时序列化

    序列化是将对象或者对象图(一堆有包含关系的对象)转换成字节流的过程.而反序列化就是将字节流转为对象或对象图. 主要用于保存.传递数据,使得数据更易于加密和压缩. .NET内建了出色的序列化和反序列化支 ...

  3. 重温CLR(十八) 运行时序列化

    序列化是将对象或对象图转换成字节流的过程,反序列化是将字节流转换回对象图的过程.在对象和字节流之间转换是很有用的机制. 1 应用程序的状态(对象图)可轻松保存到磁盘文件或数据库中,并在应用程序下次运行 ...

  4. CLR via C# 读书笔记---常量、字段、方法和参数

    常量 常量是值从不变化的符号.定义常量符号时,它的值必须能在编译时确定.确定后,编译器将唱两只保存在程序集元数据中.使用const关键字声明常量.由于常量值从不变化,所以常量总是被视为类型定义的一部分 ...

  5. CLR via C#读书笔记一:CLR的执行模型

    CLR(Common Language Runtime)公共语言进行时是一个可由多种编程语言使用的“进行时”. 将源代码编译成托管模块 可用支持CLR的任何语言创建源代码文件,然后用对应的编译器检查语 ...

  6. CLR via C# 读书笔记-21.托管堆和垃圾回收

    前言 近段时间工作需要用到了这块知识,遂加急补了一下基础,CLR中这一章节反复看了好多遍,得知一二,便记录下来,给自己做一个学习记录,也希望不对地方能够得到补充指点. 1,.托管代码和非托管代码的区别 ...

  7. clr via c#读书笔记五:常量和字段

    1.常量是值从不变化的符号.只能定义编译器识别的基元类型的常量.如:Boolean,Char,Byte,SByte,Int16,UInt16,Int32,UInt32,Int64,Single,Dou ...

  8. clr via c#读书笔记四:call、callvirt

    1.嵌套类,就是定义在类中的类:嵌套类可以访问外部类的方法.属性.字段而不管访问修饰符的限制,但是外部类只能够访问修饰符为public.internal的嵌套类的字段.方法.属性: 2.CLR如何调用 ...

  9. CLR via c#读书笔记九:字符、字符串和文本处理

    1.在.NET Framework中,字符总是表示成16位unicode代码值(关于unicode.utf8等可以到http://www.ruanyifeng.com/blog/2007/10/asc ...

随机推荐

  1. 根据select出来的数据进行update

    update t_tbl_desc set num=b.num from t_tbl_desc a, (select distinct(name) as name,count(name) num fr ...

  2. 分享一个可以把 iOS/Android 应用的下载链接合成一个二维码的工具

    芝麻二维码官网:https://www.hotapp.cn 1.在iOS系统设备扫描时 如果是微信扫描,因为第一步里使用了中间页面,此时无法直接跳转到App Store了,所以需要给出提示页面,提示用 ...

  3. hdu3307 欧拉函数

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3307 Description has only two Sentences Time Limit: 3 ...

  4. java多线程系列 目录

    Java多线程系列1 线程创建以及状态切换    Java多线程系列2 线程常见方法介绍    Java多线程系列3 synchronized 关键词    Java多线程系列4 线程交互(wait和 ...

  5. 修改oracle的字符集操作方法

    cmd环境下进行以下命令行的操作--连接sqlplus / as sysdba--命令行shutdown immediate; startup mount ALTER SYSTEM ENABLE RE ...

  6. Maven学习 六 pom.xml文件

    java jar包的搜索网址:http://mvnrepository.com/ pom作为项目对象模型.通过xml表示maven项目,使用pom.xml来实现.主要描述了项目:包括配置文件:开发者需 ...

  7. 爬取baidu的明星的名称及头像

    #!/1111111111usr/bin/env python# -*- encoding: utf-8 -*-# Created on 2018-11-15 15:24:12# Project: d ...

  8. (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 ...

  9. 在idea中,mavne项目使用mybatis-generator-maven-plugin自动生成实体了的时候,在maven插件里面始终不显示

    最近想学习mybatis的知识,自己搭了个简单的ssm框架,想通过插件自动生成实体类,发现想要的插件一直都没显示出来,着实很郁闷: pom.xm中的配置: <!--mybatis-generat ...

  10. CDR锁定方式

    每个通道的PMA包括一个通道PLL可以配置成接收器CDR.还可以把通道1和4的PLL配置成CMU PLL用于发送器. CDR有两种锁定方式 1.Lock-to-Reference Mode(LTR) ...