.Net 中的反射(查看基本类型信息) - Part.2
反射概述 和Type类
1.反射的作用
简单来说,反射提供这样几个能力:1、查看和遍历类型(及其成员)的基本信息和程序集元数据(metadata);2、迟绑定(Late-Binding)方法和属性。3、动态创建类型实例(并可以动态调用所创建的实例的方法、字段、属性)。序章中,我们所采用的那个例子,只是反射的一个用途:查看类型成员信息。接下来的几个章节,我们将依次介绍反射所提供的其他能力。
2.获取Type对象实例
反射的核心是Type类,这个类封装了关于对象的信息,也是进行反射的入口。当你获得了关于类型的Type对象后,就可以根据Type提供的属性和方法获取这个类型的一切信息(方法、字段、属性、事件、参数、构造函数等)。我们开始的第一步,就是获取关于类型的Type实例。获取Type对象有两种形式,一种是获取当前加载程序集中的类型(Runtime),一种是获取没有加载的程序集的类型。
我们先考虑Runtime时的Type,一般来说有三种获取方法:
2.1使用Type类提供的静态方法GetType()
比如我们想要获得Stream类型的Type实例,则可以这样:
Type t = Type.GetType("System.IO.Stream");
txtOutput.Text = t.ToString();
注意到GetType方法接受字符串形式的类型名称。
2.2 使用 typeof 操作符
也可以使用C# 提供的typeof 操作符来完成这一过程:
// 如果在页首写入了using System.IO; 也可以直接用 typeof(Stream);
Type t = typeof(System.IO.Stream);
这时的使用有点像泛型,Stream就好像一个类型参数一样,传递到typeof操作符中。
2.3 通过类型实例获得Type对象
我们还可以通过类型的实例来获得:
String name = "Jimmy Zhang";
Type t = name.GetType();
使用这种方法时应当注意,尽管我们是通过变量(实例)去获取Type对象,但是Type对象不包含关于这个特定对象的信息,仍是保存对象的类型(String)的信息。
3.Type类型 及 Reflection命名空间的组织结构
到现在为止,我已经多次提过Type封装了类型的信息,那么这些类型信息都包含什么内容呢?假设我们现在有一个类型的实例,它的名字叫做 demo,我们对它的信息一无所知,并通过下面代码获取了对于它的Type实例:
// 前面某处的代码实例化了demo对象
Type t = demo.GetType();
现在,我们期望 t 包含了关于 demo 的哪些信息呢?
3.1 demo的类型的基本信息
- 我们当然首先想知道 demo 是什么类型的,也就是 demo 的类型名称。
- 我们还想知道该类型位于什么命名空间下。
- 它的基类型是什么,以及它在.Net运行库中的映射类型。
- 它是值类型还是引用类型。
- 它是不是Public的。
- 它是枚举、是类、是数组、还是接口。
- 它是不是基础类型(int等)。
- 等等 ...
Type 提供了下面的属性,用于获取类型的基本信息,常用的有下面一些:
属 性 | 说 明 |
Name | 获取类型名称 |
FullName | 类型全名 |
Namespace | 命名空间名称 |
BaseType | 获取对于基类的Type类型的引用 |
UnderlyingSystemType | 在.Net中映射的类型的引用 |
Attributes | 获取TypeAttributes位标记 |
IsValueType | 是否值类型 |
IsByRef | 是否由引用传递 |
IsEnum | 是否枚举 |
IsClass | 是否类 |
IsInterface | 是否接口 |
IsSealed | 是否密封类 |
IsPrimitive | 是否基类型(比如int) |
IsAbstract | 是否抽象 |
IsPublic | 是否公开 |
IsNotPublic | 是否非公开 |
IsVisible | 是否程序集可见 |
等等... |
3.2 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类型的成员,发现可以大致将属性和方法分成这样几组:
- IsXXXX,比如 IsAbstract,这组bool属性用于说明类型的某个信息。(前面的表格已经列举了一些。)
- GetXXXX(),比如GetField(),返回FieldInfo,这组方法用于获取某个成员的信息。
- GetXXXXs(),比如GetFields(),返回FieldInfo[],这组方法用户获取某些成员信息。
- 还有其他的一些属性和方法,等后面遇到了再说。
由于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() | 获取自定义特性信息 |
NOTE:程序集和命名空间不存在必然联系,一个程序集可以包含多个命名空间,同一个命名空间也可以分放在几个程序集。
为了方便进行我们后面的测试,我们现在建立一个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=
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
反射基本类型
这里说反射基本类型,基本类型是针对 泛型类型 来说的,因为 反射泛型 会更加复杂一些。在前面的范例中,我们获得了程序集中的所有类型,并循环打印了它们,打印结果仅仅显示出了类型的全名,而我们通常需要关于类型更详细的信息,本节我们就来看看如何进一步查看类型信息。
NOTE:因为一个程序集包含很多类型,一个类型包含很多成员(方法、属性等),一个成员又包含很多其他的信息,所以如果我们从程序集层次开始写代码去获取每个层级的信息,那么会嵌套很多的foreach语句,为了阅读方便,我会去掉最外层的循环。
1.获取基本信息
有了前面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。
2.成员信息 与 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 的类型实例被获取的。
3.字段信息 与 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参数,和上面的类似,这里就不重复了。
4.属性信息 与 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对象,封装了关于方法的信息,我们会在后面看到。
5.方法信息 与 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类型的方法与前面类似,这里就不再阐述了。
4. ConstructorInfo类型、EventInfo 类型
从名称就可以看出来,这两个类型封装了类型 的构造函数 和 事件信息,大家都是聪明人,查看这些类型与之前的方法类似,这里就不再重复了。
5.小结
本文涉及了反射的最基础的内容,我们可以利用反射来自顶向下地查看程序集、模块、类型、类型成员的信息。反射更强大、也更有意思的内容:迟绑定方法、动态创建类型以后会再讲到。