张子阳的博客

首页 读书 技术 店铺 关于
张子阳的博客 首页 读书 技术 关于

.Net中的反射(查看类型信息) - Part.2

2008-01-27 张子阳 分类: .Net 框架

反射概述 和Type类

反射的作用

简单来说,反射提供这样几个能力:1、查看和遍历类型(及其成员)的基本信息和程序集元数据(metadata);2、迟绑定(Late-Binding)方法和属性。3、动态创建类型实例(并可以动态调用所创建的实例的方法、字段、属性)。序章中,我们所采用的那个例子,只是反射的一个用途:查看类型成员信息。接下来的几个章节,我们将依次介绍反射所提供的其他能力。

获取Type对象实例

反射的核心是Type类,这个类封装了关于对象的信息,也是进行反射的入口。当你获得了关于类型的Type对象后,就可以根据Type提供的属性和方法获取这个类型的一切信息(方法、字段、属性、事件、参数、构造函数等)。我们开始的第一步,就是获取关于类型的Type实例。获取Type对象有两种形式,一种是获取当前加载程序集中的类型(Runtime),一种是获取没有加载的程序集的类型。

我们先考虑Runtime时的Type,一般来说有三种获取方法:

使用Type类提供的静态方法GetType()

比如我们想要获得Stream类型的Type实例,则可以这样:

Type t = Type.GetType("System.IO.Stream"); txtOutput.Text = t.ToString();

注意到GetType方法接受字符串形式的类型名称。

使用 typeof 操作符

也可以使用C# 提供的typeof 操作符来完成这一过程:

// 如果在页首写入了using System.IO; 也可以直接用 typeof(Stream); Type t = typeof(System.IO.Stream);

这时的使用有点像泛型,Stream就好像一个类型参数一样,传递到typeof操作符中。

通过类型实例获得Type对象

我们还可以通过类型的实例来获得:

String name = "Jimmy Zhang"; Type t = name.GetType();

使用这种方法时应当注意,尽管我们是通过变量(实例)去获取Type对象,但是Type对象不包含关于这个特定对象的信息,仍是保存对象的类型(String)的信息。

Type类型 及 Reflection命名空间的组织结构

到现在为止,我已经多次提过Type封装了类型的信息,那么这些类型信息都包含什么内容呢?假设我们现在有一个类型的实例,它的名字叫做 demo,我们对它的信息一无所知,并通过下面代码获取了对于它的Type实例:

// 前面某处的代码实例化了demo对象 Type t = demo.GetType();

现在,我们期望 t 包含了关于 demo 的哪些信息呢?

demo的类型的基本信息

Type 提供了下面的属性,用于获取类型的基本信息,常用的有下面一些:

属 性 说 明
Name 获取类型名称
FullName 类型全名
Namespace 命名空间名称
BaseType 获取对于基类的Type类型的引用
UnderlyingSystemType 在.Net中映射的类型的引用
Attributes 获取TypeAttributes位标记
IsValueType 是否值类型
IsByRef 是否由引用传递
IsEnum 是否枚举
IsClass 是否类
IsInterface 是否接口
IsSealed 是否密封类
IsPrimitive 是否基类型(比如int)
IsAbstract 是否抽象
IsPublic 是否公开
IsNotPublic 是否非公开
IsVisible 是否程序集可见
等等...  

demon的类型的成员信息  

观察上面的列表,就拿第一条来说,我们想获取类型都有哪些字段,以及这些字段的信息。而字段都包含哪些信息呢?可能有字段的类型、字段的名称、字段是否public、字段是否为const、字段是否是read only 等等,那么是不是应该将字段的这些信息也封装起来呢?

实际上,.Net中提供了 FiledInfo 类型,它封装了关于字段的相关信息。对照上面的列表,类似的还有 PropertyInfo类型、ConstructorInfo类型、MethodInfo类型、EventInfo类型。而对于方法而言,对于它的参数,也会有in参数,out参数,参数类型等信息,类似的,在 System.Reflection 命名空间下,除了有上面的提到的那么多Info后缀结尾的类型,还有个ParameterInfo 类型,用于封装方法的参数信息。

