浅谈序列化与反序列化的理解

1、  什么是序列化、反序列化?
引入
都玩过游戏么?应该知道游戏有一个存档的功能,我每次不想玩得时候就可以存档,然后再玩得时候我们根本不需要重新开始玩(要是每次都重新玩,估计就没有什么人能有足够的耐心玩游戏了),只需要读档就可以了。我们现在学习的事面向对象的思想,那么在我们眼中不管是我们的游戏角色还是游戏中的怪物、装备等等都可以看成是一个个的对象,进行简单的分析。
角色对象(包含等级、性别、经验值、HPMP等等属性)
武器对象(包含武器的类型、武器的伤害、武器附加的能力值等等属性)
怪物对象(包含等级、经验值、攻击、怪物类型等等)
于是玩游戏过程变的非常有意思了,创建游戏角色就好像是创建了一个角色对象,拿到武器就好像创建了一个武器对象,遇到的怪物、NPC等等都是对象了。
然后再用学过的知识进行分析,我们发现对象的数据都是保存在内存中的,应该都知道内存的数据在断电以后是会消失的,但是我们的游戏经过存档以后,就算你关机了几天,再进入游戏的时候,读取你的存档发现你在游戏中的一切都还在呢,奇怪了,明明内存中的数据已经没有了啊,这是为什么呢?于是再仔细考虑,电脑中有硬盘这个东西在断电以后保存的数据是不会丢的(要是由于断电导致的硬盘损坏了,没有数据了,哈哈,不在此考虑中)。那么应该很容易的想到这些数据是被保存在硬盘中了。没错!这就是对象的持久化,也就是我们今天要讲的对象的序列化。那么反序列化就很好理解了就是将存放在硬盘中的信息再读取出来形成对象。
序列化/反序列化的概念:
序列化是指将对象实例的状态存储到存储媒体的过程
反序列化是指将存储在存储媒体中的对象状态装换成对象的过程

(注:上面可以拿一个自己熟悉的、或者最近比较热门的游戏来举例)
2、  序列化/反序列化的基本原理
我们知道硬盘中保存数据都是以文件的形式,那么我们要想将对象的状态保存在硬盘中就必须要使用文件的形式,也就是将这些对象的属性存入到文件中,于是就需要操作文件,同理要想得到存储在文件中的对象的信息,还是需要操作文件,而操作文件肯定要用到流,于是文件流的操作成了序列化/反序列化的主要技术之一。
另外一个要用到的技术就使反射,在反序列化的时候,存在文件中的只不过是字符,而不是对象了,那么我们在生成对象的时候就不知道该创建什么类型的对象的,那么只有根据在文件中保存的类型的名称来创建类型,这就使反射。
基本原理:
序列化是采用流的方式将对象的状态存储在文件中。
反序列化就是采用流的方式将文件中的数据读出,并且根据读取的类型的名称,经过反射形成类,然后再将读取到的状态数据赋值给创建的对象。
3、  基本序列化
命名空间
System.Runtime.Serialization

示例
要使一个类可序列化,最简单的方法是使用 Serializable 属性对它进行标记,如下所示:
[Serializable]
public class MyObject {
  public int n1 = 0;
  public int n2 = 0;
  public String str = null;
}
以下代码片段说明了如何将此类的一个实例序列化为一个文件:
1MyObject obj = new MyObject(); 
2obj.n1 = 1
3obj.n2 = 24
4obj.str = "一些字符串"
5IFormatter formatter = new BinaryFormatter(); 
6Stream stream = new FileStream("MyFile.bin", FileMode.Create, FileAccess.Write, FileShare.None); 
7formatter.Serialize(stream, obj); 
8stream.Close(); 

本例使用二进制格式化程序进行序列化。您只需创建一个要使用的流和格式化程序的实例,然后调用格式化程序的 Serialize 方法。流和要序列化的对象实例作为参数提供给此调用。类中的所有成员变量(甚至标记为 private 的变量)都将被序列化,但这一点在本例中未明确体现出来。在这一点上,二进制序列化不同于只序列化公共字段的 XML 序列化程序。
将对象还原到它以前的状态也非常容易。首先,创建格式化程序和流以进行读取,然后让格式化程序对对象进行反序列化。以下代码片段说明了如何进行此操作。
 1IFormatter formatter = new BinaryFormatter(); 
 2Stream stream = new FileStream("MyFile.bin", FileMode.Open, 
 3FileAccess.Read, FileShare.Read); 
 4MyObject obj = (MyObject) formatter.Deserialize(fromStream); 
 5stream.Close(); 
 6// 下面是证明 
 7Console.WriteLine("n1: {0}", obj.n1); 
 8Console.WriteLine("n2: {0}", obj.n2); 
 9Console.WriteLine("str: {0}", obj.str); 
10

二进制序列化的特点:

