【.net 深呼吸】自定义特性(Attribute)的实现与检索方法

作者: 晋座

更新时间:2022-03-26 13:11:23

3456 阅读

在.net的各个语言中,尤其是VB.NET和C#,都有特性这一东东,具体的概念,大家可以网上查,这里老周说一个非标准的概念——特性者,就是对象的附加数据。对象自然可以是类型、类型成员,以及程序集。

说简单点,就是你在定义一些代码时,希望为某个代码对象加上一些额外的内容,但这些内容又不便在代码中直接写。比如,你为B类定义了一个 int 类型的属性P,而且是个虚属性,就是B的派生类可以重写它。我希望可以给这个属性弄个版本号,当子类override这个属性时,给它记一个版本号,然后在其他代码中访问这个属性时,可以检查一下版本号,比如当版本号小于3时,抛出异常,不让使用。

另外比如,当某个类的某个成员在后续版本中会删除时,也可以使用特性来附加一个说明,好让调用代码识别,在.net类库中常用这种方法。在比如,可以给某些对象加上免调试的特性,当调用代码时,遇到带有这些特性的对象时就跳过调试。

 

除了API库给出的特性外(特性类通常以Attribute结尾),我们也可以根据需要,自己定义特性类。

自定义特性的定义和一般类的定义差不多,不过要注意两点:一是应该从Attribute类或其子类派生,一般是直接从Attribute类派生;二是,不管是不是直接派生自Attribute类,只要是特性类,都要在该类的定义上附加AttributeUsageAttribute特性,作用是指明你定义的这个特性的应用范围。

啥意思呢,就是你这个特性能用在哪些对象上,比如这个特性只能用在方法上,那就把AttributeTargets值设为Method,如果只希望特性只用在类上,就指定Class;要是希望特性可以同时用在属性和方法上,就把Method和Property值组合起来。如果打算让这个特性成为“万能”通,那就用值All,表示特性可以用在所有代码对象上。

 

下面,还是举个例子吧。假设我定义了这个特性。别管它干吗用的,反正只是个示例。

    [AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]
    public class AssRateAttribute : Attribute
    {
        ushort m_rate;
        public AssRateAttribute(ushort rate)
        {
            m_rate = rate;
        }

        public ushort Rate
        {
            get { return m_rate; }
        }
    }

这里我指定了,它只能用于程序集,为啥选的程序集呢,因为它比较特殊,待会儿老周顺便介绍一下如何给程序集附加特性。

 

还有两个属性,一个是Inherited,就是说这个特性是否可以被继承,当然,对于程序集而言,无所谓。不过,如果这个特性是用在类、类型成员上,就可以用上,我把这个特性用在某对象,它的派生类是否继承这个特性。要是设置为false,那么这个特性只有附加的目标类型上可以检索到,而在类型的派生类上是检索不到的。

AllowMultiple指定该特性是否可以在同一个对象上多次使用。如果为false,那在附加时只能用一次,比如,下面的用法是会报错的。

[AssRate(5)]
[AssRate(9)]
public class C
{

}

因为上面的C类上,AssRateAttribute特性用了两次,就会出错。如果希望它可以多个实例并用,就把AllowMultiple属性改为true。当然,上面的代码只是假设,因为AssRateAttribute是不能用在类上的,它已指明只能用在程序集上。

 

接下来,看看如何把特性贴到程序集上,程序集的特性用法比较特殊,因为它没有实体代码,只是个逻辑组合,因此,你听好了:请把用于程序集的特性,写在所有代码的前面

就是说,如果你的代码文件前面写了代码,那就不能再为程序集加特性了。例如这样是不对的。

namespace Test{
       ....
        public class DocumentsOpt{  .... }
}

[assembly:AssRate(12)]

这样做是错的,因为前面已经有代码了,再为程序集附加特性是不可以的。

正确的do法是这样的:

[assembly:AssRate(12)]

using System;
using System.IO;
namespace Test{ .... public class DocumentsOpt{ .... } }

 

特性前面的assembly: 是啥意思呢,其实,特性的完整语法应该如下:

[ <target:> <attributes....> ]

例如要把特性用在类上,可以写上:

[class: MyAttr ]

 

不过,对于类,我们一般是可以省掉target,因为我们会在类的定义前面写上特性,这样编译器也能识别出来是附加到类上的。

 

根据老周耍.net的多年经验,只有两种情况下才要写上target,其余情况可以省略。

1、方法的返回值,因为这个特性虽然用在方法的返回值上,但是它只能写在方法的定义前,为了告诉编译器,这个特性不是给方法的,是给返回值的,所以要写上return,就像这样:[return: Attris...]。

2、程序集,因为程序集是逻辑性的,不存在实体的代码对象,只能明确指定特性是用在程序集上的,即[assembly: MyAttr]。

 

那么如何确保用于程序集的特性都能在所有代码之前呢,VS在项目中弄了这个结构,与项目本身有关的,都放在Properties节点下,其中有个代码文件叫AssemblyInfo.cs,或者AssemblyInfo.vb,这个文件不写其他的代码,只用来放程序集的特性。就像这样:

[assembly: AssemblyTitle("app")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("app")]
[assembly: AssemblyCopyright("Copyright © 老周 2016")]

 

所以,我们刚刚定义的AssRateAttribute特性就要写在这个文件里。

[assembly: AssRate(4)]

为什么特性类要以Attribute结尾,除了便于识别,也看到在代码中使用时,可以省略Attribute。

 

那么,特性应用之后,我们在运行阶段如何检索呢,这个嘛,你想啊,要动态知道对象的结构和信息,该用啥技术?对了,反射。就像你照镜子那样,可以把你的嘴脸呈现在镜子中,然后你就能看到自己了,道理一样,反射就是给代码照镜子用的,你可以运行时知道代码的结构,尽管代码已经编译了。

比如下面的方法,检测我们刚定义的特性,看看特性指定的Rate值,然后做出响应。

        static void Check()
        {
            Assembly currassembly = Assembly.GetExecutingAssembly();
            AssRateAttribute att = currassembly.GetCustomAttribute<AssRateAttribute>();
            if (att != null)
            {
                if (att.Rate <6)
                {
                    throw new InvalidOperationException("程序集人品分太低,禁止使用。");
                }
                else
                {
                    Console.WriteLine("程序集人品分合格。");
                }
            }
        }

反射API定义了多个GetCustomAttribute或,GetCustomAttributes方法,用来检索特性实例,这些方法包括多种扩展方法,我就不一一介绍,自己查MSDN和对象浏览器。如果在定义特性类时指明了它允许多个实例,就用GetCustomAttributes方法来获取多个实例,如果AllowMultiple为False,只允许单一实例,就用GetCustomAttribute方法获取单个特性实例。获取到特性实例后就可以进行验证或处理了。

 

好,本文内容就扯到这里了。哦,关于示例源代码嘛,不提供,想玩的话,自己动手写。

 

版权声明:本文著作权归作者【晋座 】所有,不代表本网站立场。

侵权请联系:root_email@163.com