65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的特性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (171投票s)

2002 年 9 月 25 日

9分钟阅读

viewsIcon

752927

downloadIcon

2

在本教程中,我们将了解如何创建特性并将其应用于各种程序实体,以及如何在运行时环境中检索特性信息。

引言

特性是一种新型的声明性信息。我们可以使用特性来定义设计级信息(例如帮助文件、文档 URL)和运行时信息(例如将 XML 字段与类字段关联)。我们还可以使用特性创建“自描述”组件。在本教程中,我们将了解如何创建特性并将其应用于各种程序实体,以及如何在运行时环境中检索特性信息。

定义

如 MSDN 所述(ms-help://MS.MSDNQTR.2002APR.1033/csspec/html/vclrfcsharpspec_17_2.htm)

"特性是指定给声明的附加声明性信息的一部分。" 

使用预定义特性

C# 中有一小组预定义的特性。在学习如何创建自己的自定义特性之前,我们将首先了解如何在代码中使用它们。
using System;
public class AnyClass 
{
    [Obsolete("Don't use Old method, use New method", true)]
    static void Old( ) { }
   
    static void New( ) { }
   
    public static void Main( ) 
    {
        Old( );
    }
}
请看这个例子。在此示例中,我们使用了 Obsolete 特性,它标记了一个不应使用的程序实体。第一个参数是字符串,它解释了为什么该项已过时以及应该使用什么来代替它。事实上,您可以在此处写入任何其他文本。第二个参数告诉编译器将该项的使用视为错误。默认值为 false,这意味着编译器会为此生成警告。

当我们尝试编译上面的程序时,我们将收到一个错误

AnyClass.Old()' is obsolete: 'Don't use Old method,  use New method'

开发自定义特性

现在我们将了解如何开发自己的特性。以下是创建自己的特性的简要方法。

根据 C# 语言规范(从 System.Attribute 抽象类派生的类,直接或间接派生,都是特性类。特性类的声明定义了一种新的可以在声明上放置的特性),将我们的特性类从 System.Attribute 类派生,我们就完成了。

using System;
public class HelpAttribute : Attribute
{
}
信不信由你,我们刚刚创建了一个自定义特性。我们可以像使用 obsolete 特性一样用它来修饰我们的类。
[Help()]
public class AnyClass
{
}
注意:按照惯例,特性类名称的后缀使用 Attribute。但是,当我们将其应用于程序实体时,我们可以不包含 Attribute 后缀。编译器首先在 System.Attribute 派生类中搜索该特性。如果找不到类,编译器将把 Attribute 添加到指定的特性名称中进行搜索。

但是,此特性目前没有任何用处。为了使其稍微有用一些,让我们在其中添加一些内容。

using System;
public class HelpAttribute : Attribute
{
    public HelpAttribute(String Descrition_in)
    {
        this.description = Description_in;
    }
    protected String description;
    public String Description 
    {
        get 
        {
            return this.description;
                 
        }            
    }    
}
[Help("this is a do-nothing class")]
public class AnyClass
{
}
在上面的示例中,我们在特性类中添加了一个属性,我们将在最后一节中在运行时查询它。

定义或控制我们的特性的用法

AttributeUsage 类是另一个预定义的类,它将帮助我们控制自定义特性的用法。也就是说,我们可以定义我们自己的特性类的特性。

它描述了自定义特性类如何使用。

在将 AttributeUsage 应用于我们的自定义特性时,我们可以设置其三个属性。第一个属性是

ValidOn

通过此属性,我们可以定义可以在其上放置自定义特性的程序实体。可以在其上放置特性的所有可能程序实体的集合列在 AttributeTargets 枚举器中。我们可以使用按位 OR 运算组合多个 AttributeTargets 值。

AllowMultiple

此属性标记我们的自定义特性是否可以在同一程序实体上放置多次。

Inherited

我们可以使用此属性控制特性的继承规则。此属性标记在基类上放置的特性是否也会被派生自该类的类继承。

让我们做一些实际操作。我们将 AttributeUsage 特性应用于我们的 Help 特性,并借助它控制我们特性的用法。

using System;
[AttributeUsage(AttributeTargets.Class), AllowMultiple = false, 
 Inherited = false ]
public class HelpAttribute : Attribute
{
    public HelpAttribute(String Description_in)
    {
        this.description = Description_in;
    }
    protected String description;
    public String Description
    {
        get 
        {
            return this.description;
        }            
    }    
}
首先看 AttributeTargets.Class。它表明 Help 特性只能放在类上。这意味着以下代码将导致错误
AnyClass.cs: Attribute 'Help' is not valid on this declaration type. 
It is valid on 'class' declarations only.
现在尝试将其放在方法上
[Help("this is a do-nothing class")]
public class AnyClass
{
    [Help("this is a do-nothing method")]    //error
    public void AnyMethod()
    {
    }
} 
我们可以使用 AttributeTargets.All 允许 Help 特性放置在任何程序实体上。可能的值是: 
  • Assembly, 
  • Module, 
  • Class, 
  • Struct, 
  • Enum, 
  • Constructor, 
  • Method, 
  • Property, 
  • Field,
  • Event, 
  • Interface, 
  • Parameter, 
  • Delegate, 
  • All = Assembly | Module | Class | Struct | Enum | Constructor | Method | Property | Field | Event | Interface | Parameter | Delegate,
  • ClassMembers = Class | Struct | Enum | Constructor | Method | Property | Field | Event | Delegate | Interface )