效率很高,能生成非常紧凑的字节流
注意:对对象进行反序列化时并不调用构造函数
补充:
需要注意的是,无法继承 Serializable 属性。如果从 MyObject派生出一个新的类,则这个新的类也必须使用该属性进行标记,否则将无法序列化。例如,如果试图序列化以下类实例,将会显示一个SerializationException,说明 MyStuff 类型未标记为可序列化。
public class MyStuff : MyObject
{
  public int n3;
}
4、  选择序列化
通过上面的内容,我们已经知道了序列化是什么意思,而且还使用了一个例子来完成对象的序列化,但是有的时候并不是对象的所有的属性都需要被保存的。比如,我们在玩游戏的时候角色正在攻击某个怪物,然后你选择存档,退出游戏,或者突然停电了,那么我们再上游戏的时候,这个怪物肯定已经不在了,也就是说在角色对象中的攻击目标的属性并没有序列化,那么这中情况我们就叫做对象的选择序列化,也就是我们只是选择性的保存对象一部分的属性。
可以通过使用 NonSerialized 属性标记成员变量来防止它们被序列化,如下所示:
[Serializable]
public class MyObject
{
  public int n1;
  [NonSerialized] public int n2;
  public String str;
}
那么对象的成员n2的状态就不会被保存
扩展内容
1、自定义序列化(补充内容)
可以通过在对象上实现 ISerializable接口来自定义序列化过程。这一功能在反序列化后成员变量的值失效时尤其有用,但是需要为变量提供值以重建对象的完整状态。要实现ISerializable,需要实现 GetObjectData方法以及一个特殊的构造函数,在反序列化对象时要用到此构造函数。以下代码示例说明了如何在前一部分中提到的 MyObject 类上实现 ISerializable
 1[Serializable] 
 2public class MyObject : ISerializable 
 3
 4  public int n1; 
 5  public int n2; 
 6  public String str; 
 7  public MyObject() 
 8  
 9  }
 
10  protected MyObject(SerializationInfo info, StreamingContext context) 
11  
12    n1 = info.GetInt32("i"); 
13    n2 = info.GetInt32("j"); 
14    str = info.GetString("k"); 
15  }
 
16  public virtual void GetObjectData(SerializationInfo info, StreamingContext context) 
17  
18    info.AddValue("i", n1); 
19    info.AddValue("j", n2); 
20    info.AddValue("k", str); 
21  }
 
22}
 
23

在序列化过程中调用 GetObjectData 时,需要填充方法调用中提供的 SerializationInfo对象。只需按名称/值对的形式添加将要序列化的变量。其名称可以是任何文本。只要已序列化的数据足以在反序列化过程中还原对象,便可以自由选择添加至SerializationInfo 的成员变量。如果基对象实现了 ISerializable,则派生类应调用其基对象的 GetObjectData方法。需要强调的是,将 ISerializable 添加至某个类时,需要同时实现 GetObjectData 以及特殊的构造函数。如果缺少 GetObjectData,编译器将发出警告。但是,由于无法强制实现构造函数,所以,缺少构造函数时不会发出警告。如果在没有构造函数的情况下尝试反序列化某个类,将会出现异常。在消除潜在安全性和版本控制问题等方面,当前设计优于 SetObjectData 方法。例如,如果将 SetObjectData 方法定义为某个接口的一部分,则此方法必须是公共方法,这使得用户不得不编写代码来防止多次调用 SetObjectData 方法。可以想象,如果某个对象正在执行某些操作,而某个恶意应用程序却调用此对象的 SetObjectData 方法,将会引起一些潜在的麻烦。

在反序列化过程中,使用出于此目的而提供的构造函数将 SerializationInfo 传递给类。对象反序列化时,对构造函数的任何可见性约束都将被忽略,因此,可以将类标记为 publicprotectedinternal private。一个不错的办法是,在类未封装的情况下,将构造函数标记为 protect。如果类已封装,则应标记为 private。要还原对象的状态,只需使用序列化时采用的名称,从 SerializationInfo 中检索变量的值。如果基类实现了 ISerializable,则应调用基类的构造函数,以使基础对象可以还原其变量。如果从实现了 ISerializable 的类派生出一个新的类,则只要新的类中含有任何需要序列化的变量,就必须同时实现构造函数以及 GetObjectData 方法。以下代码片段显示了如何使用上文所示的 MyObject 类来完成此操作。
 1[Serializable] 
 2public class ObjectTwo : MyObject 
 3
 4  public int num; 
 5  public ObjectTwo() : base() 
 6  
 7  }
 
 8  protected ObjectTwo(SerializationInfo si, StreamingContext context) : base(si,context) 
 9  
10    num = si.GetInt32("num"); 
11  }
 
12  public override void GetObjectData(SerializationInfo si, StreamingContext context) 
13  
14    base.GetObjectData(si,context); 
15    si.AddValue("num", num); 
16  }
 
17}
 

切记要在反序列化构造函数中调用基类,否则,将永远不会调用基类上的构造函数,并且在反序列化后也无法构建完整的对象。
2XML序列化
    略...

有兴趣可以使用序列化/反序列化模拟一个游戏的存档和读档功能。

posted on 2008-03-18 09:06  wsmall  阅读(1582)  评论(0编辑  收藏  举报

导航