最后,应该注意到 Type 类型,以及所有的Info类型均 继承自 MemberInfo 类型,MemberInfo类型提供了获取类型基础信息的能力。

在VS2005中键入Type,选中它,再按下F12跳转到Type类型的定义,纵览Type类型的成员,发现可以大致将属性和方法分成这样几组:

由于MemberInfo是一个基类,当我们获得一个MemberInfo后,我们并不知道它是PropertyInfo(封装了属性信息的对象)还是FieldInfo(封装了属性信息的对象),所以,有必要提供一个办法可以让我们加以判断,在Reflection 命名空间中,会遇到很多的位标记,这里先介绍第一个位标记(本文管用[Flags]特性标记的枚举称为 位标记),MemberTypes,它用于标记成员类型,可能的取值如下:

[Flags] public enum MemberTypes { Constructor = 1, // 该成员是一个构造函数 Event = 2, // 该成员是一个事件 Field = 4, // 该成员是一个字段 Method = 8, // 该成员是一个方法 Property = 16, // 该成员是一个属性 TypeInfo = 32, // 该成员是一种类型 Custom = 64, // 自定义成员类型 NestedType = 128, // 该成员是一个嵌套类型 All = 191, // 指定所有成员类型。 }

反射程序集

在.Net中,程序集是进行部署、版本控制的基本单位,它包含了相关的模块和类型,我并不打算详细地去说明程序集及其构成,只是讲述如何通过反射获取程序集信息。

在System.Reflection命名空间下有一个Assembly类型,它代表了一个程序集,并包含了关于程序集的信息。

在程序中加载程序集时,一般有这么几个方法,我们可以使用 Assembly类型提供的静态方法LoadFrom() 和 Load(),比如:

Assembly asm = Assembly.LoadFrom("Demo.dll");

或者

Assembly asm = Assembly.Load("Demo");

当使用LoadFrom()方法的时候,提供的是程序集的文件名,当将一个程序集添加到项目引用中以后,可以直接写“文件名.dll”。如果想加载一个不属于当前项目的程序集,则需要给出全路径,比如:

Assembly asm = Assembly.LoadFrom(@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.Web.dll");

使用Load()方法的时候,只用提供程序集名称即可,不需要提供程序集的后缀名。如果想获得当前程序集,可以使用Assembly类型的静态方法 GetExecutingAssembly,它返回包含当前执行的代码的程序集(也就是当前程序集)。

Assembly as = Assembly.GetExecutingAssembly();

在获得一个Type类型实例以后,我们还可以使用该实例的Assembly属性来获得其所在的程序集:

Type t = typeof(int)
Assembly asm = t.Assembly;

一个程序集可能有多个模块(Module)组成,每个模块又可能包含很多的类型,但.Net的默认编译模式一个程序集只会包含一个模块,我们现在看下 反射 提供了什么样的能力让我们获取关于程序集的信息(只列出了部分常用的):

属 性/方 法 说 明
FullName 程序集名称
Location 程序集的路径
GetTypes() 获取程序集包含的全部类型
GetType() 获取某个类型
GetModules() 获取程序集包含的模块
GetModule() 获取某个模块
GetCustomAttributes() 获取自定义特性信息

程序集和命名空间不存在必然联系,一个程序集可以包含多个命名空间,同一个命名空间也可以分放在几个程序集。

为了方便进行我们后面的测试,我们现在建立一个Windows控制台应用程序,我给它起名叫SimpleExplore;然后再添加一个Demo类库项目,我们将来编写的代码就用户查看这个Demo项目集的类型信息 或者 是对这个程序集中的类型进行迟绑定。这个Demon项目只包含一个命名空间Demo,为了体现尽可能多的类型同时又Keep Simple,其代码如下:

namespace Demo { public abstract class BaseClass { } public struct DemoStruct { } public delegate void DemoDelegate(Object sender, EventArgs e); public enum DemoEnum { terrible, bad, common=4, good, wonderful=8 } public interface IDemoInterface { void SayGreeting(string name); } public interface IDemoInterface2 {} public sealed class DemoClass:BaseClass, IDemoInterface,IDemoInterface2 { private string name; public string city; public readonly string title; public const string text = "Const Field"; public event DemoDelegate myEvent; public string Name { private get { return name; } set { name = value; } } public DemoClass() { title = "Readonly Field"; } public class NestedClass { } public void SayGreeting(string name) { Console.WriteLine("Morning :" + name); } } }

现在我们在 SimpleExplore项目中写一个方法AssemblyExplor(),查看我们Demo项目生成的程序集Demo.dll定义的全部类型:

public static void AssemblyExplore() { StringBuilder sb = new StringBuilder(); Assembly asm = Assembly.Load("Demo"); sb.Append("FullName(全名):" + asm.FullName + "\n"); sb.Append("Location(路径):" + asm.Location + "\n"); Type[] types = asm.GetTypes(); foreach (Type t in types) { sb.Append(" 类型:" + t + "\n"); } Console.WriteLine(sb.ToString()); }

然后,我们在Main()方法中调用一下,应该可以看到这样的输出结果:

FullName(全名):Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null Location(路径):E:\MyApp\TypeExplorer\SimpleExplorer\bin\Debug\Demo.dll 模块: Demo.dll 类型:Demo.BaseClass 类型:Demo.DemoStruct 类型:Demo.DemoDelegate 类型:Demo.DemoEnum 类型:Demo.IDemoInterface 类型:Demo.IDemoInterface2 类型:Demo.DemoClass 类型:Demo.DemoClass+NestedClass

反射基本类型

这里说反射基本类型,基本类型是针对 泛型类型 来说的,因为 反射泛型 会更加复杂一些。在前面的范例中,我们获得了程序集中的所有类型,并循环打印了它们,打印结果仅仅显示出了类型的全名,而我们通常需要关于类型更详细的信息,本节我们就来看看如何进一步查看类型信息。

因为一个程序集包含很多类型,一个类型包含很多成员(方法、属性等),一个成员又包含很多其他的信息,所以如果我们从程序集层次开始写代码去获取每个层级的信息,那么会嵌套很多的foreach语句,为了阅读方便,我会去掉最外层的循环。

获取基本信息

有了前面Type一节的介绍,我想完成这里应该只是打打字而已,所以我直接写出代码,如有必要,会在注释中加以说明。我们再写一个方法TypeExplore,用于获取类型的详细信息(记得AssemblyExplore只获取了类型的名称):

public static void TypeExplore(Type t) { StringBuilder sb = new StringBuilder(); sb.Append("名称信息:\n"); sb.Append("Name: " + t.Name + "\n"); sb.Append("FullName: " + t.FullName + "\n"); sb.Append("Namespace: " + t.Namespace + "\n"); sb.Append("\n其他信息:\n"); sb.Append("BaseType(基类型): " + t.BaseType + "\n"); sb.Append("UnderlyingSystemType: " + t.UnderlyingSystemType + "\n"); sb.Append("\n类型信息:\n"); sb.Append("Attributes(TypeAttributes位标记): " + t.Attributes + "\n"); sb.Append("IsValueType(值类型): " + t.IsValueType + "\n"); sb.Append("IsEnum(枚举): " + t.IsEnum + "\n"); sb.Append("IsClass(类): " + t.IsClass + "\n"); sb.Append("IsArray(数组): " + t.IsArray + "\n"); sb.Append("IsInterface(接口): " + t.IsInterface + "\n"); sb.Append("IsPointer(指针): " + t.IsPointer + "\n"); sb.Append("IsSealed(密封): " + t.IsSealed + "\n"); sb.Append("IsPrimitive(基类型): " + t.IsPrimitive + "\n"); sb.Append("IsAbstract(抽象): " + t.IsAbstract + "\n"); sb.Append("IsPublic(公开): " + t.IsPublic + "\n"); sb.Append("IsNotPublic(不公开): " + t.IsNotPublic + "\n"); sb.Append("IsVisible: " + t.IsVisible + "\n"); sb.Append("IsByRef(由引用传递): " + t.IsByRef + "\n"); Console.WriteLine(sb.ToString()); }

然后,我们在Main方法中输入:

Type t = typeof(DemoClass); TypeExplore(t);

会得到这样的输出:

名称信息: Name: DemoClass FullName: Demo.DemoClass Namespace: Demo 其他信息: BaseType(基类型): Demo.BaseClass UnderlyingSystemType: Demo.DemoClass 类型信息: Attributes(TypeAttributes位标记): AutoLayout, AnsiClass, Class, Public, Sealed, BeforeFieldInit IsValueType(值类型): False IsEnum(枚举): False IsClass(类): True IsArray(数组): False IsInterface(接口): False IsPointer(指针): False IsSealed(密封): True IsPrimitive(基类型): False IsAbstract(抽象): False IsPublic(公开): True IsNotPublic(不公开): False IsVisible: True IsByRef(由引用传递): False

值得注意的是Attributes属性,它返回一个TypeAttributes位标记,这个标记标识了类型的一些元信息,可以看到我们熟悉的Class、Public、Sealed。相应的,IsClass、IsSealed、IsPublic等属性也返回为True。

成员信息 与 MemberInfo 类型

我们先考虑一下对于一个类型Type,可能会包含什么类型,常见的有字段、属性、方法、构造函数、接口、嵌套类型等。MemberInfo 类代表着 Type的成员类型,值得注意的是Type类本身又继承自MemberInfo类,理解起来并不困难,因为一个类型经常也是另一类型的成员。Type类提供 GetMembers()、GetMember()、FindMember()等方法用于获取某个成员类型。

我们再添加一个方法 MemberExplore(),来查看一个类型的所有成员类型。

public static void MemberExplore(Type t) { StringBuilder sb = new StringBuilder(); MemberInfo[] memberInfo = t.GetMembers(); sb.Append("查看类型 " + t.Name + "的成员信息:\n"); foreach (MemberInfo mi in memberInfo) { sb.Append("成员:" + mi.ToString().PadRight(40) + " 类型: " + mi.MemberType + "\n"); } Console.WriteLine(sb.ToString()); }

然后我们在Main方法中调用一下。

MemberExplore(typeof(DemoClass));

产生的输出如下:

查看类型 DemoClass的成员信息: -------------------------------------------------- 成员:Void add_myEvent(Demo.DemoDelegate) 类型: Method 成员:Void remove_myEvent(Demo.DemoDelegate) 类型: Method 成员:System.String get_Name() 类型: Method 成员:Void set_Name(System.String) 类型: Method 成员:Void SayGreeting(System.String) 类型: Method 成员:System.Type GetType() 类型: Method 成员:System.String ToString() 类型: Method 成员:Boolean Equals(System.Object) 类型: Method 成员:Int32 GetHashCode() 类型: Method 成员:Void .ctor() 类型: Constructor 成员:System.String Name 类型: Property 成员:Demo.DemoDelegate myEvent 类型: Event 成员:System.String text 类型: Field 成员:Demo.DemoClass+NestedClass 类型: NestedType

我们使用了GetMembers()方法获取了成员信息的一个数组,然后遍历了数组,打印了成员的名称和类型。如同我们所知道的:Name属性在编译后成为了get_Name()和set_Name()两个独立的方法;myEvent事件的注册(+=)和取消注册(-=)分别成为了add_myEvent()和remove_myEvent方法。同时,我们发现私有(private)字段name 没有被打印出来,另外,基类System.Object的成员GetType()和Equals()也被打印了出来。

有的时候,我们可能不希望查看基类的成员,也可能希望查看私有的成员,此时可以使用GetMembers()的重载方法,传入BindingFlags 位标记参数来完成。BindingFlags位标记对如何获取成员的方式进行控制(也可以控制如何创建对象实例,后面会说明)。对于本例,如果我们想获取所有的公有、私有、静态、实例 成员,那么只需要这样修改GetMembers()方法就可以了。

MemberInfo[] memberInfo = t.GetMembers( BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly );

此时的输出如下:

查看类型 DemoClass的成员信息: -------------------------------------------------- 成员:Void add_myEvent(Demo.DemoDelegate) 类型: Method 成员:Void remove_myEvent(Demo.DemoDelegate) 类型: Method 成员:System.String get_Name() 类型: Method 成员:Void set_Name(System.String) 类型: Method 成员:Void SayGreeting(System.String) 类型: Method 成员:Void .ctor() 类型: Constructor 成员:System.String Name 类型: Property 成员:Demo.DemoDelegate myEvent 类型: Event 成员:System.String name 类型: Field 成员:Demo.DemoDelegate myEvent 类型: Field 成员:System.String text 类型: Field 成员:Demo.DemoClass+NestedClass 类型: NestedType

可以看到,继承自基类 System.Object 的方法都被过滤掉了,同时,打印出了私有的 name, myEvent 等字段。

现在如果我们想要获取所有的方法(Method),那么我们可以使用 Type类的FindMembers()方法:

MemberInfo[] memberInfo = t.FindMembers( MemberTypes.Method, // 说明查找的成员类型为 Method BindingFlags.Public | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, Type.FilterName, "*" );

Type.FilterName 返回一个MemberFilter类型的委托,它说明按照方法名称进行过滤,最后一个参数“*”,说明返回所有名称(如果使用“Get*”,则会返回所有以Get开头的方法)。现在的输出如下:

查看类型 DemoClass的成员信息: -------------------------------------------------- 成员:Void add_myEvent(Demo.DemoDelegate) 类型: Method 成员:Void remove_myEvent(Demo.DemoDelegate) 类型: Method 成员:System.String get_Name() 类型: Method 成员:Void set_Name(System.String) 类型: Method 成员:Void SayGreeting(System.String) 类型: Method

MemberInfo 类有两个属性值得注意,一个是DeclaringType,一个是 ReflectedType,返回的都是Type类型。DeclaredType 返回的是声明该成员的类型。比如说,回顾我们之前的一段代码:

MemberInfo[] members = typeof(DemoClass).GetMembers();

它将返回所有的公有成员,包括继承自基类的Equals()等方法,对于Equals()方法来说,它的 DeclaringType 返回的是相当于 typeof(Object) 的类型实例,因为它是在 System.Object中被定义的;而它的ReflectedType 返回的则是相当于 typeof(DemoClass) 类型实例,因为它是通过 DemoClass 的类型实例被获取的。

字段信息 与 FieldInfo类型

如同我们之前所说,MemberInfo 是一个基类,它包含的是类型的各种成员都公有的一组信息。实际上,对于字段、属性、方法、事件 等类型成员来说,它们包含的信息显然都是不一样的,所以,.Net 中提供了 FiledInfo 类型来封装字段的信息,它继承自MemberInfo。

如果我们希望获取一个类型的所有字段,可以使用 GetFileds()方法。我们再次添加一个方法FieldExplore():

public static void FieldExplore(Type t) { StringBuilder sb = new StringBuilder(); FieldInfo[] fields = t.GetFields(); sb.Append("查看类型 " + t.Name + "的字段信息:\n"); sb.Append(String.Empty.PadLeft(50, '-') + "\n"); foreach (FieldInfo fi in fields) { sb.Append("名称:" + fi.Name + "\n"); sb.Append("类型:" + fi.FieldType + "\n"); sb.Append("属性:" + fi.Attributes + "\n\n"); } Console.WriteLine(sb.ToString()); }

产生的输出如下:

查看类型 DemoClass的字段信息: -------------------------------------------------- 名称:city 类型:System.String 属性:Public 名称:title 类型:System.String 属性:Public, InitOnly 名称:text 类型:System.String 属性:Public, Static, Literal, HasDefault

值得一提的是fi.FieldType 属性,它返回一个FieldAttributes位标记,这个位标记包含了字段的属性信息。对比我们之前定义的DemoClass类,可以看到,对于title 字段,它的属性是public, InitOnly;对于Const类型的text字段,它的属性为Public,Static,Literal,HasDefault,由此也可以看出,声明一个const类型的变量,它默认就是静态static的,同时,由于我们给了它初始值,所以位标记中也包括HasDefault。

针对于FieldType位标记,FiledInfo 类提供了一组返回为bool类型的属性,来说明字段的信息,常用的有:IsPublic, IsStatic, IsInitOnly, IsLiteral, IsPrivate 等。

如果我们想要获取私有字段信息,依然可以使用重载了的GetFields[]方法,传入BindingFlags参数,和上面的类似,这里就不重复了。

属性信息 与 PropertyInfo 类型

和字段类似,也可以通过 GetProperty()方法,获取类型的所有属性信息。

public static void PropertyExplore(Type t) { StringBuilder sb = new StringBuilder(); sb.Append("查看类型 " + t.Name + "的属性信息:\n"); sb.Append(String.Empty.PadLeft(50, '-') + "\n"); PropertyInfo[] properties = t.GetProperties(); foreach (PropertyInfo pi in properties) { sb.Append("名称:" + pi.Name + "\n"); sb.Append("类型:" + pi.PropertyType + "\n"); sb.Append("可读:" + pi.CanRead + "\n"); sb.Append("可写:" + pi.CanWrite +"\n"); sb.Append("属性:" + pi.Attributes +"\n"); } Console.WriteLine(sb.ToString()); }

输出如下:

查看类型 DemoClass的属性信息: -------------------------------------------------- 名称:Name 类型:System.String 可读:True 可写:True 属性:None

从前面的章节可以看到,Name属性会在编译后生成Get_Name()和Set_Name()两个方法,那么,应该可以利用反射获取这两个方法。PropertyInfo类的GetGetMethod()和GetSetMethod()可以完成这个工作,它返回一个MethodInfo对象,封装了关于方法的信息,我们会在后面看到。

方法信息 与 MethodInfo 类型

与前面的类似,我们依然可以编写代码来查看类型的方法信息。

public static void MethodExplore(Type t) { StringBuilder sb = new StringBuilder(); sb.Append("查看类型 " + t.Name + "的方法信息:\n"); sb.Append(String.Empty.PadLeft(50, '-') + "\n"); MethodInfo[] methods = t.GetMethods(); foreach (MethodInfo method in methods) { sb.Append("名称:" + method.Name +"\n"); sb.Append("签名:" + method.ToString() + "\n"); sb.Append("属性:" + method.Attributes + "\n"); sb.Append("返回值类型:" + method.ReturnType + "\n\n"); } Console.WriteLine(sb.ToString()); }

与前面类似,MethodInfo 类也有一个Attributes属性,它返回一个MethodAttribute,MethodAttribute 位标记标明了方法的一些属性,常见的比如Abstract, Static, Virtual,Public, Private 等。

与前面不同的是,Method可以具有参数 和 返回值,MethodInfo 类提供了 GetParameters() 方法获取 参数对象的数组,方法的参数都封装在了 ParameterInfo 类型中。查看ParameterInfo类型的方法与前面类似,这里就不再阐述了。

ConstructorInfo类型、EventInfo 类型

从名称就可以看出来,这两个类型封装了类型 的构造函数 和 事件信息,大家都是聪明人,查看这些类型与之前的方法类似,这里就不再重复了。

小结

本文涉及了反射的最基础的内容,我们可以利用反射来自顶向下地查看程序集、模块、类型、类型成员的信息。反射更强大、也更有意思的内容:迟绑定方法、动态创建类型以后会再讲到。

感谢阅读,希望这篇文章能给你带来帮助!