序列化
基本操作
本来楼主是想学点安卓的知识的,楼主发现学习一门新知识需要付出的代价太大了,加上学的太多,就没有一门精通的了,所以楼主暂时的想法是先把关于.NET的知识学透了.
流属于一种基础的概念和功能,就好像string和int这些基元类型一样,在很多场合下都会用到.在前面通过文件复制,读取文件内容的实例讲述了流的基本概念和操作.本节将会讲述他的另一个应用场景,序列化和反序列化.
当程序运行时,需要访问和处理数据,在面向对象编程中,这些数据通常保存在对象中;当程序关闭或者销毁时,这些数据需要保存到某处以便日后重建对象时能够还原对象的状态.在很多应用程序中,都会选择使用数据库用作数据存储,但是如果没有数据库,如何将对象机器状态保存起来,以便下次运行程序时使用呢?
将对象及其状态保存起来,就称为序列化,最简单和常见的一种情况就是将对象以及状态保存到文件中;而反序列化不用我说了吧,将文件还原为对象.”序列化”这个词很抽象,我刚开始也不是很理解.不着急,慢慢来.
序列化的过程就是将前面流操作的byte[]数组替换成了对象,转换的过程就是将对象变为一个个的字节序列,这个字节序列可以是人们不可读的二进制文件,也可以是人类可读的文本文件.
.NET提供了一个接口System.Runtime.Serialization.IFormatter,定义了实现序列化和反序列化的操作,并且提供了两个实现了这个接口的类:BinaryForamtter和SoapFormatter.
BinaryFormatter位于System.Runtime.Serialization.Formatter.Binary命名空间下,用于将对象序列化为二进制数据;SoapFormatter位于System.Runtime.Serialization.Formatter.Soap命名空间下,用于将对象序列化为人类直接可读的文件,这个文本使用SOAP来描述的.SOAP的全称为简单对象访问协议,是一种轻量级的,基于XML协议.有了BinaryFormatter和SoapFormatter这两个类,开发者使用它们就可以应付大多数的情况,不用去创建一个类型来实现这个接口.IFormatter中最重要的两个方法就是Serialize和Deserialize,分别用于序列化和反序列化,由于它们接收的是Stream基类,因此可以序列化到任何流类型中,而不仅限于文件流.
下面使用BinaryFormatter来序列化一个Product对象.(只保留了三个属性):
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;
namespace 流
{
class Program
{
static void Main(string[] args)
{
IFormatter formatter = new BinaryFormatter();
Product item = new Product(188) {Price=2999F,Name="SYX" };
Stream fs = File.OpenWrite(@"F:\product.obj"); ;
formatter.Serialize(fs,item);
fs.Dispose();
}
}
public class Product
{
public int Id { get; set; }
public string Name { set; get; }
public double Price { set; get; }
public Product(int id)
{
this.Id = id;
}
public override string ToString()
{
return string.Format("Id:{0} , Name:{1} , Price:{2} , ",this.Id,this.Name,this.Price);
}
}
}
如果直接运行的话,会出现一个异常”Product未标记为可序列化”.这是因为在默认情况下类型都是不可序列化的,需要明确的指定Product类是可序列化的.完成这个操作只需要为Product类加一个Serializable特性:
[Serializable]
public class Product
{
public int Id { get; set; }
public string Name { set; get; }
public double Price { set; get; }
public Product(int id)
{
this.Id = id;
}
public override string ToString()
{
return string.Format("Id:{0} , Name:{1} , Price:{2} , ",this.Id,this.Name,this.Price);
}
}
再次运行代码就可以得到序列化后的product.obj文件了,可以用记事本打开看看.
由于product.obj保存的是二进制数据,文本编辑器无法将其解析为合适的字符,所以会出现了乱码.还有一部分是按字符形式直接保存的,就显示为了相应的字符,可以看到对象的属性的属性名称和值.不止一位只要为类型加上Serializable特性就可以了,序列化不管需要该类型是标记为Serializable的,类型中的属性和字段也需要是可序列化的.如果在Product中加入了一个不可序列话的属性,比如一个private类型SqlConnection,因为SqlConnection没有使用Serializable特性进行标记,那么上面的程序就又无法进行的:
[Serializable]
public class Product
{
public int Id { get; set; }
public string Name { set; get; }
public double Price { set; get; }
private SqlConnection conn;
public Product(int id)
{
this.Id = id;
conn = new SqlConnection(@"Data Source=.;Initial Catalog=DB;User ID=sa;Password=123");
}
public override string ToString()
{
return string.Format("Id:{0} , Name:{1} , Price:{2} , Conn:{3}",this.Id,this.Name,this.Price,this.conn==null?"null":"object");
}
}
此时,就需要向编译器说明一件事:在Product类型中,Conn字段不需要进行序列化,在序列化时把它排除在外,.NET框架提供了Nonserialized特性用作这一用途:
[NonSerialized]
private SqlConnection conn;
肯定有人会问,这个序列化到底有啥用处?前面不是说了吗,保存对象的状态或者一些信息.有一点需要注意,NonSerialized只能加在紫断殇,不能加在属性上.在完成了这一过程后,再次运行序列化的代码就可以运行了,接下来输出一下Product的状态:
IFormatter formatter = new BinaryFormatter();
Stream fs = File.OpenRead(@"F:\product.obj");
Product item = (Product)formatter.Deserialize(fs);
fs.Dispose();
Console.WriteLine(item);
注意:字段是私有的也是可以序列化的,这是因为序列化的底层是通过反射来实现的,而反射可以访问到私有字段.关于反射,咱们会抽出一张来专门讲解.
尽管反序列化没有抛出异常,还是发现变成了null,这是因为反序列化并不会调用对象的构造函数,此时,由于Conn没有进行序列化,使序列化前对象和反序列化后得到的对象状态变得不一样了..NET提供了IDeserializationCallback接口来完成这件事:
public interface IDeserializationCallback
{
void OnDeserialization(object ssender);
}
从接口名称中的Callback可以看出,这个接口是一个灰调函数,在对象反序列化之后调用.sender总是传入null,可以不理会.现在让Product实现这个接口,以实现conn的序列化:
public class Product : IDeserializationCallback
{
public void OnDeserialization(object sender)
{
conn = new SqlConnection(@"Data Source=.;Initial Catalog=DB;User ID=sa;Password=123");
}
}
此时在执行上面反序列化的代码,可以得到期望的结果.
SoapFormatter类定义在命名空间下,它的使用方式和BinaryFormatter完全相同.使用SOAP的优点是可以跨平台,因为SOAP是一个开放的协议,所以使用非Windows平台下的其他程序也可以处理它.但是SOAP使用XML来描述的,因此文件的尺寸比较大.下面的代码使用SoapFormatter来序列化Product:
//使用SoapFormatter序列化
IFormatter formatter = new SoapFormatter();
Product item = new Product(188) {Name="ZSF",Price=4567F };
Stream fs = File.OpenWrite(@"F:\product.soap");
formatter.Serialize(fs,item);
fs.Dispose();
注意一点,在引用命名空间的时候,你需要在项目的引用中手动添加引用才能使该命名空间生效,如果你直接写上该命名空间的话是会报错的.
使用记事本打开product.soap文件,应该会看到一个XML文件样式的内容.