一、问题引入
我们在进行编程的时候,对于数据常常有这样的需求:我们将数据写入文件,文件我们可以绕过程序找到,但是我们不想文件被人打开查看里面的内容,我们就可以使用C#中的序列化进行加密。
当然序列化的作用不仅于此:
1.1序列化
在C#中,序列化是将对象的状态信息转换为可以存储或传输的形式的过程。这个过程通常涉及将对象转换为一个字节序列(二进制序列化),或者转换成可读的格式,如XML或JSON(文本序列化)。序列化对于以下场景非常重要:
-
持久化数据: 通过序列化,你可以将对象持久化到文件、数据库、或者其他存储介质中。这样,即使程序终止,对象的状态也可以在之后被重新加载和使用。
-
通信: 在不同的应用程序之间或者不同的执行环境(如客户端和服务器)之间传输对象时,需要将对象序列化为一种标准格式,这样接收方才能够反序列化并重建对象。
-
深拷贝: 序列化还可以用来执行对象的深拷贝,即创建一个对象的完全独立副本,其中所有的子对象都是新的实例,而不是对原始对象的引用。
-
跨平台兼容性: 序列化允许跨语言或跨平台的数据交换,因为序列化后的格式通常是标准化的,不同的系统可以理解和处理。
-
安全或隔离: 对传输的数据进行序列化可以为数据添加安全层,例如,可以在数据传输之前对其进行加密,并在接收后解密。
-
缓存: 为了提高性能,应用程序可以将对象序列化后存储在缓存中,这样可以快速地加载和使用这些对象,而不是每次都重新创建它们。
序列化可以是显式的,也可以是隐式的。显式序列化通常是你直接调用序列化相关的API来执行的,如.NET
框架提供的BinaryFormatter
, DataContractSerializer
, XmlSerializer
, JavaScriptSerializer
等。隐式序列化可能会发生在如远程过程调用(RPC)或者在某些框架和组件中,当需要自动传输或保存数据的时候。总之,序列化是现代编程中一个非常关键的特性,它使得对象数据的管理和传输成为可能。
1.2序列化的方式
在C#中,序列化可以通过多种方式进行,各种方式适用于不同的场景和需求。以下是一些常见的序列化方法:
二进制序列化: 利用 BinaryFormatter 类,可以将对象序列化为二进制格式。这种格式紧凑但不可读,适合性能敏感和带宽受限的场景。
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
// 创建对象
MyObject obj = new MyObject();
// 创建一个BinaryFormatter对象进行序列化
BinaryFormatter bf = new BinaryFormatter();
// 创建文件流
using (FileStream fs = File.Create("MyObject.dat"))
{
// 序列化对象
bf.Serialize(fs, obj);
}
2.XML序列化: 利用 XmlSerializer 类,可以将对象序列化为XML格式。XML是一种广泛使用的可读文本格式,适用于需要可读性或跨平台兼容性的场景。
using System.IO;
using System.Xml.Serialization;
// 创建对象
MyObject obj = new MyObject();
// 创建一个XmlSerializer对象进行序列化
XmlSerializer serializer = new XmlSerializer(typeof(MyObject));
// 创建文件流
using (TextWriter writer = new StreamWriter("MyObject.xml"))
{
// 序列化对象
serializer.Serialize(writer, obj);
}
3.JSON序列化: 通过使用 System.Text.Json(.NET Core 3.0及以上版本)或 Newtonsoft.Json(常用于.NET Framework或早期版本.NET Core)可以将对象序列化为JSON格式,这种格式在Web开发中非常流行。
using System.Text.Json;
// 创建对象
MyObject obj = new MyObject();
// 序列化对象到JSON字符串
string jsonString = JsonSerializer.Serialize(obj);
// 将JSON字符串写入文件
File.WriteAllText("MyObject.json", jsonString);
4.数据契约序列化: 使用 DataContractSerializer, 面向服务或WCF(Windows Communication Foundation)应用程序设计而提供了更多的配置选项。
using System.IO;
using System.Runtime.Serialization;
// 创建对象
MyObject obj = new MyObject();
// 创建一个DataContractSerializer对象进行序列化
DataContractSerializer dcs = new DataContractSerializer(typeof(MyObject));
// 创建文件流
using (Stream stream = File.Create("MyObject.xml"))
{
// 序列化对象
dcs.WriteObject(stream, obj);
}
序列化之前需要确保对象是可序列化的,对于二进制序列化,通常需要在类前面加上 [Serializable] 属性。对于XML序列化,必须有一个无参构造函数,并且序列化的属性和字段需要是public的。对于数据契约序列化,需要在类和需要序列化的成员上标注 [DataContract] 和 [DataMember] 属性。对于JSON序列化,通常没有特别的要求,但对象的结构应该符合JSON能表示的数据结构。
序列化后的数据可以保存到文件中,也可以通过网络传输给其他应用程序或服务。对端应用程序可以使用相应的反序列化方法来重建原始对象。
二、举例(实际应用)
假如我们用Windows 窗口应用程序存储学生的信息,姓名,性别,年龄,生日
我们先创建一个学生的类,根据上面讲的内容,我们在进行序列化前要进行可序列化标记,因为我们打算用二进制序列,所以使用[Serializable]
2.1保存数据
//封装数据
Student objStudent = new Student() //创建对象
{
Name = this.txtName.Text.Trim(),
Age = Convert.ToInt16(this.txtAge.Text.Trim()),
Gender = this.txtGender.Text.Trim(),
Birthday = Convert.ToDateTime(this.txtBirthday.Text.Trim())
};
//【1】创建文件流
FileStream fs = new FileStream("objStudent.stu", FileMode.Create);
//[2]创建二进制格式化器
BinaryFormatter formatter = new BinaryFormatter();
//[3]调用序列化方法
formatter.Serialize(fs, objStudent);
//[4]关闭文件流
fs.Close();
这里要注意以下几点:
1.对Student对象进行封装的原因:
通过创建一个 Student 对象并设置其属性,代码更加清晰和易于理解。这样的封装表明了你是在处理一个有具体结构和属性的数据实体,而不是零散的、无关联的数据。
另外 封装对象使得你可以在不同的方法中以对象为单位重用代码,而不必每次都重复设置个别的数据项。
其次,在一个对象内部封装相关的数据能确保数据的完整性。在这个例子中,Student 对象的属性(Name、Age、Gender、Birthday)被一起处理,这有助于确保所有相关信息在序列化和反序列化过程中保持一致。
最后 如果将来需要添加、删除或更改学生属性,封装将使得这些变更更加集中和容易管理。你只需要在 Student 类中做修改,而不是在代码的多个地方修改序列化和反序列化的逻辑。
2.创建文件流,这个可以参照C#文件读写里面的内容。
3.创建二进制格式化器
BinaryFormatter
类在.NET Framework中用于二进制序列化和反序列化对象。当使用 BinaryFormatter.Serialize
方法时,所传递的对象会被转换为二进制流。这里简要说明这个过程的原理:
1). 对象图遍历: 序列化过程从根对象开始,递归地遍历对象中的所有字段,包括简单类型的字段(如 int
, double
等)、复杂类型字段(如其他类的实例)、数组、集合等。对于每个被访问的对象,都会检查它是否已被序列化过,以避免循环引用导致的无限循环。
2). 转换字段为二进制: 对象的每个字段值被转换成二进制数据。对于基础类型,这通常是直接的,因为存在明确的映射(例如,一个 int
类型会被转换为四个字节)。对于复杂类型,会先记录类型信息,然后将其字段递归序列化。
3). 保留类型信息: 为了在反序列化时能够重建对象,序列化过程中会保留对象的类型信息。这通常涉及记录对象的完整类型名称(包括其命名空间和程序集)。
4). 处理对象身份: 如果在对象图中多次引用同一个对象实例,BinaryFormatter
会处理这种情况,确保在反序列化时能够重建相同的引用关系,而不是创建多个副本。
5). 流写入: 序列化后的二进制数据会被写入到提供的 FileStream
中。这个数据流可能包含大量的字节,这些字节按照特定格式排列,以便于 BinaryFormatter
可以准确地重建对象图。
6). 支持自定义序列化: 如果对象实现了 ISerializable
接口,BinaryFormatter
会调用该接口的 GetObjectData
方法来获取对象要序列化的数据,这允许对象自定义其序列化过程。
使用 BinaryFormatter
的二进制序列化以紧凑和高效的格式存储对象,主要用于对象持久化和远程通信。但是,需要注意的是,由于安全和兼容性的问题,从.NET Core开始,BinaryFormatter
不再被推荐使用,而是推荐使用其他序列化方法,如 System.Text.Json.JsonSerializer
或 System.Runtime.Serialization.Formatters.Binary
。
4.关闭文件流
这个在C#文件读写已经讲明。
我们运行成功后,输入:
发现在D:\IDE\my.obj这个路径中有了这样一个文件:
但是打不开,或者打开不是我们真实的内容。
2.2 读取数据
private void btnDeserialize_Click(object sender, EventArgs e)
{
//【1】创建文件流
FileStream fs = new FileStream("D:\\IDE\\my.obj", FileMode.Open);
//[2]创建二进制格式化器
BinaryFormatter formatter = new BinaryFormatter();
//【3】调用反序列化方法
Student objStudent = (Student)formatter.Deserialize(fs);
//[4]关闭文件流
fs.Close();
this.txtName.Text = objStudent.Name;
this.txtAge.Text = objStudent.Age.ToString();
this.txtGender.Text = objStudent.Gender;
this.txtBirthday.Text = objStudent.Birthday.ToShortDateString();
}
这里面要注意的就是下面这一句里面的强制类型转换
Student objStudent = (Student)formatter.Deserialize(fs);
下面说明一下原因:
在.NET中,序列化和反序列化的过程涉及将对象状态转换为可存储的格式(如二进制),以及将这种格式转换回对象实例。BinaryFormatter.Deserialize
方法将二进制数据转换为最初序列化时的对象类型,但是返回值是 object
类型的,因为在.NET中所有类型都继承自 object
。
当你调用 formatter.Deserialize(fs)
方法时,它读取二进制数据流 fs
并根据其中的信息创建一个新的对象。因为 Deserialize
方法的返回类型是 object
,即最基本的数据类型,你需要将其转换回原始的具体类型(在本例中是 Student
)以便能够访问特定的属性和方法。
这种转换被称为强制类型转换(或类型转换),它告诉编译器你期望的返回对象的具体类型。在这个例子中,如果序列化的数据确实代表了一个 Student
对象,那么这个强制转换就是安全的。转换之后,你就可以将对象赋值给一个 Student
类型的变量,并访问其特有的属性和方法。
如果序列化的数据不是 Student
类型,这个强制转换将会失败并抛出一个 InvalidCastException
,提示无法将对象从一种类型转换为另一种类型。
在C# 7.0及以后的版本中,我们可以使用模式匹配来进行更为安全的类型转换,例如:
if(formatter.Deserialize(fs) is Student objStudent)
{
// 使用 objStudent
}
else
{
// 处理错误情况
}
这种方式更加安全,因为它会在类型不匹配时执行另一段代码,而不是抛出异常。