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)
这个方法的主要功能是将特定对象序列化到特定文件中,具体参数意义以下。
若是咱们要序列化的对象包含子类对象,那么这个序列化的基本过程大体如图11-1所示。
图11-1 序列化的基本过程
若是须要序列化某个特定对象,那么它的各个成员对象也必须是可序列化的。对咱们的网络电视精灵而言,若是要将程序中的SavingInfo对象序列化,那么它包含的对象都须要加上可序列化标记,例如SavingInfo、 ChannelBase、 TypeAChannel等。
11.1.4 反序列化
既然能将对象的状态保存到特定介质中,那么咱们又应该怎样将这些对象状态读取回来呢?这就用到了另外一个知识:反序列化。所谓反序列化,顾名思义就是与序列化相反,序列化是将对象的状态信息保存到存储介质中,反序列化则是从特定存储介质中将数据从新构建对象的过程。经过反序列化,能够将存储在文件上的对象信息读取,而后从新构建为对象。这样就不须要咱们再将文件上的信息一一读取、分析再组织为对象了,仍然以二进制格式化器为例,它的反序列化方法原型以下。
语法:
public Object Deserialize (Stream serializationStream)
注意,Deserialize()方法将存储介质的数据文件流转换为Object,一般咱们仍然须要进一步将这个Object转换为相应的对象类型。参考示例1中的LoadFromTxt()方法。
反序列化将建立出与原对象彻底相同的副本,在序列化时所保存的数据将被无损失地保存下来。
11.1.5 序列化和反序列化的用途
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[]类型,用来给方法传递参数。