在调用 API 的时候,经常能够发现一些提示使用的方法已过时,例如

这用到了一种叫做特性(Attribute)的技术,下面我们了解它的概念以及如何自定义特性。

什么是特性 (Attribute)

特性是用于保存程序结构信息的某种特殊类型的类,它允许我们向程序集中添加元数据,比如编译器指令和注释、描述、方法、类等其他信息。MSDN 中对它的解释是:特性提供功能强大的方法用来将声明信息与 C# 代码(类型、方法、属性等)相关联,特关联后即可在运行时使用反射(reflection)技术查询属性。

使用特性

特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集,它可以放置在几乎所有的声明中(但特定的属性可能限制在其上有效的声明类型)。其语法为:

● 在结构前放置特性片段来运用特性

● 特性片段被方括号包围,其中是特性名和特性的参数列表

[Serializable]    //不含参数的特性
    public class MyClass
    {...}
​
   [MyAttribute("firt","second","finally")]    //带有参数的特性
  public class MyClass {...}

大多数特性只针对直接跟随在一个或多个特性片段后的结构

单个结构可以运用多个特性,使用时可以把独立的特性片段互相叠在一起或使用分成单个特性片段,特性之间用逗号分隔

[Serializable]  
[MyAttribute("firt","second","finally")]    //独立的特性片段
...
[MyAttribute("firt","second","finally"), Serializable]    //逗号分隔...

某些属性对于给定实体可以指定多次。例如,_Conditional_就是一个可多次使用的属性:

[Conditional("DEBUG"), Conditional("TEST1")]
void TraceMethod()
{
    // ...
}

特性的目标是应用该特性的实体。例如,特性可以应用于类、特定方法或整个程序集。默认情况下,特性应用于它后面的元素。但是,您也可以显式标识要将特性应用于方法还是它的参数或返回值。

自定义特性

特性的用法虽然很特殊,但它只是一种特殊类型的类。

声明自定义的特性

总体上声明特性和声明其他类是一样的,只是所有的特性都派生自_System.Attribute_。根据惯例,特性名使用_Pascal_命名法并且以_Attribute_后缀结尾,当为目标应用特性时,我们可以不使用后缀。如:对于_SerializableAttribute_和_MyAttributeAttribute_这两个特性,我们在把它应用到结构的时候可以使用 [_Serializable_和_MyAttribute_短名

public class MyAttributeAttribute : System.Attribute
{...}

当然它也有构造函数。和其他类一样,每个特性至少有一个公共构造函数,如果你不声明构造函数,编译器会产生一个隐式、公共且无参的构造函数。当使用特性的时候,就会执行这个构造函数,在这个构造函数中,我们可以做一些自己想要实现的功能,比如最简单的将这些参数打印出来。

public class MyAttributeAttribute : System.Attribute
{
    public string Description;
    public string ver;
    public string Reviwer;
​
    public MyAttributeAttribute(string desc,string ver,string Rev)    //构造函数 {
        Description = desc;
        this.ver = ver;
        Reviwer = Rev;
        
        Console.WriteLine(desc + ver + Rev);
    }
}

限制特性的使用

前面我们已经知道,可以在类上面运用特性,而特性本身就是类,有一个很重要的预定义特性_AttributeUsage_可以运用到自定义特性上,我们可以用它来限制特性使用在某个目标类型上,下面限制自定义的特性只能应用到方法和类中。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyAttributeAttribute : System.Attribute
{...}

简单解读一下_AttributeUsage_特性,它有三个重要的公共属性,如下表

在 vs 中按 f12 查阅定义我们可以看到,_AttributeTarget_枚举的成员有

看一个小例子

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,   //必须的,指示MyAttribute只能应用到类和方法上
        Inherited = false,   //可选,表明不能被派生类继承
        AllowMultiple = false)]   //可选,表明不能有MyAttribute的多个实例应用到同一个目标上
    public class MyAttributeAttribute : System.Attribute
    {...}

访问特性

定义好特性了,怎么进行访问呢?对于自定义的特性,我们可以用_Type_中的_IsDefined_和_GetCustomAttributes_方法来获取

使用 IsDefined 方法

public abstract bool IsDefined(Type attributeType, bool inherit)

它是用来检测某个特性是否应用到了某个类上

参数说明: attributeType : 要搜索的自定义特性的类型。 搜索范围包括派生的类型。

inherit:true 搜索此成员继承链,以查找这些属性; 否则为 false。 属性和事件,则忽略此参数

