C# 篇基础知识9——特性、程序集和反射
特性(Attribute)是用于为程序元素添加额外信息的一种机制。比如记录文件修改时间或代码作者、提示某方法已经过期、描述如何序列化数据等等。方法、变量、属性、类、接口、结构体以及程序集等都是程序元素。
1.使用特性
可以使用特性标注一个方法已经过时,已经有新方法了,但旧方法仍可以使用,当编译器发现某段代码调用了被ObsoleteAttribute 标识的程序元素时,就会产生一个警告或错误信息。.NET 约定,所有特性的名称都以Attribute 结尾,但在使用时可以省略后缀Attribute。.NET 中共预定义了200 多个特性,可以为代码中的程序元素提供丰富的描述信息。CLR 通过这些描述信息控制程序元素的行为特征,比如方法是否过时、如何序列化数据、记录文件修改时间、记录代码作者、为代码添加调试信息等。例如:
[Obsolete("该方法已经过时,请使用新方法NewDrawMyself()", false)]
public void DrawMyself() {…}
2.自定义特性
特性也是以类的方式实现的,一个特性就是一个类。现在来自定义一个特性类,自定义特性类都要继承自系统的Attribute类。例如自定义一个描述动物生物学分类信息的特性类AnimalInfoAttribute:
class AnimalInfoAttribute:Attribute
{ public AnimalInfoAttribute(string nameValue)
{ Name = nameValue; }
public string Name { get; set; } //属性:名称
public string Phylum { get; set; } //属性:门
public string Classis { get; set; }//属性:纲
public string Familia { get; set; }//属性:科
}
使用自定义特性:
[AnimalInfo("狼", Phylum = "脊索动物门", Classis = "哺乳纲", Familia = "犬科")]
class Wolf{}
特性作用于程序元素后,就成为程序元素的一部分。查看与程序元素相关的特性信息需要使用Attribute类的GetCustomAttribute()方法。如果程序元素中有多个特性,可使用Attribute 类的GetCustomAttributes()方法获取所有特性。例如:
static void Main(string[] args)
{ //获取Wolf 类中的AnimalInfoAttribute 特性
Attribute myAttribute =
Attribute.GetCustomAttribute(typeof(Wolf),typeof(AnimalInfoAttribute));
AnimalInfoAttribute wolfAttribute = myAttribute as AnimalInfoAttribute;
if (wolfAttribute == null)
{Console.WriteLine("Wolf 类中不存在特性。");}
else
{Console.WriteLine(wolfAttribute.Name + ":");
Console.WriteLine(wolfAttribute.Phylum);
Console.WriteLine(wolfAttribute.Classis);
Console.WriteLine(wolfAttribute.Familia);}}
自定义的特性本质上也是一个类,在定义它的过程中,也可以被其他特性修饰,比如经常用AttributeUsageAttribute 特性修饰自定义的特性。
[AttributeUsage(AttributeTargets.All,Inherited = true, AllowMultiple = true)]
class AnimalInfoAttribute:Attribute{}
AttributeUsageAttribute 类是专门用于修饰特性的特性,用它可以限定特性的某些重要行为特征。AttributeUsageAttribute 类有三个重要属性:AttributeTargets、Inherited 和AllowMultiple。
AttributeTargets 是一个枚举,用来规定特性可作用于哪些程序元素。AttributeTargets 的值可以通过“或”操作组合起来,使自定义的特性能作用于多种程序元素,例如[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]。Inherited 属性用来指示特性能否被派生类继承,例如class GrayWolf : Wolf{},当修饰AnimalInfoAttribute 的AttributeUsageAttribute.Inherited 为true 时,Wolf 类中的AnimalInfoAttribute 特性信息会被派生类GrayWolf 继承。AttributeUsageAttribute 的AllowMultiple 属性用于指示特性能否多次作用于同一程序元素。当AllowMultiple 为true 时,我们可以多次用AnimalInfoAttribute 修饰Wolf 类,例如:
[AnimalInfo("狼", Phylum = "脊索动物门", Classis = "哺乳纲", Familia = "犬科")]
[AnimalInfo("Wolf",Phylum ="Chordata",Classis = "Mammals",Familia = "Canids")]
class Wolf{…}
3.程序集
程序集(Assembly)是组织程序的逻辑单元,编写好的代码最终都会被编译器编译为若干个程序集。需要指出的是程序集只是逻辑上的划分,一个程序集可以只由一个文件组成,也可由多个文件组成。不管是单文件程序集还是多文件程序集,它们都由固定的结构组成。
两种最常见的程序集——可执行文件(.exe)和动态链接库文件(.dll),动态链接库分为早期的“Win32 函数库”、“COM”和最近的“.NET 框架类库”三种,虽然扩展名同为dll,但其含义不同,这里主要学习.NET中的动态链接库。
(1)创建Animals.dll程序集
这个程序中包含两个程序集,一个可执行文件ZooMap.exe,另一个是动态链接库Animals.dll。ZooMap.exe 是主程序,它调用Animals.dll 中的类来绘制动物图像,而Animals.dll 中则包含了几种和动物相关的类,为主程序提供支持。
(2)创建ZooMap.exe程序集
创建一个名为ZooMap的Windows应用程序解决方案,默认就会有一个ZooMap项目,然后再创建一个Animals的项目,此项目下编写5个动物类,继承通用接口IAnimal,均实现其ShowAnimal(Graphics g,int x,int y) 方法,然后将项目生成Animals.dll。最后在ZooMap项目将Animals.dll引用进来,再编写窗体程序,在窗体中添加5个按钮,分别为按钮添加事件处理程序,调用不同动物的ShowAnimal方法,显示动物。至此在ZooMap的Debug文件夹中,便可以看到刚刚做好的两个程序集ZooMap.exe和Animals.dll。
ShowAnimal(Graphics g,int x,int y)
{Bitmap animalImage = new Bitmap(Resource.Panda); //这里以Panda为例
g.DrawImage(animalImage , x, y);}
(3)程序集的结构
任何事物都由一定的结构组成,程序集也不例外,它由程序集元数据、类型元数据、MSIL 代码和资源四部分组成。
①程序集元数据,程序集元数据也叫清单,它记录了程序集的许多重要信息,是程序集进行自我说明的核心文档。当程序运行时,CLR 通过这份清单就能获取运行程序集所必需的全部信息。清单中主要主要包含如下信息:标识信息(包括程序集的名称、版本、文化和公钥等);文件列表(程序集由哪些文件组成);引用程序集列表(该程序集所引用的其他程序集);一组许可请求(运行这个程序集需要的许可)。
②类型元数据,类型元数据列举了程序集中包含的类型信息,详细说明了程序集中定义了哪些类,每个类包含哪些属性和方法,每个方法有哪些参数和返回值类型,等等。
③MSIL代码,程序集元数据和类型元数据只是一些辅助性的说明信息,它们都是为描述MSIL代码而存在的。MSIL 代码是程序集的真正核心部分,正是它们实现了程序集的功能。比如在“Animals”项目中,五个动物类的C#代码最终都被转换为MSIL 代码,保存在程序集Animals.dll 中,当运行程序时,就是通过这些MSIL 代码绘制动物图像的。
④资源,程序集中还可能包含图像、图标、声音等资源。
总之,程序集是自我说明的,这些自我说明文档提供了运行程序集所必须的信息,这样我们不必依靠注册表等外部信息就能运行程序。所有的信息存于一处,这种设计方式也大大简化了程序的安装过程。
需要澄清的是,命名空间与程序集并不总是一一对应的。命名空间可以看作类名的扩展,一个程序集可以包含多个命名空间,一个命名空间也可以分布在多个程序集中。
(4)私有程序集和共享程序集
私有程序集是仅供单个软件使用的程序集,安装很简单,只需把私有程序集复制到软件包所在文件夹中即可。而那些被不同软件共同使用的程序就是共享程序集,.NET类库的程序集就是共享程序集,共享程序集为不同的程序所共用,所以它的部署就不像私有程序集那么简单,必须考虑命名冲突和版本冲突等问题。解决这些问题的办法是把共享程序集放在系统的一个特定文件夹内,这个特定文件夹称为全局程序集高速缓存(GAC)。这个过程可用专门的.NET 工具完成。
(5)程序集的特性
右击“bin\Debug”目录下的可执行文件ZooMap.exe,选择“属性”,可以看到程序集相关的信息,如下图所示,这些信息主要定义在“AssmeblyInfo.cs”文件中,如下图所示。
打开“AssmeblyInfo.cs”文件,会看到如下的代码:
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的常规信息通过下列属性集控制,更改这些属性值可修改
//与程序集关联的信息。
[assembly: AssemblyTitle("ZooMap")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Clear Studio")]
[assembly: AssemblyProduct("ZooMap")]
[assembly: AssemblyCopyright("版权所有 (C) Clear Studio 2008")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 使此程序集中的类型对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型,则将该类型上的 ComVisible 属性设置为 true。
[assembly: ComVisible(false)]
// 如果此项目向 COM 公开,则下列 GUID 是用于类型库的 ID
[assembly: Guid("816a1507-8ca5-438d-87b4-9f3bef5b2481")]
// 程序集的版本信息由下面四个值组成:主版本、次版本、内部版本号、修订号
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
程序集的属性信息是由特性实现的,与普通特性的不同的是,描述程序集的特性前要添加前缀“assembly:”。与程序集相关的一些特性如下表所示:
4.反射
通过.NET 框架的反射机制(Reflection),可以轻松的获取类型和程序集的信息。.NET 在System.Reflection 命名空间中定义了一系列反射类,它们与System 命名空间中的Type 类一起提供了反射功能。利用后两者来获取前者相关信息。
(1)获取类型信息(Type类)
通过 Type 类提供的方法和属性获取某个数据类型的相关信息,例如:
using System.Reflection;
static void Main(string[] args)
{//获取类型
Type type = Type.GetType("System.Int32");
//输出类型的相关信息
Console.WriteLine(" 类型名:" + type.Name);
Console.WriteLine(" 类的全名:" + type.FullName);
Console.WriteLine(" 命名空间名:" + type.Namespace);
Console.WriteLine(" 程序集名:" + type.Assembly.GetName().Name);
Console.WriteLine(" 模块名:" + type.Module);
Console.WriteLine(" 基类名:" + type.BaseType);
Console.WriteLine(" 运行时映射的类名:" + type.UnderlyingSystemType);
Console.WriteLine(" 是否是类:" + type.IsClass);
Console.WriteLine(" 是否是接口:" + type.IsInterface);
Console.WriteLine(" 是否是抽象类:" + type.IsAbstract);
Console.WriteLine(" 是否是数组:" + type.IsArray);
Console.WriteLine(" 是否是值类型:" + type.IsValueType);
Console.WriteLine(" 是否是基本类型:" + type.IsPrimitive);
//获取并输出类的成员
MemberInfo[] members = type.GetMembers();
Console.WriteLine(" 类的公共成员:");
foreach (MemberInfo member in members)
{Console.WriteLine(" :" + member.MemberType + ":" + member.Name);}
}
通过 MemberInfo 等类的属性和方法就可以获取相应成员的具体信息。
(2)获取程序集信息(Assembly类)
System.Reflection 命名空间的Assembly 类是为程序集设计的,通过它的属性和方法,可以获取程序集的元数据信息。获取程序集的元数据之前,需要先用Assembly对象的Load()方法(或LoadFile()、LoadFrom())把程序集加载到内存中。新建一个项目,并把“Animals.dll”动态链接库添加到项目中,然后通过下面的代码就可获取程序集的相关信息。
using System.Reflection;
static void Main(string[] args)
{ //加载程序集
Assembly assembly = Assembly.LoadFrom("Animals.dll");
Console.WriteLine("程序集全名: " + assembly.FullName);
Console.WriteLine("程序集版本: " + assembly.GetName().Version);
Console.WriteLine("公共语言运行时版本: " + assembly.ImageRuntimeVersion);
Type[] types = assembly.GetTypes();//获取并输出程序集中的类型
Console.WriteLine("\n 程序集中的类型:");
foreach (Type type in types)
{Console.WriteLine(type);} }
(3)动态加载类型
以前编写的程序中,对象的类型在编译阶段就已经确定下来了,然而在某些问题中,对象的类型在运行时才能确定,需要我们动态的加载类型。利用反射技术可以轻松实现动态加载类型。为了实现上述功能,这里创建了一个项目,添加了Animals.dll引用,然后在Form1类中编写一个名为DynamicallyDisplay()的方法,传给它一个代表动物名称的参数后,它将动态加载参数所指定的类,并进行绘图。
①方法一:利用接口
using Animals
//在Form1类中添加下列方法,绘图方法(利用动态加载的类)
public void DynamicallyDisplay(string typeName)
{ Graphics graphics = this.CreateGraphics();
Assembly assembly = Assembly.LoadFrom("Animals.dll");//动态加载程序集
Type AnimalType = assembly.GetType(typeName); //获取动物类型
//创建动物实例
IAnimal someone = (IAnimal)Activator.CreateInstance(AnimalType,null);
someone.ShowAnimal(graphics, 75, 50); //调用对象
}
//下拉式列表框的事件处理程序(响应用户点击)
private void comboBox_SelectedIndexChanged(object sender, EventArgs e)
{ string typeName = "Animals." + comboBox.SelectedItem;
DynamicallyDisplay(typeName); }
注意,虽然AnimalType 包含指定动物类的全部信息,但它仍然是一个Type 类的对象,并不是动物类本身。所以不能通过new 运算符创建对象,而需要通过System.Activator 类的CreateInstance()方法创建对象。并且,,通过CreateInstance()方法得到的对象为Object型,需要强制转换为IAnimal 型才能调用ShowAnimal()方法。
②方法二:利用Invoke()方法
利用了所有动物都实现了 IAnimal 接口这一特征,如果我们前面没有定义IAnimal 接口该怎么办呢?不用怕,我们可以通过Type 类的GetMethod()方法获取ShowAnimal()方法的信息。
//绘图方法(利用动态加载的类)
public void DynamicallyDisplay(string typeName)
{Assembly assembly = Assembly.LoadFrom("Animals.dll");
Type AnimalType = assembly.GetType(typeName);
object someone = Activator.CreateInstance(AnimalType); //创建动物实例
//获取类型中的ShowAnimal()方法
MethodInfo methodInfo = AnimalType.GetMethod("ShowAnimal");
//创建方法ShowAnimal()的参数并调用该方法
Graphics graphics=this.CreateGraphics();
object[] args = new object[] {graphics, 75, 50 };
methodInfo.Invoke(someone, args); }
}
③方法三:利用dynamic关键字
//注意添加命名空间
using Animals
//在Form1 类中添加下列方法
//绘图方法(利用动态加载的类)
public void DynamicallyDisplay(string typeName)
{Graphics graphics = this.CreateGraphics();
Assembly assembly = Assembly.LoadFrom("Animals.dll");
Type AnimalType = assembly.GetType(typeName);
//创建动物实例(仔细观察这一句和方法一有和不同)
dynamic someone = Activator.CreateInstance(AnimalType);
someone.ShowAnimal(graphics, 75, 50); //调用对象
}
dynamic关键字是C#4.0 提供的新功能,用它声明的引用符可以接受任何类型的对象, 创建动物实例这条语句中没有像方法一那样进行强制类型转换,现在你知道为什么了吗?这是因为用dynamic 声明的someone 可以接受任何类型的对象。在方法三中,对象someone 在编译阶段是不知道其类型的,它绕过了类型检查,直到程序运行时才确定someone 的类型。
dynamic 关键字更常用于处理来自外界(COM、DLR、HTML DOM、XML、IronPython等)的对象,在这些时候,你很可能不能确定这些对象的具体类型而仅仅知道它的一些属性和方法,你只想调用需要的方法,至于操作对象是什么类型,并不关心。由于C#现在有了dynamic关键字就简单多了,编译器不对dynamic 声明的对象进行类型检查,只需直接调用你所需要的方法就行了,避免了大量的类型转换工作,从而得到干净清爽的代码。