| 用属性扩展.NET元数据 |
|
|
| 薇 赛迪网 2001-10-19 |
1,问题的提出当我第一次看到为.NET框架写的代码时,最引起我注意的是代码中对属性的运用。作为一个C++ COM的程序员,我习惯于使用属性来为IDL增加额外的信息。当我学习了.NET的属性后,我渐渐明白它们与IDL中的类型信息并没有太大的不同,因为它们都是为某一项提供类型信息。所存在的不同包括:.NET中的类型信息能描述些什么、储存在哪里、如何使用它们。(.NET SDK beta1可从http://msdn.microsoft.com/net下载)
广告
在COM中,类型信息由IDL文件生成并存放在类型库中。类型库用于类库的marshalling,所以如果类的描述和类的实现不一致,你就无法marshal这个类的接口。另外,COM+组件服务snap-in利用服务器上的类型库中的信息将组件加入到COM+应用程序中去。所以,如果描述组件的信息不正确,COM+就无法应用interception和COM+服务。
IDL文件独立于它们所描述的实现文件,你可以独立地编译IDL文件以得到类型库,再将类型库编译成资源,最后把这资源加入到代码模块中去。类型库中并不包含所有的组件信息。你需要为组件指定CLSID、ProgID,如果是基于DLL的组件,还要为其指定线程模型(ThreadingModel)。在用ATL实现时,这些都由注册表脚本完成。
因为组件的描述分散在好几个不同的源文件中,这样有可能使得在一个文件中的改动没能在另一个文件中反映出来。一个简单的情况就是对类的线程模型的修改,你可能在用ATL对象模版创建组件的时候指定的是Apartment threaded,但后来决定将其改为Free threaded,这时你必须对RGS脚本和ATL 类的头文件都作相应的改动,如果不这样,组件注册时的线程模型和实际的实现不一致。
最后来看一下组件实现的接口。COM 的规则中提到判断一个COM类是否实现了某个借口的唯一保险的做法是先创建这个类的一个实例,然后调用IUnkonwn::QueryInterface()来查询这个接口。这也意味着,用相当多的开销创建一个组件仅仅是为了看看它是否对你希望的任务合适,当然你也可以用IDL中coclass语句将一个组件所支持接口的列表加入到一个类型库中。这种列表只是一个建议,因为类的类型信息仍然有可能过时于实际的类实现。
2.带元数据的类在.NET中所以的类型都有着完整的描述,这包括.NET 框架提供的类型和你自己定义的类型。你不必使用某种类型描述语言来描述你编写的类,因为一个类的描述含在这个类的代码之中。这样做的好处在于,不会出现类的描述与具体实现不同步的情况。
当一个.NET 编译器正在编译的源代码中看到一个类型时,它给出该类型的元数据。这个"类型"包括所有.NET框架支持的语言实体,其中有类(classes)、结构(structs)、常量(enums)、接口(interfaces)和子域(constituent fields)、方法(及参数)、属性、实践以及这些实体的代理。所有的这一切都被编译器编译成元数据,存放在最终的代码模块中。这意味着元数据的解读器(reader)可以从一个.NET assembly中装入这些数据,并清楚地知道这个assembly中有哪些类以及它们支持的功能。这个解读器不用创建类个实例就可以完成所有的工作。
元数据非常复杂。它不仅涵盖了基本的类信息(类的名字、大小和参数),它还包含了更复杂的信息。比如,含有指明当一个函数是native to Win32时,应该通过.NET 平台(platform)来调用的信息。大部分的信息是来自于编译器正在处理的源代码,但另有一些来源于源代码中项目(item)提供的属性。
属性扩展了元数据,提供了用标准代码生成的元数据无法表达一些额外信息。属性有三种:自定义属性、明显自定义属性(distinguished-custom attributes)和伪自定义属性(pseudo-custom attributes)。它们的区别在于如何影响由编译器生成的元数据。
改变标准的元数据
伪自定义属性(Pseudo-custom attributes)改变标准元数据中的值,但不扩展元数据。比如,用于指明一个类是否要串行化的标准元数据位。.NET提供了流式类(Stream classes),允许你将数据块(blobs of data)移到另一个进程 (或同一个进程的Appdomain)中或文件中。这些数据块可以是一个串行化了的类实例,也就是说,类中域的值和用以标示这个类的信息一起传输。代码从进程间通讯流或文件中读取数据块,然后可以创建这个已确认类的一个新实例并用数据流中的值对它进行初始化。
在你的类中可能有一些域,它们只是用于存放临时值的,你并不想对它们进行串行化,因为这毫无意义,所以元数据本身有一个域,你可以用它来指明类中的域是否应该串行化。C++的受托管Extensions和C#在如何改变元数据上不同。
在清单1的示例代码中,FacilityUser类描绘了一个用户的一些假象设备。比如,当地的游泳池或video商店。
这代码有趣的地方在于用户的数据是通过属性来访问的。拿ID来说,这仅仅意味着缓存着的值。但对Name来说,值是在第一次对数据库的users表进行属性访问是获得的,表users中使用ID作为主键。另外,UsedFacilityCount属性是在运行的时候通过查询数据库计算得出的,当一个用户进入facility时,相应的信息被存放于这个数据库中。
清单1. 一个假象的用户 这个类有三个属性,包括用作Users表主键的ID属性,Name属性可以根据ID 获得。ID属性可用于用户使用facility日期的查询,这个查询返回UsedFacilityCount属性的值。
// C++#using using namespace System;public __gc class acilityUser{privateString* name;int id;public: FacilityUser(int userID) : name(NULL){id = userID;}__property String*get_Name(){if (name == NULL){// get name from database// using idname = String::Format(S"User{0:D4}",__box(id));}return name; }__property int get_ID(){return id;}__property int get_UsedFacilityCount(){int count = 0;// do a database look up with// the user ID to see how many// times the user has used the// facilityreturn count;}};
一个程序可以创建一个FacilityUser的实例,用以描绘从某特定地区来的人,并对这些人做一些分析。假象一个程序为了日后的分析想收集储存FacilityUsers 的兴趣情况,那么它应该储存些什么信息吗?很明显userID 是这个用户所有信息的主键,所以其他信息可以在运行的时候用这键计算得出。你或许会争辩说:如果磁盘便宜的话,还应该存放Name项。但有一样东西无容置疑,那就是如果你希望使包含用户使用设备次数的信息是动态的,你就不能储存UsedFacilityCount这个值。下面的代码对一些FacilityUser对象进行了串行化:
// ME C++FacilityUser* user10 = new FacilityUser(10);FacilityUser* user42 = new FacilityUser(42);FacilityUser* user67 = new FacilityUser(67);Stream* writer = File::Create(S"users.bin");BinaryFormatter* bf = new BinaryFormatter();bf->Serialize(writer, user10);bf->Serialize(writer, user42);bf->Serialize(writer, user67);
BinaryFormatter类将一个对象串行化一个流,它将被传递到Serialize()作为其第一个参数。在这段代码中,这个流是基于一个名为"users.bin"的文件。方法Serialize()只能对可串行化类中域进行串行化处理。在C++中,你可以指明类中所有的域用_serializable修饰符来串行化:
__serializablepublic __gc class FacilityUser{private String* name; int id;// other code};
上面的代码保证了Name和ID域被串行化到users.bin文件中。为了指出Name域不应该被串行化,你可以使用_transient修饰符:
__serializablepublic __gc class FacilityUser{private __transient String* name; int id;// other code};
这是必要的行为。唯一要串行化的数据是每个对象的ID域,Name域和属性不用串行化。你也可以用[Serializ-ableAttribute]和[NonSerializedAttribute]属性来达到相同的效果:
public [SerializableAttribute] __gc class FacilityUser{private [NonSerializedAttribute] String* name; int id;// other code};
这些代码有些像C#语言。在C#中,属性用方括号来表示,在大多数情况下属性的后缀可以省略(当编译器在省略后缀的情况下无法解析名字时是个例外)。_serializable修饰符和[Serializable]属性把元数据的标志位设为True,这对所作用的类中所有的域都有效。而_transient修饰符和[NonSerialized]属性将这个标志位设为False,但其作用范围仅限于当前这个域。
其他伪自定义属性的例子有GuidAttribute和DllImportAttribute,它们是System.RuntimeInteropServices的属性。标准的元数据已经拥有存放这些信息的域,所以编译器可以直接将通过属性传来的属性值放入元数据中。
3.明显自定义属性一个明显自定义属性对元数据进行了扩展。也就是说,它往元数据中添加了新值,但运行时(runtime)知道这扩展部分的用途所在并需要它。这儿有个例子,就是ThreadAffinity属性,当你在一个类中使用这个属性时,一块数据就会被加入到元数据中,因为没有描述Thread affinity的标准元数据。如果一个类是从ContextBoundObject衍生出来的,那就必须要使用这个属性,因为Thread affinity暗含了这个对象在某个特定的上下文中运行的信息。
Thread affinity意味着代码必须一直在一个线程中运行。要使你的代码做到这一点,你可以使用System.Runtime.Remoting名字空间中的Thread-Affinity属性。清单2中的AffinityClass给我们阐明了这个属性(见清单2)。
清单2. 使用ThreadAffinity属性的类在一个类中使用明显自定义的ThreadAffinity属性会在代码模块中加入自定义元数据,这些数据指明了这个类需要使用的affinity类型。在标准的元数据中没有专门存放affinity信息的地方,所以运行时(runtime)特别期待这些自定义元数据。请试着将ThreadAffinity的值分别设为ThreadAffinity.REQUIRED和ThreadAffinity.NOTSUPPORTED,然后运行这代码,比较一下两次的运行结果。
// C#using System;using System.Threading;using System.Runtime.Remoting;[ThreadAffinity(ThreadAffinity.REQUIRED)]public class AffinityClass : ContextBoundObject{public void f(){Console.WriteLine("f() thread hash: {0}", Thread.CurrentThread.GetHashCode());}}public class TestAffinity{public static AffinityClass c;public static void Proc(){Console.WriteLine("Proc() thread hash: {0}",Thread.CurrentThread.GetHashCode());c.f();}public static void Main(){c = new AffinityClass();for (int i = 0; i
这里,Main()函数先为有thread affinity信息的AffinityClass创建了一个对象。这意味着无论哪个线程试图访问这个对象,它的代码只能在一个单一的线程中执行。然后Main()函数创建了五个线程,并把Proc()方法作为线程过程传给了这些线程。这个Proc()方法打印出当前线程的Hash号,它们彼此都不同,这与Win32线程ID不一样。Proc()首先访问这个对象的thread affinity,然后打印这个线程的hash号。我在我的机器上编译并运行了这些代码,得到如下结果:
Proc() thread hash: 34Proc() thread hash: 35Proc() thread hash: 36Proc() thread hash: 37Proc() thread hash: 38f() thread hash: 25f() thread hash: 25f() thread hash: 25f() thread hash: 25f() thread hash: 25
以上的输出表明Proc()运行在五个线程中,这真是你所希望的,但AffinityClass.f()一直运行在同一个线程。这就是使用了将ThreadAffinity属性赋予ThreadAffinity.REQUIRED参数的效果。如果我把这个属性移去(或是将参数改成ThreadAffinity.NOTSUPPORTED),我得到:
Proc() thread hash: 25f() thread hash: 25Proc() thread hash: 26f() thread hash: 26Proc() thread hash: 27f() thread hash: 27Proc() thread hash: 28f() thread hash: 28Proc() thread hash: 29f() thread hash: 29
上面的结果并不是意料之外的结果,只是因为这个类没有了thread affinity信息。所以AffinityClass.f()运行在调用它的线程中。这段代码的重要之处在于:通过属性值来改变类的元数据可以使类的行为发生改变。
4. 自定义属性(Custom Attributes)最后,让我们来看看自定义属性(Custom Attributes)!我在这里使用C#,但过程同C++一样。自定义属性往元数据中添加数据,但这些数据对运行时(runtime)毫无意义。然而你的代码可以通过一个称为reflection的.NET API调用来读取这个自定义元数据。.NET中的每一个类型(type)都是从System.Object这个基类中衍生出来的,System.Oject类有一个名为GetType()的方法,它将返回一个System.Type对象。你可以使用这个Type对象的方法和属性去访问该对象的元数据。比如访问应用在这个类上的属性。
在下面的例子中,我编写了一个名为Catalog的新属性,它为应用程序使用的类构建一个目录(catalog)。这个属性有一个单一的默认参数,就是用于存放dump信息的文件的名字。另外,它还有两个命名参数(named parameter):Commnet是一个类的描述信息,Level决定要对多少信息进行编目录。如果你希望在创建一个类的实例的时候将这些信息加到目录文件中去,你必须从Catalogable衍生出这个类:
[Catalog("objects.txt", Level = CatalogLevelType.All,Comment = "First Class")]class TestOne : Catalogable{}[Catalog("objects.txt", Level = CatalogLevelType.Basic,Comment = "Second Class")]class TestTwo : Catalogable{}[Catalog("objects.txt")]class TestThree : Catalogable{}
在这里我定义了三个类,并用文件objects.txt来每个类的目录信息。开始的两个使用了命名参数(named parameters)。TestOne的Level参数指明类中所有的信息都要进行编目,而TestTwo只需对基本的信息进行编目。我们可以这样使用这些类:
public class App{ public static void Main() {TestOne one1 = new TestOne();TestOne one2 = new TestOne();TestTwo two = new TestTwo();TestThree three = new TestThree(); }}
我为TestOne创建了两个实例,为其他两个类各创建了一个实例。运行这个程序将得到一个objects.txt文件,其中包含了one1和one2的信息。我们并没有得到one3的信息,因为Level参数的默认值为None。
属性(attributes)是在一个从System.Attributes衍生出来的类中实现的。元数据来自于你类中的域。为了提供属性的必要参数,你必须实现一个构造器(constructor)来提取这些参数。那些可选的参数是由类的属性处理的。
CatalogAttribute是一个描述Catalog属性的类
清单3. 一个自定义属性将目录信息加入一个类 CatalogAttribute属性加入了自定义元数据,这些数据指出了目录文件的名字,进行编目信息的数量和一个关于这个类的描述。真正的目录工作其实不是这个类完成的,而是由一个名为Catalogable的类完成的。这个目录的信息保存在CatalogAttribute相应的域中。Catalogable类通过reflection 这个API来访问这些信息。
public enum CatalogLevelType {None, Basic, All};
[AttributeUsage(AttributeTargets.Class)]public class talogAttribute : Attribute{private string name;private CatalogLevelType level;private string comment;public CatalogAttribute(string fileName){name = fileName;level = CatalogLevelType.None;comment = "";}public CatalogLevelType Level{get{ return level;}set{ level = value;}}public string Name{get{ return name;}set{ name = value;}}public string Comment{get {return comment;}set {comment = value;}}}
AttributeUsage属性指明你在哪里可以使用这个属性。AttributeUsage的值可以是AttributeTargets枚举的常量中任意几个值的位组合。在这里的代码中,我把这个属性设为Class,使它只能作用在类上,因为这个属性所作用的item必须是从Catalogable衍生出来的(见清单4)。
清单4. 访问元数据 使用CatalogAttribute的类可以从Catalogable衍生出来。将对象的信息储存到目录中件中的工作实际上是Catalogable完成的,它的构造器使用reflection API访问类实例的属性以获得目录文件的文件名和要写入的信息。我把这段代码放入到构造器,这样可以在一个类被衍生的时候能自动运行这段代码。你并不是一定要使用Catalogable才能使用CatalogAttribute,这样用只是为了方便。
public class Catalogable{ public Catalogable() {Type myType = this.GetType();foreach (Attribute attr in myType.GetCustomAttributes()){ if (attr.ToString() !="CatalogAttribute")continue; CatalogAttribute cat =(CatalogAttribute)attr; if (cat.Level ==CatalogLevelType.None)break; if (cat.Name.Length != 0) {StreamWriter dump = null;try{ dump = File.AppendText(cat.Name);}catch( FileNotFoundException){ dump = File.CreateText(cat.Name);}dump.WriteLine( "an instance of " + myType.ToString() + " has been created");if (cat.Level ==CatalogLevelType.All){dump.WriteLine(cat.Comment);}dump.Close(); } break;} }}
当一个类从Catalogable衍生时,Catalogable的构造器就会被调用,它会对类实例的属性进行访问。从这段代码我们可以看到,它首先获取类的类型。这里要注意,因为这个类型是通过衍生类的引用调用的,所以它返回的是这个衍生类的Type对象,而不是Catalogable类的Type对象。接下来,调用GetCustomAttributes(),它将返回一个装有属性的对象数组。Foreach循环访问了这个数组中的每一个属性。因为在这个属性数组中除了目录自定义属性外,可能还会有其他属性,所以要对每个属性的名字进行检测,看它是不是CatalogAttribute。如果检测成功,把这个属性的引用映射到CatalogAttribute,并访问这个类的成员以决定要写那些内容到目录文件中去。要注意到,一个属性只能在一个类上运用一次(Note that an attribute can be applied only once to a class),在你处理完CatalogAttribute后,跳出这个循环。
至此你已经得到了一个自定义属性,你可以对其使用自己的代码。
上面讨论的重点在于教你如何利用自己的自定义属性来扩展元数据。对元数据的扩展很直接:编译器将一些域作为自定义元数据加入到一个从System::Attribute衍生出来的类中。至于如何使用这些自定义元数据和如何访问它们完全取决于你自己。在这例子中,我把reflection代码放在一个独立的名为Catalogable 的类中,这样任何要使用CatalogAttribute属性的类可以从Catalogable中衍生并得到这些reflection代码。但你不一定要这么做,你也可以编写一个集合类(collection class),使它含有带CatalogAttribute属性的对象。这个集合类还应该包含reflection代码并在目录文件中写入:
CatalogContainer c = new CatalogContainer();c.Add(one1);c.Add(one2);c.Add(two);c.Add(three);
这里的Add方法含有reflection代码对对象进行编目。你可以去http://www.devx.com/free/mgznarch/vcdj/code/2001/02feb01/vc0102rg.zip 下载一个示例程序,其中演示了具体的做法。
|
|
|
|
|
 |
|
|
 |
|
|
|
|