返回结果: true 如果一个或多个实例 attributeType 或其派生任何的类型为应用于此成员; 否则为 false

下面代码片段是用来检查_MyAttribute_特性是否被运用到_MyClass_类

MyClass mc = new MyClass();
Type t = mc.GetType();
bool def = t.IsDefined(typeof(MyAttributeAttribute),false);
if (def)
    Console.WriteLine("MyAttribute is defined!");

使用 GetCustomAttributes 方法

public abstract object[] GetCustomAttributes(bool inherit)

调用它后,会创建每一个与目标相关联的特性的实例

参数说明: inherittrue 搜索此成员继承链,以查找这些属性; 否则为 false

返回结果:返回所有应用于此成员的自定义特性的数组, 因此我们必须将它强制转换为相应的特性类型

//自定义特性
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 
public class MyAttributeAttribute : System.Attribute
{
    public string Description;
    public string ver;
    public string Reviwer;
​
    public MyAttributeAttribute(string desc,string ver,string Rev) {
        Description = desc;
        this.ver = ver;
        Reviwer = Rev;
    }
}
​
//定义类
[MyAttribute("firt","second","finally")]
class MyClass
{
​
}
​
 static void Main(string[] args) {
     MyClass mc = new MyClass();
     Type t = mc.GetType();
     Object[] obj = t.GetCustomAttributes(false);
​
     foreach(Attribute a in obj)
     {
         MyAttributeAttribute attr = a as MyAttributeAttribute;
         if(attr != null)
         {
             Console.WriteLine("Description : {0}", attr.Description);
             Console.WriteLine("ver : {0}", attr.ver);
             Console.WriteLine("review: {0}", attr.Reviwer);
         }
     }
 }

结果如下

预定义的特性

_Obsolete_特性

_Obsolete_特性将程序结构标注为过期的,并且在代码编译时显式有用的警告信息,它有三种重载

public ObsoleteAttribute()
​
//参数说明: message:描述了可选的变通方法文本字符串。
public ObsoleteAttribute(string message) 
​
//参数说明:message:描述了可选的变通方法文本字符串。 
//error:true 如果使用过时的元素将生成编译器错误; false 如果使用它将生成编译器警告。
public ObsoleteAttribute(string message, bool error)

 举个例子:

using System;
using System.Runtime.CompilerServices;
​
namespace 特性
{
    class Program
    {
        [Obsolete("Use method SuperPrintOut")]
        static void Print(string str,[CallerFilePath] string filePath = "") {
            Console.WriteLine(str);
            Console.WriteLine("filePath {0}", filePath);
        }
​
​
        static void Main(string[] args) {
            string path = "no path";
            Print("nothing",path);
            Console.ReadKey();
        }
    }
}

运行没有问题,不过出现了警告:

如果将 [Obsolete(“Use method SuperPrintOut”)] 改成_[Obsolete(“Use method SuperPrintOut”,true)]_ 的话,编译则会出现错误信息

开头例子中就是使用了这个特性来提示方法过时

Conditional 特性

public ConditionalAttribute(string conditionString)

指示编译器,如果定义了_conditionString_编译符号,就和普通方法没有区别,否则忽略代码中对这个方法的所有调用

#define funSign    //定义编译符号
using System;
using System.Runtime.CompilerServices;
​
namespace 特性
{
    class Program
    {
        [Conditional("funSign")]
        static void Fun(string str) {
            Console.WriteLine(str);
        }
​
        static void Main(string[] args) {
            Fun("hello");
            Console.ReadKey();
        }
    }
​
}

由于在代码第一行定义了_funSign_标记,所以_Fun_函数会被调用,如果没有定义,这忽略_Fun_函数的调用

调用者信息特性

调用者信息特性可以访问文件路径、代码行数、调用成员的名称等源代码信息,这三个特性的名称分别为_CallerFilePath_、CallerLineNumber_和_CallerMemberName,这些方法只能用于方法中的可选参数,系统会对这些参数进行赋值。

using System;
using System.Runtime.CompilerServices;
​
namespace 特性
{
    class Program
    {
        static void Print(string str,
            [CallerFilePath] string filePath = "",
            [CallerLineNumber] int num = 0,
            [CallerMemberName] string name = "") {
            Console.WriteLine(str);
            Console.WriteLine("filePath {0}", filePath);
            Console.WriteLine("Line {0}", num);
            Console.WriteLine("Call from {0}", name);
        }
​
        static void Main(string[] args) {
            Print("nothing");
            Console.ReadKey();
        }
    }
}