本文将从这两个格式器入手,先向大家介绍分别用它们如何实现序列化和反序列化,然后比较两种格式器的不同点。接着我会向大家介绍实现序列化对对象类型的一些要求,同时还要向大家介绍两种不同的序列化方式:基本序列化(Basic Serialization)和自定义序列化(Custom Serialization)。最后,我还会给大家介绍一个实例程序以加深大家对序列化机制的理解程度。
程序员在编写应用程序的时候往往要将程序的某些数据存储在内存中,然后将其写入某个文件或是将它传输到网络中的另一台计算机上以实现通讯。这个将程序数据转化成能被存储并传输的格式的过程被称为"序列化"(Serialization),而它的逆过程则可被称为"反序列化"(Deserialization)。
.Net框架对序列化机制具有非常好的支持,它提供了两个名字空间(namespace):System.Runtime.Serialization和System.Runtime.Serialization.Formatters以完成序列化机制的大部分功能。系列化这项技术可以应用在将程序产生的结果数据存储到文件系统中,但是它更主要的应用是在于.Net Remoting和Web服务的实现上。
序列化机制的实现是依靠格式器(Formatter)而完成的,它是一个从System.Runtime.Serialization.IFormatter继承下来的类的对象。格式器完成了将程序数据转化到能被存储并传输的格式的工作,同时也完成了将数据转化回来的工作。.Net框架为程序员提供了两种类型的格式器,一种通常是应用于桌面类型的应用程序的,它一个是System.Runtime.Serialization.Formatters.Binary.BinaryFormatter类的对象,而另一种则更主要的应用于.Net Remoting和XML Web服务等领域的,它一个是System.Runtime.Serialization.Formatters.Soap.SoapFormatter类的对象。从它们的名称来看,我们不妨将它们分别称为二进制格式器和XML格式器。
一、二进制格式器 vs XML格式器
下面我先向大家介绍两种不同的格式器,分别用它们如何实现序列化机制和反序列化机制,请看下面的代码:
#region Binary Serializers public static System.IO.MemoryStream SerializeBinary(object request) { System.Runtime.Serialization.Formatters.Binary.BinaryFormatter serializer = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); System.IO.MemoryStream memStream = new System.IO.MemoryStream(); serializer.Serialize(memStream, request); return memStream; } public static object DeSerializeBinary(System.IO.MemoryStream memStream) { memStream.Position=0; System.Runtime.Serialization.Formatters.Binary.BinaryFormatter deserializer = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); object newobj = deserializer.Deserialize(memStream); memStream.Close(); return newobj; } #endregion #region XML Serializers public static System.IO.MemoryStream SerializeSOAP(object request) { System.Runtime.Serialization.Formatters.Soap.SoapFormatter serializer = new System.Runtime.Serialization.Formatters.Soap.SoapFormatter(); System.IO.MemoryStream memStream = new System.IO.MemoryStream(); serializer.Serialize(memStream, request); return memStream; } public static object DeSerializeSOAP(System.IO.MemoryStream memStream) { object sr; System.Runtime.Serialization.Formatters.Soap.SoapFormatter deserializer = new System.Runtime.Serialization.Formatters.Soap.SoapFormatter(); memStream.Position=0; sr = deserializer.Deserialize(memStream); memStream.Close(); return sr; } #endregion
从上面的代码我们可以发现无论运用哪种格式器,其基本的过程都是一样的,而且都是非常容易实现的,唯一的不同就是定义格式器的类型不同。不过在实际的应用中,二进制格式器往往应用于一般的桌面程序和网络通讯程序中,而XML格式器禀承了XML技术的优点,大多数被应用于.Net Remoting和XML Web服务等领域。下面我们来分析一下两种格式器各自的优点。
二进制序列化的优点:
1. 所有的类成员(包括只读的)都可以被序列化;
2. 性能非常好。
XML序列化的优点:
1. 互操作性好;
2. 不需要严格的二进制依赖;
3. 可读性强。
通过分析上面的代码,我们知道了选择二进制序列化的方式还是选择XML序列化的方式仅仅是对不同的格式器进行选择而已。你可以根据实际的需要选择相应的格式器完成序列化和反序列化工作。同时请注意,代码中的序列化函数和反序列化函数仅仅是在调用Serialize()和Deserialize()这两个核心函数上产生了差别,即它们的参数不同。因此以上的代码完成了一些最最基本但是很重要的功能,你可以将它们运用在你的程序中,或是将其进行适当扩充以满足程序的特定需要。
二、序列化机制对类的要求
如果你要对一个对象进行序列化,那么你必须将它的类型标记为[Serializable()],该操作是通过SerializableAttribute属性来实现的。将SerializableAttribute属性应用于一种数据类型可表明该数据类型的实例可以被序列化。如果正在序列化的对象图中的任何类型未应用SerializableAttribute属性,公共语言运行库则会引发SerializationException。默认情况下,类型中由SerializableAttribute标记的所有公共和私有字段都会进行序列化,除非该类型实现ISerializable接口来重写序列化进程(通过实现该接口我们便可以实现将在后面介绍的"自定义序列化")。默认的序列化进程会排除用NonSerializedAttribute属性标记的字段,即你可以将该类型标记为[NonSerialized()]以表明它是不可以被序列化的。如果可序列化类型的字段包含指针、句柄或其他某些针对于特定环境的数据结构,并且不能在不同的环境中以有意义的方式重建,则最好将NonSerializedAttribute属性应用于该字段。有关序列化的更多信息,请参阅System.Runtime.Serialization名字空间中的相关内容。
下面我给大家介绍一个例子,以显示如何正确的运用SerializableAttribute属性和NonSerializedAttribute属性。该程序中运用到了XML格式器,不过同时给出了二进制格式器为参考(程序中将其用"//"标注),其实现的结果是一样的。该程序实现的功能是在序列化和反序列化操作前后测试对象因包含了[NonSerialized()]的字段而显示不同的屏幕打印结果。其代码如下:
三、基本序列化、自定义序列化
.Net框架为我们提供了两种方式的序列化:一种为基本序列化、另一种为自定义序列化。值得注意的是,序列化的方式和前面提到的序列化的格式是不同的概念。序列化的方式是指.Net框架将程序的数据转化为能被存储并传输的格式的实际过程,它是不管程序员运用了何种类型的格式器的(二进制格式器还是XML格式器)。而序列化的格式则指程序的数据是被转化成二进制格式了还是被转化成XML格式了。
完成序列化的最简单的方法便是让.Net框架自动为我们完成整个过程,而我们不必去管它内部是如何具体实现的,这种方法便是前面提到的"基本序列化"。在这种方式下,我们需要做的仅仅是将类标记上[Serializable()]属性。然后.Net框架便调用该类的对象并将它转化为所需的格式。同时你还可以控制其中的某些字段不被序列化,方法就是前面所述的将该字段标记上[NonSerialized()]属性。这样,最最简单和基本的序列化工作就完成了,不过其内部是如何实现的你是不得而知的,同时你也不能进一步控制序列化过程的程序行为。
如果你要获得对序列化的更大的控制权,那么你就得使用"自定义序列化"的方式。通过使用这种方式,你可以完全的控制类的哪些部分能被序列化而哪些部分不能,同时你还可以控制如何具体的进行序列化。运用该方式的好处就是能克服基本序列化所会遇到的问题。我们在运用基本序列化将一个类的对象序列化完毕并存储在文件中后,假设该对象原来有三个字段,如果此时该对象增加了一个字段,那么再将该对象从文件中反序列化出来时会发生字段数不一致的错误。这样的问题是基本序列化所不能解决的,只能运用自定义序列化的方式来解决。
在介绍自定义序列化之前,我先给出介绍过程中所要用到的实例程序的代码。这是一个时间安排程序,其中要用到将不同的时间格式进行转化的操作。所以运用序列化的机制能很好的解决这个问题。
using System; using System.Runtime.Serialization; namespace SerializationSample { [Serializable()] public class Schedule { protected System.DateTime start; protected System.DateTime end; // 每个时间间隔所要增加的毫秒数 protected long interval; public System.DateTime Start {get{return start;}set{start=value;}} public System.DateTime End {get{return end;}set{end=value;}} public long Interval {get{return interval;}set{interval=value;}} public Schedule(System.DateTime Start, System.DateTime End, long Interval) { start=Start; end=End; interval=Interval; } // 如果已经到了结束的时间,则返回结束时间,否则返回下一次运行的时间 public System.DateTime NextRunTime { get { System.TimeSpan ts = new System.TimeSpan(end.Ticks-System.DateTime.Now.Ticks); if(ts.Milliseconds>0) { return System.DateTime.Now.AddMilliseconds(interval); } else { return end; } } } } }
自定义序列化:
下面我就向大家介绍自定义序列化以及反序列化的具体过程。首先,程序的类必须实现System.Runtime.Serialization.ISerializable接口,该接口的功能就是允许对象控制其自己的序列化和反序列化过程。所以我们得重新定义上面的类:
[Serializable()]
public class ScheduleCustom : System.Runtime.Serialization.ISerializable
接下来,我们必须对该接口调用GetObjectData()的实现,也即我们必须在上面的类中给出GetObjectData()的具体实现。其函数原型如下:
void GetObjectData(SerializationInfo info, StreamingContext context);
上面的类中GetObjectData()的具体实现如下:
public void GetObjectData(SerializationInfo info,StreamingContext context) { // 运用info对象来添加你所需要序列化的项 info.AddValue("start", start); info.AddValue("end", end); info.AddValue("interval", interval); }
然而对于这么一个简单的方法,读者可能不能理会到系列化带来的强大功能,所以下面我就给这个方法添加一些东西。如果在系列化过程中我们要查看类型为DateTime的"start"属性的输出的话,其结果会是.Net框架默认的格式:
而对于没有.Net框架的用户,或是在其他时间区域内的用户而言,这么一个格式的时间可能是非常难以理解的,所以我们有必要将时间的格式转化为格林威治标准时间格式,于是修改GetObjectData()方法如下:
public void GetObjectData(SerializationInfo info,StreamingContext context) { // 运用info对象来添加你所需要序列化的项 // 同时,将"start"和"end"属性的时间格式转化为格林威治标准时间格式 info.AddValue("start", System.TimeZone.CurrentTimeZone.ToUniversalTime(start)); info.AddValue("end", System.TimeZone.CurrentTimeZone.ToUniversalTime(end)); info.AddValue("interval", interval); info.AddValue("timeformat", "utc"); }
这样一来,我们在系列化过程中查看"start"属性时就会得到如下结果:
同时请注意我们在GetObjectData()方法中添加的一个名为"timeformat"的额外属性,通过它我们可以方便的知道系列化过程中所使用的时间格式。如果有兴趣的话,你还可以从System.Globalization.DateTimeFormatInfo这个名字空间中获取更多有关时间格式的信息。
自定义反序列化:
你可以通过调用一个自定义的构造函数来完成自定义反序列化的操作。该构造函数的定义如下:
public ScheduleCustom (SerializationInfo info,StreamingContext context);
在上面的类中,我们的ScheduleCustom()方法将完成把时间格式从格林威治标准时间格式反序列化为当地时间的格式的操作,其函数实现如下:
public ScheduleCustom (SerializationInfo info,StreamingContext context) {
this.start = info.GetDateTime("start").ToLocalTime();
this.end = info.GetDateTime("end").ToLocalTime();
this.interval = info.GetInt32("interval");
}
在完成自定义序列化和自定义反序列化后,我们的时间安排程序变成了如下形式:
using System; using System.Runtime.Serialization; namespace SerializationSample { [Serializable()] public class ScheduleCustom : System.Runtime.Serialization.ISerializable { protected System.DateTime start; protected System.DateTime end; // 每个时间间隔所要增加的毫秒数 protected long interval; public System.DateTime Start {get{return start;}set{start=value;}} public System.DateTime End {get{return end;}set{end=value;}} public long Interval {get{return interval;}set{interval=value;}} public ScheduleCustom(System.DateTime Start, System.DateTime End, long Interval) { start=Start; end=End; interval=Interval; } // 如果已经到了结束的时间,则返回结束时间,否则返回下一次运行的时间 public System.DateTime NextRunTime { get { System.TimeSpan ts = new System.TimeSpan(end.Ticks-System.DateTime.Now.Ticks); if(ts.Milliseconds>0) { return System.DateTime.Now.AddMilliseconds(interval); } else { return end; } } } public void GetObjectData(SerializationInfo info,StreamingContext context) { // 运用info对象来添加你所需要序列化的项 // 同时,将"start"和"end"属性的时间格式转化为格林威治标准时间格式 info.AddValue("start", System.TimeZone.CurrentTimeZone.ToUniversalTime(start)); info.AddValue("end", System.TimeZone.CurrentTimeZone.ToUniversalTime(end)); info.AddValue("interval", interval); info.AddValue("timeformat", "utc"); } public ScheduleCustom (SerializationInfo info,StreamingContext context) { this.start = info.GetDateTime("start").ToLocalTime(); this.end = info.GetDateTime("end").ToLocalTime(); this.interval = info.GetInt32("interval"); } } }
总结以上内容,前三部分向大家介绍了.Net框架下系列化机制的一些基本概念和基本的运用方法,读者在读完本文后,应该对以下几个概念有个初步了解:二进制系列化、XML系列化、基本序列化和自定义系列化,并应能够完成一些基本的系列化应用。最后,希望大家能合理有效的运用系列化机制并发挥它的功效以更好地满足实际工作需要。
参考代码1
可以使用.net提供的序列化和反序列化方法来实现,你可将对象序列化成XML字符串,然后存入数据库中,当你要使用对象的时候,再把数据库中保存字符串反序列化成对象就可以使用了,以下为示例代码:
public class Cat { public string Color { get; set; } public int Speed { get; set; } public string Name{ get; set; } } //序列化 var cat1=new Cat{Color="Write",Speed=50,Name="MiMi" }; XmlSerializer ser = new XmlSerializer(typeof(Cat)); MemoryStream ms = new MemoryStream(); ser.Serialize(ms, cat1); string xmlString = Encoding.UTF8.GetString(ms.ToArray()); //xmlString就是你要保存到数据库的字符串 //反序列化 XmlSerializer dser = new XmlSerializer(typeof(Cat)); //xmlString是你从数据库获取的字符串 Stream xmlStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlString)); Cat cat2=dser.Deserialize(xmlStream) as Cat;//cat2 就是你要得到的class对象
参考代码2
创建项目
1. 添加一个名为RWTest的表到 SQL Server MYTest 数据库。 表字段设置如下:
a. 唯一标识字段名称为"ID",类型为Int。
b. 名称为"Description"的VarChar类型的字段,字段长度为50。
c. 名称为"Data" 的varbinary(Max) 类型的字段。
2. 启动 Visual Studio .NET, 并创建一个新的 Visual C# Windows 应用程序项目。
3. 从工具栏中拖两个Button 控件到默认窗体, Form1。
4. 在属性窗口中修改Name为buttonFileToDB, Text 属性为从文件保存到数据库, 然后修改Name为buttonDBToFile ,Text 属性为从数据库保存到文件。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using System.Data.SqlClient; using System.IO; using System.Collections; using System.Runtime.Serialization.Formatters.Binary; //数据库说明:MyTest数据库,RWTest表,包含3列:ID(int),Description(varchar(50), Data(varbinary(max)) namespace RWArrayListSQL { public partial class Form1 : Form { ArrayList arrayList = new ArrayList(); public Form1() { InitializeComponent(); for (int i = 0; i < 100; i++) { PTData ptData = new PTData(); ptData.PTID = i + 1; ptData.PTName = Convert.ToString(i + 1); ptData.PT_Data = i + 1; arrayList.Add(ptData); } } private void buttonFileToDB_Click(object sender, EventArgs e) { SqlConnection sqlConnection = new SqlConnection("Data Source=liuxueqin;Initial Catalog=MyTest;Integrated Security=True"); SqlDataAdapter sqlDataAdapter = new SqlDataAdapter("Select * from RWTest", sqlConnection); SqlCommandBuilder sqlCommandBuilder = new SqlCommandBuilder(sqlDataAdapter); System.Data.DataSet dataSet = new DataSet("RWTest"); sqlDataAdapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;//确定现有 DataSet 架构与传入数据不匹配时需要执行的操作。 //定义一个流 Stream stream = new MemoryStream(); //定义一个格式化器 BinaryFormatter bf = new BinaryFormatter(); foreach (object obj in arrayList) { bf.Serialize(stream, obj); //序列化 } byte[] array = null; array = new byte[stream.Length]; //将二进制流写入数组 stream.Position = 0; stream.Read(array, 0, (int)stream.Length); //关闭流 stream.Close(); try { sqlDataAdapter.Fill(dataSet, "RWTest"); //DataRow表示DataTable中的一行数据 System.Data.DataRow dataRow1; dataRow1 = dataSet.Tables["RWTest"].NewRow(); dataRow1["ID"] = 1; dataRow1["Description"] = "This would be description text"; dataRow1["Data"] = array; dataSet.Tables["RWTest"].Rows.Add(dataRow1); sqlDataAdapter.Update(dataSet, "RWTest"); sqlConnection.Close(); MessageBox.Show("写入数据库成功!", " 信息提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { if (sqlConnection.State == ConnectionState.Open) { sqlConnection.Close(); } MessageBox.Show("写入数据库失败"+ex.Message, " 信息提示", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void buttonDBToFile_Click(object sender, EventArgs e) { SqlConnection sqlConnection = new SqlConnection("Data Source=liuxueqin;Initial Catalog=MyTest;Integrated Security=True"); SqlDataAdapter sqlDataAdapter = new SqlDataAdapter("Select * from RWTest", sqlConnection); SqlCommandBuilder sqlCommandBuilder = new SqlCommandBuilder(sqlDataAdapter); DataSet dataSet = new DataSet("RWTest"); sqlDataAdapter.Fill(dataSet, "RWTest"); DataRow myRow; myRow = dataSet.Tables["RWTest"].Rows[0]; byte[] b = null; b = (byte[])myRow["Data"]; //定义一个流 MemoryStream stream = new MemoryStream(b); //定义一个格式化器 BinaryFormatter bf = new BinaryFormatter(); while (stream.Position != stream.Length) { arrayList.Add(bf.Deserialize(stream)); //反序列化 } stream.Close(); for(int i=0;i<5;i++) //信息提示,是否正确从数据库中取出了ArrayList链表 MessageBox.Show(((PTData)arrayList[i]).PTName, "信息提示", MessageBoxButtons.OK, MessageBoxIcon.Information); if (sqlConnection.State == ConnectionState.Open) { sqlConnection.Close(); } MessageBox.Show(" 从数据库读出数据成功!", " 信息提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } } //在类的上面增加了属性:Serializable.(如果不加这个属性,将抛出SerializationException异常)。在不继承自接口ISerializable的情况下,通过增加[Serializable]属性可以允许该类可以被序列化。 [Serializable] class PTData { public PTData() { PTID = 1; PTName = "None"; PT_Data = 1.0; } public int PTID; public string PTName; public double PT_Data; } }
参考文章
Crazy Coder, C#实现将一个类序列化存储在数据库中