现在考虑 AllowMultiple = false。这表示特性不能多次放置。

[Help("this is a do-nothing class")]
[Help("it contains a do-nothing method")]
public class AnyClass
{
    [Help("this is a do-nothing method")]        //error
    public void AnyMethod()
    {
    }
}
这会生成编译时错误。
AnyClass.cs: Duplicate 'Help' attribute
好的,现在讨论最后一个属性。Inherited,表示当特性放置在基类上时,是否也会被派生自该基类的类继承。如果特性类的 Inherited 为 true,则该特性将被继承。但是,如果特性类的 Inherited 为 false 或未指定,则该特性不会被继承。

假设我们有以下类关系。

[Help("BaseClass")] 
public class Base
{
}

public class Derive :  Base
{
}
我们有四种可能的组合
  • [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false
  • [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false
  • [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true
  • [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true

第一种情况

如果我们查询(稍后我们将看到如何在运行时查询类的特性)派生类以获取 Help 特性,我们将找不到它,因为继承的特性设置为 false。

第二种情况

第二种情况没有区别,因为在这种情况下,继承的属性也设置为 false。

第三种情况

为了解释第三种和第四种情况,让我们也将相同的特性添加到派生类中。
[Help("BaseClass")] 
public class Base
{
}
[Help("DeriveClass")] 
public class Derive :  Base
{
}
现在,如果我们查询 Help 特性,我们将仅获得派生类的特性,因为继承为 true,但不允许重复,因此基类的 Help 被派生类的 Help 特性覆盖。

第四种情况

在第四种情况下,当我们查询派生类以获取 Help 特性时,我们将获得两个特性,因为在这种情况下,继承和重复都允许。

注意:AttributeUsage 特性仅对派生自 System.Attribute 的类有效,并且此特性的 AllowMultiple 和 Inherited 都为 false。

位置参数 vs. 命名参数

位置参数是特性的构造函数的参数。它们是强制性的,每次将特性应用于任何程序实体时都必须传递一个值。另一方面,命名参数实际上是可选的,不是特性构造函数的参数。

为了更详细地解释,让我们在 Help 类中添加另一个属性。

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false,
 Inherited = false)]
public class HelpAttribute : Attribute
{
    public HelpAttribute(String Description_in)
    {
        this.description = Description_in;
        this.verion = "No Version is defined for this class";
    }
    protected String description;
    public String Description
    {
        get 
        {
            return this.description;
        }
    }
    protected String version;
    public String Version
    {
        get 
        {
            return this.version;
        }
        //if we ever want our attribute user to set this property, 
        //we must specify set method for it 
        set 
        {
            this.verion = value;
        }
    }
}
[Help("This is Class1")]
public class Class1
{
}

[Help("This is Class2", Version = "1.0")]
public class Class2
{
}

[Help("This is Class3", Version = "2.0", 
 Description = "This is do-nothing class")]
public class Class3
{
}
当我们查询 Class1 以获取 Help 特性及其属性时,我们将获得

Help.Description : This is Class1
Help.Version :No Version is defined for this class
由于我们没有为 Version 属性定义任何值,因此使用了构造函数中设置的值。如果未定义值,则使用类型的默认值(例如:对于 int,默认值为零)。

现在,查询 Class2 将导致

Help.Description : This is Class2
Help.Version :  1.0
不要为可选参数使用多个构造函数。而是将其标记为命名参数。我们称它们为命名参数,因为当我们在构造函数中为其提供值时,必须为其命名。例如,在第二个类中,我们定义了 Help。
[Help("This is Class2", Version = "1.0")]
在 AttributeUsage 示例中,ValidOn 参数是位置参数,而 Inherited 和 AllowMultiple 是命名参数。

注意:要在特性的构造函数中设置命名参数的值,我们必须为该属性提供 set 方法,否则将生成编译时错误。

'Version' : Named attribute argument can't be a read only property
现在,当我们查询 Class3 以获取 Help 特性及其属性时会发生什么?结果是相同的编译时错误。
'Desciption' : Named attribute argument can't be a read only property
现在修改 Help 类并添加 Description 的 set 方法。现在输出将是
Help.Description : This is do-nothing class 
Help.Version : 2.0
后台发生的事情是,首先使用位置参数调用构造函数,然后为每个命名参数调用 set 方法。在构造函数中设置的值将被 set 方法覆盖。

参数类型

特性类的参数类型限制为
  • bool
  • byte, 
  • char
  • double
  • float,
  • int
  • long
  • short
  • string 
  • System.Type 
  • object 
  • 枚举类型,前提是它及其所在的任何嵌套类型都可公开访问。涉及上述任何类型的一维数组 

特性标识符

假设我们想将 Help 特性应用于整个程序集。第一个问题是,将 Help 特性放在哪里,以便编译器能够确定它是应用于整个程序集的?考虑另一种情况,我们想将一个特性应用于方法的返回类型。编译器如何确定我们将其应用于方法返回类型而不是整个方法?

为了解决这种歧义,我们使用特性标识符。借助特性标识符,我们可以显式说明我们正在应用特性的实体。

例如

[assembly: Help("this a do-nothing assembly")]
Help 特性之前的程序集标识符明确告诉编译器该特性已附加到整个程序集。可能的标识符是 
  • assembly
  • module
  • type
  • method
  • 属性
  • 事件
  • 字段
  • param
  • return

运行时查询特性

我们已经了解了如何创建特性以及如何将其应用于程序元素。现在是时候学习我们的类的用户如何在运行时查询这些信息了。

要查询程序实体有关其附加特性的信息,我们必须使用反射。反射是在运行时发现类型信息的能力。

我们可以使用 .NET Framework 反射 API 迭代整个程序集的元数据,并生成该程序集已定义的所有类、类型和方法的列表。

还记得旧的 Help 特性和 AnyClass 类吗?

using System;
using System.Reflection;
using System.Diagnostics;

//attaching Help attribute to entire assembly
[assembly : Help("This Assembly demonstrates custom attributes 
 creation and their run-time query.")]

//our custom attribute class
public class HelpAttribute : Attribute
{
    public HelpAttribute(String Description_in)
    {
        //
        // TODO: Add constructor logic here
        this.description = Description_in;
        //
    }
    protected String description;
    public String Description
    {
        get 
        {
            return this.deescription;
                 
        }            
    }    
}
//attaching Help attribute to our AnyClass
[HelpString("This is a do-nothing Class.")]
public class AnyClass
{
//attaching Help attribute to our AnyMethod
    [Help("This is a do-nothing Method.")]
    public void AnyMethod()
    {
    }
//attaching Help attribute to our AnyInt Field
    [Help("This is any Integer.")]
    public int AnyInt;
}
class QueryApp
{
    public static void Main()
    {
    }
}
我们将在接下来的两个部分将特性查询代码添加到我们的 Main 方法中。

查询程序集特性

在下面的代码中,我们获取当前进程名并使用 Assembly 类的 LoadFrom 方法加载程序集。然后我们使用 GetCustomAttributes 方法获取附加到当前程序集的所有自定义特性。接下来的 foreach 语句遍历所有特性,并尝试将每个特性强制转换为 Help 特性(使用 as 关键字强制转换对象的好处是,如果强制转换无效,我们不必担心抛出异常。取而代之的是,结果将是 null)。下一行检查强制转换是否有效且不等于 null,然后显示特性的 Help 属性。
class QueryApp
{
    public static void Main()
    {
        HelpAttribute HelpAttr;

        //Querying Assembly Attributes
        String assemblyName;
        Process p = Process.GetCurrentProcess();
        assemblyName = p.ProcessName + ".exe";

        Assembly a = Assembly.LoadFrom(assemblyName);

        foreach (Attribute attr in a.GetCustomAttributes(true))
        {
            HelpAttr = attr as HelpAttribute;
            if (null != HelpAttr)
            {
                Console.WriteLine("Description of {0}:\n{1}", 
                                  assemblyName,HelpAttr.Description);
            }
        }
}
}
以下程序的输出是
Description of QueryAttribute.exe:
This Assembly demonstrates custom attributes creation and 
their run-time query.
Press any key to continue

查询类、方法和字段特性

在下面的代码中,唯一不熟悉的是 Main 方法中的第一行。
Type type = typeof(AnyClass);
它使用 typeof 运算符获取与我们的 AnyClass 类关联的 Type 对象。其余的类特性查询代码与上面的示例类似,不需要任何解释(我认为)。

对于方法和字段特性的查询,我们首先获取类中存在的所有方法和字段,然后以与查询类特性相同的方式查询它们相关的特性。

class QueryApp
{
    public static void Main()
    {

        Type type = typeof(AnyClass);
        HelpAttribute HelpAttr;


        //Querying Class Attributes
        foreach (Attribute attr in type.GetCustomAttributes(true))
        {
            HelpAttr = attr as HelpAttribute;
            if (null != HelpAttr)
            {
                Console.WriteLine("Description of AnyClass:\n{0}", 
                                  HelpAttr.Description);
            }
        }
        //Querying Class-Method Attributes  
        foreach(MethodInfo method in type.GetMethods())
        {
            foreach (Attribute attr in method.GetCustomAttributes(true))
            {
                HelpAttr = attr as HelpAttribute;
                if (null != HelpAttr)
                {
                    Console.WriteLine("Description of {0}:\n{1}", 
                                      method.Name, 
                                      HelpAttr.Description);
                }
            }
        }
        //Querying Class-Field (only public) Attributes
        foreach(FieldInfo field in type.GetFields())
        {
            foreach (Attribute attr in field.GetCustomAttributes(true))
            {
                HelpAttr= attr as HelpAttribute;
                if (null != HelpAttr)
                {
                    Console.WriteLine("Description of {0}:\n{1}",
                                      field.Name,HelpAttr.Description);
                }
            }
        }
    }
}
以下程序的输出是。
Description of AnyClass:
This is a do-nothing Class.
Description of AnyMethod:
This is a do-nothing Method.
Description of AnyInt:
This is any Integer.
Press any key to continue
© . All rights reserved.