第11章 序列化和反射
11.1 序列化与反序列化
11.1.1 为什么需要序列化
在本书第10章中的示例4和示例5中,我们分别实现了定制频道信息写入文本文件和读取定制频道信息的功能。试想如果Channel类的属性发生变化,我们该如何处理呢?我们肯定要修改示例中的SaveAsTxt()方法和LoadFromTxt()方法。但是如果一些信息需要经常变化,是否每次都要这样繁琐地改动呢?答案是否定的,本章我们要学习一种新技术,只要简简单单的几步就可以一劳永逸地完成配置信息的读写操作。步骤如下。
(1)在ChannelManager类中引入这样一个命名空间。
using System.Runtime.Serialization.Formatters.Binary;
(2)在SavingInfo、 ChannelBase、 TypeAChannel、 TypeBChannel类的头部添加一个标记[Serializable],例如,这样用于标记该类是否可序列化。
[Serializable]
abstract class ChannelBase
{
//…
}
(3)修改SaveAsTxt()和LoadFromTxt()方法,如示例1所示。
示例1
//保存定制频道信息的文本文件名称
private string saveFileName = @"files\save";
//将我的电台信息存储到文本文件之中
public void SaveAsTxt()
{
FileStream fs = null;
try
{
fs = new FileStream(saveFileName + ".bin", FileMode.Create);
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(fs, this.seria.MyFavor);
//this.seria.MyFavor是"我的电视台"频道集合对象
}
catch
{
throw;
}
finally
{
fs.Close();
}
}
// 从文本文件之中读取"我的电台"信息
public void LoadFromTxt()
{
FileStream fs = null;
try
{
fs = new FileStream(saveFileName + ".bin", FileMode.Open);
BinaryFormatter bf = new BinaryFormatter();
SavingInfo info=(SavingInfo)bf.Deserialize(fs);
}
catch
{
throw;
}
finally
{
fs.Close();
}
}
经过验证,程序中对象的值都被正确地保存和读取了。简单的几段代码,实现了示例1 的一大堆代码实现的功能,而且不用关心文件的结构。这种实现方式就称为序列化。更美妙的是,一旦你的配置发生了变化,直接修改你的SavingInfo类即可,SaveAsTxt()和LoadFromTxt()方法无须改变!
11.1.2 特性
在上面的代码中。我们发现了一个特别的地方,就是在我们的类声明上面加了如下一行代码。
[Serializable]
abstract class ChannelBase
{
//…
}
这个[Serializable]主要用来告诉系统,下面的类是可序列化的。而[Serializable]本身,我们称之为可序列化特性。所谓特性,就是为目标元素(可以是数据集、模块、类、属性、方法、甚至函数参数等)加入附加信息,类似于注释。特性本质上也是一个类,如[Serializable]对应的类是SerializableAttribute。 (一般来说,特性命名都以Attribute结尾,但是我们在使用它时,可以省略这个小尾巴,聪明的.NET会自动找到对应的特性类)。特性可以直接影响代码的运行方式,例如示例2中的可序列化特性。在.NET中还有很多特性,可以标记指定元素的特殊编译或者运行方式,参考如示例2所示的代码,ObsoleteAttribute用于标记一个不再使用的程序元素。
示例2
class Program
{
[Obsolete("不要使用旧的方法, 请使用新的方法", true)]
static void Old() { }
static void New() { }
public static void Main()
{
Old();
}
}
ObsoleteAttribute标记了一个不该再被使用的语言元素Old(),该特性的第一个参数是 string类型,它解释为什么该元素被荒弃,以及我们该使用什么元素来代替它。实际上,我们可以书写任何其他文本来代替这段文本。第二个参数是告诉编译器把依然使用这个被标识的元素视为一种错误,这就意味着编译器会因此而产生一个警告。如果试图编译这段代码就会提示错误"MyAttributes.Program.Old()"已过时: "不要使用旧的方法,请使用新的方法"。
定制特性主要应用在序列化、编译器指令、设计模式等方面。以后我们会在开发中学习其他的特性。
11.1.3 序列化
序列化是将对象的状态存储到特定存储介质中的过程,也可以说是将对象状态转换为可保持或传输的格式的过程。在序列化过程中,会将对象的公有成员、私有成员包括类名,都转换成数据流的形式,存储到存储介质中,这里说的存储介质通常指的是文件。例如,示例1中,我们通过序列化保存了SaveingInfo对象的信息。.NET提供多种形式的序列化,文本或XML流等。目前使用二进制方式对泛型支持得最好。参考示例1中如下代码。
FileStream fileStream = null;
//定义一个文件流
fileStream = new FileStream("profile.bin", FileMode.Create);
//二进制方式
BinaryFormatter bf = new BinaryFormatter();
//序列化保存配置文件对象Profile
bf.Serialize(fileStream, Profile);
因为序列化需要通过文件流来保存到文件,所以要先定义一个文件流,BinaryFormatter是一个二进制格式化器,这个二进制格式化器具有一个非常重要的Serialize()方法。
语法:
public void Serialize (Stream serializationStream, Object graph)
这个方法的主要功能是将特定对象序列化到特定文件中,具体参数意义如下。
- serializationStream是指定序列化过程的文件流。
-
graph是要保存的对象。
如果我们要序列化的对象包含子类对象,那么这个序列化的基本过程大致如图11-1所示。
图11-1 序列化的基本过程
如果需要序列化某个特定对象,那么它的各个成员对象也必须是可序列化的。对我们的网络电视精灵而言,如果要将程序中的SavingInfo对象序列化,那么它包含的对象都需要加上可序列化标记,例如SavingInfo、 ChannelBase、 TypeAChannel等。
11.1.4 反序列化
既然能将对象的状态保存到特定介质中,那么我们又应该怎样将这些对象状态读取回来呢?这就用到了另一个知识:反序列化。所谓反序列化,顾名思义就是与序列化相反,序列化是将对象的状态信息保存到存储介质中,反序列化则是从特定存储介质中将数据重新构建对象的过程。通过反序列化,可以将存储在文件上的对象信息读取,然后重新构建为对象。这样就不需要我们再将文件上的信息一一读取、分析再组织为对象了,仍然以二进制格式化器为例,它的反序列化方法原型如下。
语法:
public Object Deserialize (Stream serializationStream)
注意,Deserialize()方法将存储介质的数据文件流转换为Object,通常我们仍然需要进一步将这个Object转换为相应的对象类型。参考示例1中的LoadFromTxt()方法。
反序列化将创建出与原对象完全相同的副本,在序列化时所保存的数据将被无损失地保存下来。
11.1.5 序列化和反序列化的用途
- 我们经常需要将对象的字段值保存到磁盘中,并在以后检索此数据。尽管不使用序列化也能完成这项工作,但这种方法通常很繁琐,而且容易出错。可以想象一下编写包含大量对象的大型业务应用程序的情形,程序员不得不为每一个对象编写代码,以便将字段和属性保存至磁盘以及从磁盘还原这些字段和属性。序列化提供了轻松实现这个目标的快捷方法。
- 通过序列化将对象从一个应用程序发送到另一个应用程序中。这将会在我们下一门课,三层结构开发中体现出来。
-
在远程通信中应用非常广泛,可以将一个应用程序中的对象序列化,然后通过网络通信,远程传递给其他地点的另一个应用程序,例如 WebService开发。这些内容都将在我们以后的课程中学习。
11.2 程序集与反射
11.2.1 什么是程序集
程序集虽然是一个新概念,但是我们使用它其实已经很久了。在一个.NET的WinForms应用程序编译后,在bin\Debug文件夹下会生成一个.exe文件,例如我们的网络电视精灵,会生成一个TVXmlRead.exe文件,双击这个文件,会打开网络电视精灵的应用程序,实现整个应用程序的功能,为什么运行这个文件就能实现这个功能,无须打开开发环境呢?其实,这个编译好的.exe文件,称为程序集。程序集是.NET框架应用程序的生成块,它包含编译好的代码逻辑单元。
11.2.2 程序集的结构
程序集由描述它的程序集清单、类型元数据、MSIL代码和资源组成,这些部分都分布在一个文件中,或者分布在几个文件中,如图11-2所示。
图11-2 程序集内容
1.程序集清单
每一个程序集都包含描述该程序集中各元素彼此如何关联的数据集合。程序集清单包含这些程序集的元数据。程序集清单包含指定该程序集的版本要求和安全标识所需的所有元数据。程序集清单的主要内容见下表。
信息
说明
程序集名称
指定程序集名称的文本字符串
版本号
主版本号和次版本号,以及修订号和内容版本号
区域性
有关该程序集支持的区域性或语言的信息
强名称信息
如果已经为程序集提供了一个强名称,则为来自发行者的公钥
程序集中所有文件的列表
构成该程序集的文件
类型引用信息
控制对该程序集的类型和资源的引用如何映射到包含其声明和实现的文件中
有关被引用程序集的信息
该信息用于从程序集导出的类型
程序集清单的主要功能如下。
(1)列举构成该程序集的文件。
(2)控制对该程序集的类型和资源的引用如何映射到包含其声明和实现的文件中。
(3)列举该程序集所依赖的其他程序集。
(4)在程序集的使用者和程序集的实现详细信息的使用者之间提供一定程度的间接性。
(5)呈现程序集自述。
2.元数据
元数据是一种二进制信息,它以非特定语言的方式描述在代码中定义的每一个类型和成员,程序集清单也是元数据的一部分,上面已经讲过它主要存储以下信息。
(1)程序集的说明。
(2)标识(名称、版本、区域性、公钥)。
(3)导出的类型。
(4)该程序集所依赖的其他程序集。
(5)运行所需的安全权限。
而类型元数据包含以下内容。
(1)类型的说明。
(2)名称、可见性、基类和实现的接口。
(3)成员(方法、字段、属性、事件、嵌套的类型)。
(4)属性。
(5)修饰类型和成员的其他说明性元素。
3.其他内容
MSIL是微软中间代码,它是实现类型元数据的中间代码,而资源就是我们程序中的图片、音乐文件等。
11.2.3 查看程序集
知道了程序集的结构,如何查看一个程序集的结构呢? .NET中提供了一个反编译工具ILDasm,使用它可以查看IL汇编代码,也可以看到程序集中的类和方法等。在Visual Studio 2017的命令行窗口,输入ILDasm.exe,就可以打开这个反编译器,打开我们要查看的TVXmlRead.exe程序集,就能够将程序集中的内容显示出来,如图11-3所示。
图11-3 TVXmlRead的程序集结构
打开该程序集清单,就可以看到版本号。打开类的方法,就可以查看MSIL代码。使用这个工具,你便可以查看一些程序集的清单,了解它的结构。在Visual Studio中,所有C#项目类型都会创建一个程序集,无论是类库还是可执行的EXE应用程序。在我们创建一个Visual Studio项目时,会自动生成源文件AssemblyInfo.cs,在这个文件中,可以使用一般的源代码编辑器编辑程序集的特性。下面就是TVXmlRead的AssemblyInfo文件的主要内容。
[assembly: AssemblyTitle("TVXmlRead")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("邯郸翱翔")]
[assembly: AssemblyProduct("TVXmlRead")]
[assembly: AssemblyCopyright("Copyright © AoXiang")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
这个文件用于配置程序集清单。编译器读取程序集的属性,把特定的信息插入到程序集清单中。用于程序集属性的参数是命名空间System.Reflection、System.Runtime.CompilerServices等。下表列出了一些程序集属性
属性
说明
AssemblyCompany
指定公司名
AssemblyTitle
程序集的描述性名称
AssemblyDescription
描述程序集或产品
AssemblyConfifuration
指定建立信息,例如零售或者调试信息
AssemblyProduct
指定程序集所属产品的名称
AssemblyCopyright
包含版权和商标信息
AssemblyVersion
程序集的版本号
当右击查看TVXmlRead.exe文件的属性时,就可以看到该程序集中的一些属性,如图11-4所示。
图11-4 TVXmlRead.exe属性
11.2.4 程序集中的访问修饰符
在本章之前,我们学习了3种访问修饰符private、public、protected。对于它们修饰的成员的作用域,都很熟悉。本章提出另一个访问修饰符internal,它修饰的成员在同一个程序集中都可以访问,但是其他的程序集就不能访问,应用程序中的类,如果不指定访问修饰符,默认就是internal修饰。4种访问修饰符的作用域见下表。
| 类内部 | 同一程序集的派生类 | 同一程序集的其他类 | 不同程序集的派生类 | 不同程序集的其他类 |
private | 可以 | 不可以 | 不可以 | 不可以 | 不可以 |
protected | 可以 | 可以 | 不可以 | 可以 | 不可以 |
internal | 可以 | 可以 | 可以 | 不可以 | 不可以 |
public | 可以 | 可以 | 可以 | 可以 | 可以 |
11.2.5 反射
刚才介绍了ILDasm工具的使用,可以用ILDasm反编译工具浏览一个dll和exe的构成,这种机制我们称为反射。它用于在运行时通过编程方式获得类型信息。反射其实在我们编程中经常能够用到,例如当在Visual Studio中输入一个类型,然后输入"."时,就会拉出一个列表,显示这个类型的属性、方法、事件等。这都是利用了反射机制。反射可以获取已加载的程序集和在其中其中定义的类型(如类、接口和值类型)的信息。也可以使用反射在运行时创建类型实例,以及调用和访问这些实例。反射的一个主要功能就是查找程序集的信息。System.Reflection.Assembly类可以用于访问给定程序集的信息,它允许访问给定程序集的元数据,如示例4所示,我们利用一个外部应用程序来获取TVXmlRead的版本号。
示例3
class Program
{
static void Main(string[] args)
{
string version = Assembly.LoadFile(@"D:\TVXmlRead.exe")
.GetName().Version.ToString();
Console.WriteLine(version);
}
}
Assembly.LoadFile(string path)方法用于通过文件路径加载程序集,其参数path必须为完整物理路径。运行结果如图11-5所示。
图11-5 反射获得版本号
反射得到版本号的最大用处就是定期升级软件,反射得到当前版本号与升级程序版本号相比较,如果不一致就执行升级程序。反射是一个非常强大的机制,利用反射,我们可以了解一些没有源代码程序的结构,从而提高程序集的利用效率。
11.2.6 通过反射获取类型
通过反射除了可以获取版本信息之外,我们还可以从程序集中获取类型信息。如实例4所示,我们可以从TVXmlRead.exe程序集中获取其中包含的类型。
实例4
class Program
{
static void Main(string[] args)
{
//加载程序集
Assembly assembly = Assembly.LoadFile(@"D:\TVXmlRead.exe");
//获取程序集中全部的类型
Type[] types= assembly.GetTypes();
foreach(Type type in types)
{
//输出类型的全名
Console.WriteLine(type.FullName);
}
Console.ReadLine();
}
}
运行结果如图11-6所示。
图11-6 读取程序集中的全部类型
程序集对象的GetTypes()方法可以得到程序集中全部类型信息的数组。另外还有GetType(string name)可以得到指定的类型信息。
Type是.Net定义的表示类型的类,位于System命名空间。其常用属性和方法如下表所示。
属性 |
| 说明 |
Namespace |
| 获取Type的命名空间。 |
Name |
| 数据类型名。 |
FullName |
| 获取该类型的完全限定名称,包括其命名空间,但不包括程序集 |
BaseType |
| 获取当前 Type 直接从中继承的类型。 |
返回值 | 方法 | 说明 |
PropertyInfo | GetProperty(String name) | 搜索具有指定名称的公共属性。 |
PropertyInfo[] | GetProperties() | 返回为当前 Type 的所有公共属性。 |
MethodInfo | GetMethod(string name) | 搜索具有指定名称的公共方法。 |
MethodInfo[] | GetMethods() | 返回为当前 Type 的所有公共方法。 |
除了通过程序集获取类型信息外,还可以通过实例对象和类型获取类型信息。
(1) 通过实例对象的GetType()方法获取类型信息
//创建一个对象
Example obj = new Example();
//获取类型信息
Type type= obj.GetType();
(2) 通过类获取类型信息
Type type= typeof(Example) ;
11.2.7 动态创建和使用对象
我们还可以通过获取的类型信息创建对象。语法如下:
object obj = Activator.CreateInstance(Type对象);
Activator类的CreateInstance()静态方法用来创建一个对象,返回类型为object。
我们也可以通过指定的程序集来创建对象。语法如下:
object obj = 程序集对象.CreateInstance("类型全名");
资料
CreateInstance()方法还提供了多个重载版本,例如可以给类型的有参构造函数传递参数。请大家参阅MSDN。
对象创建完成之后,我们可以给对象属性赋值和调用对象的方法,如实例5所示。
实例5
static void Main(string[] args)
{
//加载程序集
Assembly assembly = Assembly.LoadFile(@"D:\TVXmlRead.exe");
//获取TypeAChannel类型信息
Type channel = assembly.GetType("TVXmlRead.TypeAChannel");
//创建TypeAChannel类型的实例
object obj = assembly.CreateInstance("TVXmlRead.TypeAChannel");
//循环给实例的属性赋值
foreach(PropertyInfo pi in channel.GetProperties())
{
switch(pi.Name)
{
case "ChannelName":
pi.SetValue(obj, "北京电视台");
break;
case "Path":
pi.SetValue(obj, @"北京电视台.xml");
break;
}
}
Console.WriteLine("输出属性值:");
Console.WriteLine("ChannelName属性:"
+ channel.GetProperty("ChannelName").GetValue(obj));
Console.WriteLine("Path属性:"+channel.GetProperty("Path").GetValue(obj));
//获取方法信息
MethodInfo Show = channel.GetMethod("Show");
Console.WriteLine("\nShow方法执行结果:");
Show.Invoke(obj,null); //调用方法
Console.ReadLine();
}
程序运行结果如图11-7所示。
图11-7 动态创建和使用对象
实例5中,属性信息的SetValue()方法用来给属性赋值,GetValue()方法用户获取属性值。方法信息对象的Invoke()方法用来调用方法,第二个参数为object[]类型,用来给方法传递参数。