简单的序列化
代码可以在我的Github主页上找到,地址是https://github.com/techstay/csharp-learning-note 。
利用BinaryFormatter进行序列化
有时候需要将对象保存到文件里、存储到数据库中或者通过网络传输到远程的计算机中,这个时候就需要将对象转化成字节流的形式,以便进行传输或者持久化。这个过程叫做对象的序列化,而把已经序列化的字节流重新包装成活动的对象的过程叫做反序列化。.NET提供了完善的对象序列化和反序列的机制,因此我们可以很方便地进行对象的序列化和反序列化操作。
简单的序列化只需要一个字节流,然后调用System.Runtime.Serialization.Formatters.Binary.BinaryFormatter类的实例方法Serialize方法,即可完成序列化操作。反序列化的时候,只要使用该类的Deserialize方法,即可从字节流中将对象还原回来。
下面的例子使用了内存流来存放序列化产生的字节流。在序列化完成之后,流的位置发生了变化,因此需要将流重置以便能够正确的进行反序列化。
public enum PersonSex : byte
{
Male = 0,
Female = 1,
}
[Serializable]
public sealed class Person
{
private string _name;
private PersonSex _personSex;
private DateTime _birthday;
private int _age;
public Person()
{
}
public string Name
{
get { return _name; }
set { _name = value; }
}
public PersonSex Sex
{
get { return _personSex; }
set { _personSex = value; }
}
public DateTime Birthday
{
get { return _birthday; }
set
{
_birthday = value;
_age = (int)Math.Round((DateTime.Today - _birthday).TotalDays / 365);
}
}
public int Age
{
get { return _age; }
}
public override string ToString()
{
return $"Name={Name},Age={Age},Sex={Sex},Birthday={Birthday}";
}
}
public static void SimpleSerialize()
{
Console.WriteLine("简单的序列化:");
var someData = new List<object> { "张三", 22, DateTime.Now };
var somePeople = new List<Person>
{
new Person {Birthday = DateTime.Today, Name = "张三", Sex = PersonSex.Male},
new Person {Birthday =DateTime.Today,Name="李四",Sex=PersonSex.Female},
new Person {Birthday = DateTime.Today,Name="妹子",Sex=PersonSex.Female}
};
Console.WriteLine("序列化:");
PrintData(someData);
PrintData(somePeople);
var memory = new MemoryStream();
//构建格式化器并序列化
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(memory, someData);
formatter.Serialize(memory, somePeople);
//重置流的位置并清空原来的对象
someData = null;
somePeople = null;
memory.Position = 0;
//反序列化
someData = (List<object>)formatter.Deserialize(memory);
somePeople = (List<Person>)formatter.Deserialize(memory);
Console.WriteLine("反序列化:");
PrintData(someData);
PrintData(somePeople);
}
PrintData方法是我写的一个辅助方法,用来将对象打印在控制台上。
private static void PrintData(IEnumerable<object> data)
{
foreach (var d in data)
{
Console.WriteLine(d);
}
}
对象序列化的过程
并不是所有的对象都可以序列化。.NET类库中的常用的类型比如集合类和int等基本类型以及枚举和委托类型都支持序列化,使用的时候可以直接序列化。但是如果是程序员自己定义的类型,则不一定可以序列化。对于这样的对象进行序列化,格式化器会抛出SerializationException异常。要让一个类可以被序列化,需要向其类定义添加[Serializable]特性。格式化器读取到这个特性之后,会利用反射获取对象的所有字段,对所有类型进行序列化。如果有字段不能被序列化,同样会抛出SerializationException异常。如果所有字段都支持序列化,格式化器就会将所有的字段和对应的值写入到流中。需要注意的是:含有自动属性的类不能序列化和反序列化。因为自动属性是由编译器自动生成的,没有确定的名字,每次编译和代码的时候都不同,所以在反序列化的时候导致序列化失败。
下面的代码演示了如果在序列化的时候遇到了不能序列化的字段,就会抛出异常。这时候可能已经向流里面写入了一定的数据,但是由于没有序列化完成,所以流里面的数据不完整,无法进行反序列化。避免在流中产生错误数据的一种方法是先把对象序列化到内存流中,确认无误之后再把内存流中的内容复制到实际的目的地流中。
// [Serializable]
internal class NonserializableObject { }
[Serializable]
internal class ObjectContainingNonserializableObject
{
private string _string = "A String";
private int _integer = 150;
private DateTime _date = DateTime.Today;
private NonserializableObject _nonserializableObject = new NonserializableObject();
}
public static void SerializeANonserilizableObject()
{
Console.WriteLine(Environment.NewLine + "序列化不能序列化的对象:");
var memoryStream = new MemoryStream();
var obj = new NonserializableObject();
var binaryFormatter = new BinaryFormatter();
try
{
binaryFormatter.Serialize(memoryStream, obj);
}
catch (SerializationException e)
{
Console.WriteLine(e.Message);
}
Console.WriteLine("流中会含有错误数据:");
memoryStream.Position = 0;
var streamReader = new StreamReader(memoryStream);
Console.WriteLine(streamReader.ReadToEnd());
}
序列化到XML文件
不仅可以将对象序列化成字节流,
还可以将其序列化为一个XML文件。这需要一个System.Xml.Serialization.XmlSerializer对象,然后将要序列化的对象的类型传入其构造器,构造出合适的XML格式,然后调用其对应的Serialize方法将对象序列化到文件中。
public static void SerializeToXml()
{
Console.WriteLine(Environment.NewLine + "列表序列化到XML:");
var someData = new List<object> { "张三", 22, DateTime.Now };
Console.WriteLine("序列化之前:");
PrintData(someData);
var memory = new MemoryStream();
//构建XML序列化器并序列化
var dataSerializer = new XmlSerializer(someData.GetType());
dataSerializer.Serialize(memory, someData);
//重置流位置准备显示XML文档内容
memory.Position = 0;
StreamReader reader = new StreamReader(memory);
Console.WriteLine("XML文件内容:");
string line = string.Empty;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
//重置流位置准备反序列化
memory.Position = 0;
someData = null;
someData = (List<object>)dataSerializer.Deserialize(memory);
Console.WriteLine("反序列化之后:");
PrintData(someData);
}
控制序列化的过程
利用特性控制序列化
要将一个类型标记为可序列化的,只需要向其应用SerializableAttribute特性。但是这会将该类型中所有的字段都序列化。有时候不想将所有的字段都序列化。比如:
- 字段含有反序列化之后无效的信息。比如当前进程的PID,或者是当前使用电脑的用户,这样的话在把这样一个对象发送到其他计算机上面的时候,这些信息就会失效。
- 某些字段的值可以被计算出来。这样的话就没有必要在流中传输这些字段,这样可以减小带宽压力,增强计算机的性能。
- 安全因素。用户的密码等重要信息可能会在传输的过程中被拦截破译,因此这类信息不应被序列化。
要让某一个字段不能被序列化,对其使用NonSerialized特性即可。
[Serializable]
public class Cuboid
{
private readonly double _length;
private readonly double _width;
private readonly double _height;
[NonSerialized]
private double _area;
[NonSerialized]
private double _volume;
public Cuboid(double length, double width, double height)
{
_length = length;
_width = width;
_height = height;
_area = 2 * (_length * _width + _length * _height + _height * _width);
_volume = _height * _width * _length;
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
_area = 2 * (_length * _width + _length * _height + _height * _width);
_volume = _height * _width * _length;
}
public override string ToString()
{
return $"Cuboid(Length:{_length},Width:{_width},Height:{_height},Area:{_area},Volume:{_volume}";
}
}
如果只是简单地使用了NonSerialized特性,那么反序列化之后应用该特性的字段会被赋值成默认值。例如上面的代码中,Cuboid类的_area和_volume在反序列化之后就会变成零。为了让它们正确地被赋值,可以在类中写一个匹配Action< StreamingContext>的方法,并向其应用OnDeserialized特性。格式化器会在其他字段全部反序列化之后调用该方法。为避免该方法被误调用,通常将该方法设为private的。
另外还有几个特性可以控制序列化和反序列化时候所执行的操作,它们都需要应用到匹配Action< StreamingContext>的方法上:
特性名称 | 作用 |
---|---|
OnSerializing | 格式化器序列化对象前 |
OnSerialized | 格式化器序列化对象后 |
OnDeserializing | 格式化器反序列化对象前 |
OnDeserialized | 格式化器反序列化对象后 |