zoukankan      html  css  js  c++  java
  • 深入浅出Attribute(中)——Attribute本质论

    小序:

             上篇里,我们把Attribute“粘”在类的成员方法上show了一把,让Attribute跟大家混了个脸儿熟。中篇里,我们将探讨“究竟什么是Attribute”和“如何创建及使用Attribute”这两个问题。

             准备好了吗?Let’s go!

            

    正文:

             从上篇里我们可以看到,Attribute似乎总跟publicstatic这些关键字(Keyword)出现在一起。莫非使用了Attribute就相当于定义了新的修饰符(Modifier)吗?让我们来一窥究竟!

             先把下面这个例子编译出来:

                                  

    #define OK

    using System;
    using System.Diagnostics;

    namespace Sample
    {
             class
    Program
             {
                       [Conditional("OK")]
                       public static void TargetMethod()
                       {
                                Console.ForegroundColor = ConsoleColor.Green;
                                Console.WriteLine(""t=<
    水之真谛>="nhttp://blog.csdn.net/FantasiaX"n"n");
                       }
                       static void Main(string[] args)
                       {
                                TargetMethod();
                       }
             }
    }

                        

             毋庸置疑,它的运行结果会是这样:

                

                  

             接下来,让我们把编译出的结果(.EXE文件)用“微软中间语言反编译器”打开,查看存储在程序集(Assembly,这在个例子中就是这个.EXE文件)中的中间语言代码(中间语言也就是我们常说的通用语言)。

                

                      

             如果你认为反汇编是件很神秘的事情,那你可就错了!比起x86汇编语言来,对.NET程序集的反汇编要简单得多——甚至可以说是与C#语言一一对应:

              

                       

            严格地来说,用来形成上图中树状结构的代码并不是程序集中的中间语言,而主要是元数据(Metadata)的功劳。包含在程序集中的元数据记录了这个程序集里有多少个namespace、多少个类、类里有什么成员、成员的访问级别是什么……而且,元数据是以文本(也就是Unicode字符)形式存在的,使用.NET的反射(Reflection)技术,很容易就能把它们读取出来并形成各种各样的漂亮视图——上面的树状图、VS里的Object Browser视图和自动代码提示功能,都是元数据与反射技术结合的产物。一个程序集(.EXE.DLL)能够使用包含在自己体内的元数据来完整地说明自己,而不必像C/C++那样带着一大捆头文件,这就叫作“自包含性”或“自描述性”。

                     

             扯的有点儿远了——让我们回到正题,双击反编译器中的TargetMethod:void()。这回弹出窗口里显示的内容是真正的微软中间语言代码了。这些代码也都是文本形式的,需要经过.NET的“虚拟机”再编译后才能被CPU所执行。顺便说一句:VB.NET代码也会编译成这样的中间代码,所以,.NET平台上所有语言的编译结果都是通用的。换句话说,你用C#编写了一个组件,把它编译成一个DLL文件并交给VB.NET程序员,VB.NET程序员可以直接使用,丝毫不必有任何担心J

                          

             今天我们不打算研究中间语言的编译和执行,主要是打算通过中间语言对一些被C#语言所掩盖的事实一窥究竟。

           

                   

             仔细观察中间代码之后,Attribute变得了无秘密!图中蓝色箭头所指处是两个“真正的”修饰符——Attribute并没有出现在这里。而在红色箭头所标识的位置,我们可以清楚地看出——这分明是在调用mscorlib.dll程序集System.Diagnostics名称空间中ConditionalAttribute类的构造函数。可见,Attribute并不是修饰符,而是一个有着独特实例化形式的类

                      

             Attribute实例化有什么独特之处呢?还是让我们再次观察中间语言代码——它有两个独特之处。

    1.         它的实例是使用.custom声明的。查看中间语言语法,你会发现.custom是专门用来声明自定义特性的

    2.         声明的位置是在函数真正的代码(IL_0000:IL_0014)之前

               

    God,我怀疑是不是讲的太深了。没关系,上面关于中间语言的东西你都可以不care,只需要记住一个结论就可以了——我们已经从“底层”证明了Attribute不是什么“修饰符”,而是一种实例化方式比较特殊的类

             

    Attribute的实例化

             就像牡蛎天生就要吸附在礁石或船底上一样,Attribute的实例一构造出来就必需“粘”在一个什么目标上。

             

             Attribute实例话的语法是相当怪异的,主要体现在以下三点上

    1.         不使用new操作符来产生实例,而是使用在方括号里调用构造函数的来产生实例。

    2.         方括号必需紧挨着放置在被附着目标的前面。

    3.         因为方括号里空间有限,不能像使用new那样先构造对象后再对对象的属性(Property)一一赋值。因此,对Attribute实例的属性的赋值也都挤在了构造函数的圆括号里L

    说实话,写代码的时候对于第1、第2两条适应起来还算容易,第3条写出来怎么看怎么别扭……而且尤其要记着的是:

    1.         构造函数的参数是一定要写的——有几个就得写几个——因为你不写的话实例就无法构造出来。

    2.         构造函数参数的顺序不能错,这个很容易理解——调用任何一个函数你都不能改变参数的顺序——除非它有相应的重载(Overload)。因为这个顺序的固定的,所以有些书里管这些参数称为“定位参数”,意即“个数和位置固定的参数”。

    3.         Attribute实例的属性的赋值可有可无——反正它会有一个默认值。而且,先对哪个属性赋值、后对哪个属性赋值不受限制。有些书管这些为属性赋值的参数叫“具名参数”——令人匪夷所思。

    OK,百闻不如一见,还是让我们自己写一个Attribute类来体验一下吧!

          

    自己动手写Attribute

             这回我们抛弃.NET Framework给我们准备好的各种Attribute,从头写一个全新的Attribute——Oyster

             下面我给出一个完整的小例子:

               

    //======水之真谛=======//
    //   
    上善若水,润物无声
     //
    /* http://blog.csdn.net/FantasiaX */

    using System;
    namespace OysterAttributeSample
    {
             class Oyster: System.Attribute                         //
    必需以System.Attribute类为基类

             {
                       // Kind
    属性,默认值为
    null
                       private string kind;
                       public string Kind
                       {
                                get { return kind; }
                                set { kind = value; }
                       }

                       // Age
    属性,默认值为

                       private uint age;
                       public uint Age
                       {
                                get { return age; }
                                set { age = value; }
                       }

                       //
    值为nullstring是危险的,所以必需在构造函数中赋值

                       public Oyster(string arg)                                     // 定位参数
                       {
                                this.Kind = arg;
                       }
             }

             [Oyster("Thorny ", Age=3)]    // 3
    年的多刺牡蛎附着在轮船(这是一个类)上。注意:对属性的赋值是在圆括号里完成的!

             class Ship
             {
                       [Oyster("Saddle")]          // 0
    年的鞍形牡蛎附着在船舵(这是一个数据成员)上,Age使用的是默认值,构造函数的参数必需完整
                       public string Rudder;
             }

             class
    Program
             {
                       static void Main(string[] args)
                       {
                                // ...
    使用反射来读取Attribute
                       }
             }
    }

                  

             为了不把代码拖的太长,上面这个例子中Oyster类的构造函数只有一个参数,所以对“定位参数”体现的还不够淋漓尽致。大家可以再为Oyster类添加几个属性,并在构造函数里多设置几个参数,体验一下Attribute实例化时对参数个数及参数位置的敏感性。

              

    能被Attribute所附着的目标

               

    让我们思考这样一个问题:牡蛎可以附着在船底、礁石上、桥墩上……甚至是别的牡蛎身上,那么Attribute呢?都可以将自己的实例附着在什么目标上呢?

    这个问题的答案隐藏在AttributeTargets这个枚举类型里——这个类型的可取值集合为:

                  

    =============================================================================

    All                                                  Assembly                                    Class                                             Constructor

    Delegate                                     Enum                                             Event                                             Field

    GenericParameter                   Interface                                       Method                                         Module

    Parameter                                   Property                                       ReturnValue                               Struct

    =============================================================================

                    

             一共是16个可取值。

             不过,上面这张表是按字母顺序排列的,并不代表它们真实值的排列顺序。使用下面这个小程序可以查看每个枚举值对应的整数值。

             

    // =<水之真谛>=
    // http://blog.csdn.net/FantasiaX
    using
    System;
    namespace AttributeTargetValue
    {
             class
    Program
             {
                       static void Main(string[] args)
                       {
                                Console.WriteLine("Assembly"t"t"t{0}", Convert.ToInt32(AttributeTargets.Assembly));
                                Console.WriteLine("Module"t"t"t"t{0}", Convert.ToInt32(AttributeTargets.Module));
                                Console.WriteLine("Class"t"t"t"t{0}", Convert.ToInt32(AttributeTargets.Class));
                                Console.WriteLine("Struct"t"t"t"t{0}", Convert.ToInt32(AttributeTargets.Struct));
                                Console.WriteLine("Enum"t"t"t"t{0}", Convert.ToInt32(AttributeTargets.Enum));
                                Console.WriteLine("Constructor"t"t"t{0}", Convert.ToInt32(AttributeTargets.Constructor));
                                Console.WriteLine("Method"t"t"t"t{0}", Convert.ToInt32(AttributeTargets.Method));
                                Console.WriteLine("Property"t"t"t{0}", Convert.ToInt32(AttributeTargets.Property));
                                Console.WriteLine("Field"t"t"t"t{0}", Convert.ToInt32(AttributeTargets.Field));
                                Console.WriteLine("Event"t"t"t"t{0}", Convert.ToInt32(AttributeTargets.Event));
                                Console.WriteLine("Interface"t"t"t{0}", Convert.ToInt32(AttributeTargets.Interface));
                                Console.WriteLine("Parameter"t"t"t{0}", Convert.ToInt32(AttributeTargets.Parameter));
                                Console.WriteLine("Delegate"t"t"t{0}", Convert.ToInt32(AttributeTargets.Delegate));
                                Console.WriteLine("ReturnValue"t"t"t{0}", Convert.ToInt32(AttributeTargets.ReturnValue));
                                Console.WriteLine("GenericParameter"t"t{0}", Convert.ToInt32(AttributeTargets.GenericParameter));
                                Console.WriteLine("All"t"t"t"t{0}", Convert.ToInt32(AttributeTargets.All));
                                Console.WriteLine(""n");
                       }
             }
    }
               

             程序的运行结果是:

             

          

             可能出乎你的预料——它们的值并不是步长值为1的线性递增。你观察出什么规律来了吗?提醒你一下:从二进制的角度来考虑喔!!

         

             我想你一定发现了规律——除了All的值之外,每个值的二进制形式中只有一位是“1”,其余位全是“0”。这就是枚举值的另一种用法——标识位。那么,标识位有什么好处呢? 

    考虑这样一种情况:我们的Attribute要求既能附着在类上,又能附着在类的方法上,应该怎么做呢?

            

    我们知道,C#中有一个操作符“|”(也就是按位求“或”)。有了它,我们只需要书写

    AttributeTargets.Class | AttributeTargets.Method

    就可以了。因为这两个枚举值的标识位(也就是那个唯一的“1”)是错开的,所以只需要按位求或就解决问题了。我想,聪明的你一定立刻就能解释为什么AttributeTargets.All的值是32767了吧:p

            

             OK,了解了这些之后,我们应该怎样控制一个Attribute的附着目标呢?

             默认情况下,当我们声明并定义一个新Attribute类时,它的可附着目标是AttributeTargets.All。大多数情况下AttributeTargets.All就已经满足需求了,不过,如果你非要对它有所限制,那就要费点儿周折了。

         

    还拿我们上面创建的OysterAttribute举例——如果你想把它的附着目标限制为只有“类”和“值域”,你就应该这样书写:

             [AttributeUsage(AttributeTargets.Class|AttributeTargets.Field)]
             class Oyster : System.Attribute     
             {
                       // OysterAttribute
    类的具体实现

             }
               

             没想到吧!原来是用Attribute(的实例)附着在Attribute(类)上!本来吗,Attribute的本质就是类,而AttributeTargets.Class又说明Attribute可以附着在类上,所以,使用Attribute来“修饰”Attribute也就顺理成章了J

                

             最后,细心的读者可能会问这样两个问题:

    1.         如果一个Attribute附着在了某个类上,那么这个Attribute会为会随着继承关系也附着在派生类上呢?

    2.         可不可以像多个牡蛎附着在同一艘船上那样,让一个Attribute的多个实例附着在同一个目标上呢?

    Very good! 这真是两个好问题!请看下面的代码:

              

             [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field, Inherited = false, AllowMultiple = true)]
             class Oyster : System.Attribute     
             {
                       // OysterAttribute
    类的具体实现

             }
              

             原来,AttributeUsage这个用来专门修饰AttributeAttribute除了可以控制修饰目标外,还能决定被它修饰的Attribute是否能够随宿主“遗传”以及是否可以使用多个实例来修饰同一个目标!

             OK,大家猜一猜,修饰ConditionalAttributeAttributeUsage会是什么样子呢?(提示:答案在MSDN里。)

  • 相关阅读:
    GTK+ 3.6.2 发布,小的 bug 修复版本
    RunJS 新增 Echo Ajax 测试功能
    Mozilla 发布 Popcorn Maker,在线创作视频
    Sina微博OAuth2框架解密
    Mina状态机State Machine
    Mozilla 发布 Shumway —— 纯JS的SWF解析器
    Code Browser 4.5 发布,代码浏览器
    ROSA 2012 "Enterprise Linux Server" 发布
    ltrace 0.7.0 发布,程序调试工具
    Artifactory 2.6.5 发布,Maven 扩展工具
  • 原文地址:https://www.cnblogs.com/yangjunwl/p/1031236.html
Copyright © 2011-2022 走看看