《C#高级编程.第13章反 射教学内容.doc》由会员分享,可在线阅读,更多相关《C#高级编程.第13章反 射教学内容.doc(62页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、Good is good, but better carries it.精益求精,善益求善。C#高级编程.第13章 反 射-第13章反射反射是一个普通术语,描述了在运行过程中检查和处理程序元素的功能。例如,反射允许完成以下任务:枚举类型的成员实例化新对象执行对象的成员查找类型的信息查找程序集的信息检查应用于类型的定制特性创建和编译新程序集这个列表列出了许多功能,包括.NETFramework类库提供的一些最强大、最复杂的功能。但本章不可能介绍反射的所有功能,仅讨论最常用的功能。首先讨论定制特性,定制特性允许把定制的元数据与程序元素关联起来。这些元数据是在编译过程中创建的,并嵌入到程序集中。接着
2、就可以在运行期间使用反射的一些功能检查这些元数据了。在介绍了定制特性后,本章将探讨支持反射的一些基类,包括System.Type和System.Reflection.Assembly类,它们可以访问反射提供的许多功能。为了演示定制特性和反射,我们将开发一个示例,说明公司如何定期升级软件,自动解释升级的信息。在这个示例中,要定义几个定制特性,表示程序元素最后修改或创建的日期,以及发生了什么变化。然后使用反射开发一个应用程序,在程序集中查找这些特性,自动显示软件自某个给定日期以来升级的所有信息。本章要讨论的另一个示例是一个应用程序,该程序读写数据库,并使用定制特性,把类和特性标记为对应的数据库表和
3、列。然后在运行期间从程序集中读取这些特性,使程序可以自动从数据库的相应位置检索或写入数据,无需为每个表或列编写特定的逻辑。第部分C#语言13.1定制特性前面介绍了如何在程序的各个数据项上定义特性。这些特性都是Microsoft定义好的,作为.NETFramework类库的一部分,许多特性都得到了C#编译器的支持。对于这些特性,编译器可以以特殊的方式定制编译过程,例如,可以根据StructLayout特性中的信息在内存中布置结构。.NETFramework也允许用户定义自己的特性。显然,这些特性不会影响编译过程,因为编译器不能识别它们,但这些特性在应用于程序元素时,可以在编译好的程序集中用作元数
4、据。这些元数据在文档说明中非常有用。但是,使定制特性非常强大的因素是使用反射,代码可以读取这些元数据,使用它们在运行期间作出决策,也就是说,定制特性可以直接影响代码运行的方式。例如,定制特性可以用于支持对定制许可类进行声明代码访问安全检查,把信息与程序元素关联起来,由测试工具使用,或者在开发可扩展的架构时,允许加载插件或模块。13.1.1编写定制特性为了理解编写定制特性的方式,应了解一下在编译器遇到代码中某个应用了定制特性的元素时,该如何处理。以数据库为例,假定有一个C#属性声明,如下所示。FieldName(SocialSecurityNumber)publicstringSocialSec
5、urityNumberget/etc.当C#编译器发现这个属性有一个特性FieldName时,首先会把字符串Attribute添加到这个名称的后面,形成一个组合名称FieldNameAttribute,然后在其搜索路径的所有命名空间(即在using语句中提及的命名空间)中搜索有指定名称的类。但要注意,如果用一个特性标记数据项,而该特性的名称以字符串Attribute结尾,编译器就不会把该字符串加到组合名称中,而是不修改该特性名。因此,上面的代码实际上等价于:FieldNameAttribute(SocialSecurityNumber)publicstringSocialSecurityNum
6、berget/etc.编译器会找到含有该名称的类,且这个类直接或间接派生自System.Attribute。编译器还认为这个类包含控制特性用法的信息。特别是属性类需要指定:特性可以应用到哪些程序元素上(类、结构、属性和方法等)它是否可以多次应用到同一个程序元素上特性在应用到类或接口上时,是否由派生类和接口继承322第12章反射这个特性有哪些必选和可选参数如果编译器找不到对应的特性类,或者找到一个这样的特性类,但使用特性的方式与特性类中的信息不匹配,编译器就会产生一个编译错误。例如,如果特性类指定该特性只能应用于字段,但我们把它应用到结构定义上,就会产生一个编译错误。继续上面的示例,假定定义了一
7、个FieldName特性:AttributeUsage(AttributeTargets.Property,AllowMultiple=false,Inherited=false)publicclassFieldNameAttribute:Attributeprivatestringname;publicFieldNameAttribute(stringname)this.name=name;下面几节讨论这个定义中的每个元素。1.AttributeUsage特性要注意的第一个问题是特性(attribute)类本身用一个特性System.AttributeUsage来标记。这是Microsoft
8、定义的一个特性,C#编译器为它提供了特殊的支持(AttributeUsage根本不是一个特性,它更像一个元特性,因为它只能应用到其他特性上,不能应用到类上)。AttributeUsage主要用于表示定制特性可以应用到哪些类型的程序元素上。这些信息由它的第一个参数给出,该参数是必选的,其类型是枚举类型AttributeTargets。在上面的示例中,指定FieldName特性只能应用到属性(property)上这是因为我们在前面的代码段中把它应用到属性上。AttributeTargets枚举的成员如下:AllAssemblyClassConstructorDelegateEnumEventFie
9、ldGenericParameter(仅.NET2.0提供)InterfaceMethodModuleParameterProperty323第部分C#语言ReturnValueStruct这个列表列出了可以应用该特性的所有程序元素。注意在把特性应用到程序元素上时,应把特性放在元素前面的方括号中。但是,在上面的列表中,有两个值不对应于任何程序元素:Assembly和Module。特性可以作为一个整体应用到程序集或模块中,而不是应用到代码中的一个元素上,在这种情况下,这个特性可以放在源代码的任何地方,但需要用关键字assembly或module来做前缀:assembly:SomeAssembly
10、Attribute(Parameters)module:SomeAssemblyAttribute(Parameters)在指定定制特性的有效目标元素时,可以使用按位OR运算符把这些值组合起来。例如,如果指定FieldName特性可以应用到属性和字段上,可以编写下面的代码:AttributeUsage(AttributeTargets.Property|AttributeTargets.Field,AllowMultiple=false,Inherited=false)publicclassFieldNameAttribute:Attribute也可以使用AttributeTargets.Al
11、l指定特性可以应用到所有类型的程序元素上。AttributesUsage特性还包含另外两个参数AllowMultiple和Inherited。它们用不同的语法来指定:=,而不是只给出这些参数的值。这些参数是可选的,如果需要,可以忽略它们。AllowMultiple参数表示一个特性是否可以多次应用到同一项上,这里把它设置为false,表示如果编译器遇到下述代码,就会产生一个错误:FieldName(SocialSecurityNumber)FieldName(NationalInsuranceNumber)publicstringSocialSecurityNumber/etc.如果Inheri
12、ted参数设置为true,就表示应用到类或接口上的特性也可以自动应用到所有派生的类或接口上。如果特性应用到方法或属性上,也可以自动应用到该方法或属性的重载上。2.指定特性参数下面介绍如何指定定制特性的参数。在编译器遇到下述语句时:FieldName(SocialSecurityNumber)publicstringSocialSecurityNumber/etc.会检查传送给特性的参数(在本例中,是一个字符串),并查找该特性中带这些参数的324第12章反射构造函数。如果找到一个这样的构造函数,编译器就会把指定的元数据传送给程序集。如果找不到,就生成一个编译错误。如后面所述,反射会从程序集中读取
13、元数据,并实例化它们表示的特性类。因此,编译器需要确保存在这样的构造函数,才能在运行期间实例化指定的特性。在本例中,仅为FieldNameAttribute提供了一个构造函数,而这个构造函数有一个字符串参数。因此,在把FieldNameAttribute特性应用到一个属性上时,必须为它提供一个字符串参数,如上面的代码所示。如果可以选择特性的参数类型,当然可以提供构造函数的不同重载方法,但一般是仅提供一个构造函数,使用属性来定义其他可选参数,下面将介绍可选参数。3.指定特性的可选参数在AttributeUsage特性中,可以使用另一个语法,把可选参数添加到特性中。这个语法指定可选参数的名称和值,
14、处理特性类中的公共属性或字段。例如,假定修改SocialSecurityNumber属性的定义,如下所示:FieldName(SocialSecurityNumber,Comment=Thisistheprimarykeyfield)publicstringSocialSecurityNumber/etc.在本例中,编译器识别第二个参数的语法=,所以不会把这个参数传递给FieldNameAttribute构造函数,而是查找一个有该名称的公用属性或字段(最好不要使用公用字段,所以一般情况下要使用属性),编译器可以用这个属性设置第二个参数的值。如果希望上面的代码工作,必须给FieldNameAtt
15、ribute添加一些代码:AttributeUsage(AttributeTargets.Property,AllowMultiple=false,Inherited=false)publicclassFieldNameAttribute:Attributeprivatestringcomment;publicstringCommentgetreturncomment;setcomment=value;/etc.325第部分C#语言13.1.2定制特性示例:WhatsNewAttributes本节开始编写前面描述过的示例WhatsNewAttributes,该示例提供了一个特性,表示最后一次修
16、改程序元素的时间。这个示例比前面所有的示例都复杂,因为它包含3个不同的程序集:WhatsNewAttributes程序集,它包含特性的定义。VectorClass程序集,包含所应用的特性的代码。LookUpWhatsNew程序集,包含显示已改变的数据项信息的项目。当然,只有LookUpWhatsNew是前面使用的一个控制台应用程序,其余两个程序集都是库文件,它们都包含类的定义,但都没有程序的入口。对于VectorClass程序集,我们使用了VectorAsCollection示例,但删除了入口和测试代码类,只剩下Vector类。在命令行上编译,以此管理3个相关的程序集要求较高的技巧,所以我们分
17、别给出编译这3个源文件的命令。也可以编辑代码示例,(可以从WroxPress网站上下载),组合为一个VisualStudio2005解决方案,详见第14章。下载的文件包含所需的VisualStudio2005解决方案文件。1.WhatsNewAttributes库程序集首先从核心的WhatsNewAttributes程序集开始。其源代码包含在文件WhatsNewAttributes.cs中,该文件位于本章示例代码的WhatsNewAttributes解决方案的WhatsNewAttributes项目中。编译为库的语法非常简单:在命令行上,给编译器提供标记target:library即可。要编译
18、WhatsNewAttributes,键入:csc/target:libraryWhatsNewAttributes.csWhatsNewAttributes.cs文件定义了两个特性类LastModifiedAttribute和SupportsWhatsNew-Attribute。LastModifiedAttribute特性可以用于标记最后一次修改数据项的时间,它有两个必选参数(该参数传递给构造函数);修改的日期和包含描述修改的字符串。它还有一个可选参数Issues(表示存在一个公共属性),它可以描述该数据项的任何重要问题。在现实生活中,或许想把特性应用到任何对象上。为了使代码比较简单,这里
19、仅允许将它应用于类和方法,并允许它多次应用到同一项上(AllowMultiple=true),因为可以多次修改一个项,每次修改都需要用一个不同的特性实例来标记。SupportsWhatsNew是一个较小的类,表示不带任何参数的特性。这个特性是一个程序集的特性,用于把程序集标记为通过LastModifiedAttribute维护的文档说明书。这样,以后查看这个程序集的程序会知道,它读取的程序集是我们使用自动文档说明过程生成的那个程序集。这部分示例的完整源代码如下所示:usingSystem;namespaceWrox.ProCSharp.WhatsNewAttributesAttributeUs
20、age(AttributeTargets.Class|AttributeTargets.Method,AllowMultiple=true,Inherited=false)326第12章反射publicclassLastModifiedAttribute:AttributeprivateDateTimedateModified;privatestringchanges;privatestringissues;publicLastModifiedAttribute(stringdateModified,stringchanges)this.dateModified=DateTime.Parse(
21、dateModified);this.changes=changes;publicDateTimeDateModifiedgetreturndateModified;publicstringChangesgetreturnchanges;publicstringIssuesgetreturnissues;setissues=value;AttributeUsage(AttributeTargets.Assembly)publicclassSupportsWhatsNewAttribute:Attribute从上面的描述可以看出,上面的代码非常简单。但要注意,不必将set访问器提供给Change
22、s和DateModified属性,不需要这些访问器是因为在构造函数中,这些参数都是必选参数。需要get访问器,是因为以后可以读取这些特性的值。327第部分C#语言2.VectorClass程序集本节就使用这些特性,我们用前面的VectorAsCollection示例的修订版本来说明。注意这里需要引用刚才创建的WhatsNewAttributes库,还需要使用using语句指定相应的命名空间,这样编译器才能识别出这些特性:usingSystem;usingSystem.Collections;usingSystem.Text;usingWrox.ProCSharp.WhatsNewAttribu
23、tes;assembly:SupportsWhatsNew在这段代码中,添加了一行用SupportsWhatsNew特性标记程序集本身的代码。下面考虑Vector类的代码。我们并不是真的要修改这个类中的任何内容,只是添加两个LastModified特性,以标记出本章对Vector类进行的操作。把Vector定义为一个类,而不是结构,以简化后面显示特性所编写的代码(在VectorAsCollection示例中,Vector是一个结构,但其枚举器是一个类。于是,这个示例的下一个版本在查看程序集时,必须同时考虑类和结构。这会使例子比较复杂)。namespaceWrox.ProCSharp.Vecto
24、rClassLastModified(14Feb2007,IEnumerableinterfaceimplemented+SoVectorcannowbetreatedasacollection)LastModified(10Feb2007,IFormattableinterfaceimplemented+SoVectornowrespondstoformatspecifiersNandVE)classVector:IFormattable,IEnumerablepublicdoublex,y,z;publicVector(doublex,doubley,doublez)this.x=x;th
25、is.y=y;this.z=z;LastModified(10Feb2002,Methodaddedinordertoprovideformattingsupport)publicstringToString(stringformat,IFormatProviderformatProvider)if(format=null)returnToString();再把包含的VectorEnumerator类标记为new:LastModified(14Feb2007,ClasscreatedaspartofcollectionsupportforVector)328第12章反射privateclass
26、VectorEnumerator:IEnumerator为了在命令行上编译这段代码,应键入下面的命令:csc/target:library/reference:WhatsNewAttributes.dllVectorClass.cs上面是这个示例的代码。目前还不能运行它,因为我们只有两个库。在描述了反射的工作原理后,就介绍这个示例的最后一部分,查找和显示这些特性。13.2反射本节先介绍System.Type类,通过这个类可以访问任何给定数据类型的信息。然后简要介绍System.Reflection.Assembly类,它可以用于访问给定程序集的信息,或者把这个程序集加载到程序中。最后把本节的代
27、码和上一节的代码结合起来,完成WhatsNewAttributes示例。13.2.1System.Type类在本书中的许多场合中都使用了Type类,但它只存储类型的引用:Typet=typeof(double)我们以前把Type看作一个类,但它实际上是一个抽象的基类。只要实例化了一个Type对象,就实例化了Type的一个派生类。Type有与每种数据类型对应的派生类,但一般情况下派生的类只提供各种Type方法和属性的不同重载,返回对应数据类型的正确数据。一般不增加新的方法或属性。获取指向给定类型的Type引用有3种常用方式:使用C#的typeof运算符,如上所示。这个运算符的参数是类型的名称(不
28、放在引号中)。使用GetType()方法,所有的类都会从System.Object继承这个类。doubled=10;Typet=d.GetType();在一个变量上调用GetType(),而不是把类型的名称作为其参数。但要注意,返回的Type对象仍只与该数据类型相关:它不包含与类型实例相关的任何信息。如果有一个对象引用,但不能确保该对象实际上是哪个类的实例,这个方法也是很有用的。还可以调用Type类的静态方法GetType():Typet=Type.GetType(System.Double);Type是许多反射技术的入口。它执行许多方法和属性,这里不可能列出所有的方法和属性,而主要介绍如何使
29、用这个类。注意,可用的属性都是只读的:可以使用Type确定329第部分C#语言数据的类型,但不能使用它修改该类型!1.Type的属性由Type执行的属性可以分为下述3类:有许多属性都可以获取包含与类相关的各种名称的字符串,如表12-1所示。表12-1属性返回值Name数据类型名FullName数据类型的完全限定名(包括命名空间名)Namespace定义数据类型的命名空间名属性还可以进一步获取Type对象的引用,这些引用表示相关的类,如表12-2所示。表12-2属性返回对应的Type引用BaseType这个Type的直接基本类型UnderlyingSystemType这个Type在.NET运行库
30、中映射的类型(某些.NET基类实际上映射由IL识别的特定预定义类型)许多Boolean属性表示这个类型是一个类、还是一个枚举等。这些属性包括IsAbstract、IsArray、IsClass、IsEnum、IsInterface、IsPointer、IsPrimitive(一种预定义的基本数据类型)、IsPublic、IsSealed和IsValueType例如,使用一个基本数据类型:TypeintType=typeof(int);Console.WriteLine(intType.IsAbstract);/writesfalseConsole.WriteLine(intType.IsCla
31、ss);/writesfalseConsole.WriteLine(intType.IsEnum);/writesfalseConsole.WriteLine(intType.IsPrimitive);/writestrueConsole.WriteLine(intType.IsValueType);/writestrue或者使用Vector类:TypeintType=typeof(Vector);Console.WriteLine(intType.IsAbstract);/writesfalseConsole.WriteLine(intType.IsClass);/writestrueCon
32、sole.WriteLine(intType.IsEnum);/writesfalseConsole.WriteLine(intType.IsPrimitive);/writesfalseConsole.WriteLine(intType.IsValueType);/writesfalse也可以获取定义类型的程序集的引用,该引用作为System.Reflection.Assembly类实例的一个引用来返回:330第12章反射Typet=typeof(Vector);AssemblycontainingAssembly=newAssembly(t);2.方法System.Type的大多数方法都用
33、于获取对应数据类型的成员信息:构造函数、属性、方法和事件等。它有许多方法,但它们都有相同的模式。例如,有两个方法可以获取数据类型的方法信息:GetMethod()和GetMethods()。GetMethod()方法返回System.Reflection.MethodInfo对象的一个引用,其中包含一个方法的信息。GetMethods()返回这种引用的一个数组。其区别是GetMethods()返回所有方法的信息,而GetMethod()返回一个方法的信息,其中该方法包含特定的参数列表。这两个方法都有重载方法,该重载方法有一个附加的参数,即BindingFlags枚举值,表示应返回哪些成员,例如
34、,返回公有成员、实例成员和静态成员等。例如,GetMethods()最简单的一个重载方法不带参数,返回数据类型所有公共方法的信息:Typet=typeof(double);MethodInfomethods=t.GetMethods();foreach(MethodInfonextMethodinmethods)/etc.Type的成员方法如表12-3所示遵循同一个模式。表12-3返回的对象类型方法(名称为复数形式的方法返回一个数组)ConstructorInfoGetConstructor(),GetConstructors()EventInfoGetEvent(),GetEvents()F
35、ieldInfoGetField(),GetFields()InterfaceInfoGetInterface(),GetInterfaces()MemberInfoGetMember(),GetMembers()MethodInfoGetMethod(),GetMethods()PropertyInfoGetProperty(),GetProperties()GetMember()和GetMembers()方法返回数据类型的一个或所有成员的信息,这些成员可以是构造函数、属性和方法等。最后要注意,可以调用这些成员,其方式是调用Type的InvokeMember()方法,或者调用MethodIn
36、fo,PropertyInfo和其他类的Invoke()方法。13.2.2TypeView示例下面用一个短小的示例TypeView来说明Type类的一些功能,这个示例可以列出数据类331第部分C#语言型的所有成员。本例中主要介绍double型的TypeView用法,也可以修改该样列中的一行代码,使用其他的数据类型。TypeView提供的信息要比在控制台窗口中显示的信息多得多,所以我们将打破常规,在一个消息框中显示这些信息。运行double型的TypeView示例,结果如图12-1所示。图12-1该消息框显示了数据类型的名称、全名和命名空间,以及底层类型和基类的名称。然后迭代该数据类型的所有公有
37、实例成员,显示所声明类型的每个成员、成员的类型(方法、字段等)以及成员的名称。声明类型是实际声明类型成员的类名(换言之,如果在System.Double中定义或重载,该声明类型就是System.Double,如果成员继承了某个基类,该声明类就是相关基类的名称)。TypeView不会显示方法的签名,因为我们是通过MemberInfo对象获取所有公有实例成员的信息,参数信息不能通过MemberInfo对象来获得。为了获取该信息,需要引用MemberInfo和其他更特殊的对象,即需要分别获取每一个成员类型的信息。TypeView会显示所有公有实例成员的信息,但对于double来说,仅定义了字段和方法
38、。把TypeView编译为一个控制台应用程序,可以在控制台应用程序中显示消息框。但是,使用消息框就意味着需要引用基类程序集System.Windows.Forms.dll,它包含System.Windows.Forms命名空间中的类,在这个命名空间中,定义了我们需要的MessageBox类。下面列出TypeView的代码。开始时需要添加两条using语句:usingSystem;usingSystem.Text;usingSystem.Windows.Forms;usingSystem.Reflection;332第12章反射需要System.Text的原因是我们要使用StringBuilde
39、r对象建立在消息框中显示的文本,以及消息框本身的System.Windows.Forms。全部代码都放在类MainClass中,这个类包含两个静态方法和一个静态字段,StringBuilder的一个实例叫作OutputText,用于创建在消息框中显示的文本。Main方法和类的声明如下所示:classMainClassStaticStringBuilderOutputText=newStringBuilder();staticvoidMain()/modifythislinetoretrievedetailsofany/otherdatatypeTypet=typeof(double);Anal
40、yzeType(t);MessageBox.Show(OutputText.ToString(),Analysisoftype+t.Name);Console.ReadLine();Main()方法首先声明一个Type对象,表示我们选择的数据类型,再调用方法AnalyzeType(),从Type对象中提取信息,并使用该信息建立输出文本。最后在消息框中显示输出。使用MessageBox类是非常直观的:只需调用其静态方法Show(),给它传递两个字符串,分别为消息框中的文本和标题。这些都由AnalyzeType()来完成:staticvoidAnalyzeType(Typet)AddToOutpu
41、t(TypeName:+t.Name);AddToOutput(FullName:+t.FullName);AddToOutput(Namespace:+t.Namespace);TypetBase=t.BaseType;if(tBase!=null)AddToOutput(BaseType:+tBase.Name);TypetUnderlyingSystem=t.UnderlyingSystemType;if(tUnderlyingSystem!=null)AddToOutput(UnderlyingSystemType:+tUnderlyingSystem.Name);AddToOutpu
42、t(nPUBLICMEMBERS:);MemberInfoMembers=t.GetMembers();foreach(MemberInfoNextMemberinMembers)333第部分C#语言AddToOutput(NextMember.DeclaringType+NextMember.MemberType+NextMember.Name);执行这个方法,仅需调用Type对象的各种属性,就可以获得我们需要的类型名称的信息,再调用GetMembers()方法,获得一个MemberInfo对象数组,该数组用于显示每个成员的信息。注意这里使用了一个辅助方法AddToOutput(),该方法创
43、建要在消息框中显示的文本:staticvoidAddToOutput(stringText)OutputText.Append(n+Text);使用下面的命令编译TypeView程序集:csc/reference:System.Windows.Forms.dllTypeView.cs13.2.3Assembly类Assembly类是在System.Reflection命名空间中定义的,它允许访问给定程序集的元数据,它也包含可以加载和执行程序集(假定该程序集是可执行的)的方法。与Type类一样,Assembly类包含非常多的方法和属性,这里不可能逐一论述。下面仅介绍完成示例WhatsNewAtt
44、ributes所需要的方法和属性。在使用Assembly实例做一些工作前,需要把相应的程序集加载到运行进程中。为此,可以使用静态成员Assembly.Load()或Assembly.LoadFrom()。这两个方法的区别是Load()的参数是程序集的名称,运行库会在各个位置上搜索该程序集,这些位置包括本地目录和全局程序集高速缓存。而LoadFrom()的参数是程序集的完整路径名,不会在其他位置搜索该程序集:Assemblyassembly1=Assembly.Load(SomeAssembly);Assemblyassembly2=Assembly.LoadFrom(C:MyProjectsS
45、oftwareSomeOtherAssembly);这两个方法都有许多其他重载,它们提供了其他安全信息。加载了一个程序集后,就可以使用它的各种属性,例如查找它的全名:stringname=assembly1.FullName;1.查找在程序集中定义的类型Assembly类的一个特性是可以获得在相应程序集中定义的所有类型的信息,只要调用Assembly.GetTypes()方法,就可以返回一个包含所有类型信息的System.Type引用数组,然后就可以按照上一节的方式处理这些Type引用了:334第12章反射Typetypes=theAssembly.GetTypes();foreach(TypedefinedTypeintypes)DoSomethingWith(definedType);2.查找定制特性用于查找在程序集或类型中定义了什么定制特性的方法取决于与该特性相关的