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

C# 讲座 - 第 6 讲:C# 中的特性、自定义特性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (35投票s)

2016年3月1日

CPOL

8分钟阅读

viewsIcon

53336

downloadIcon

1136

我系列讲座的第 6 讲。本讲座是关于特性的。

全部课程集


引言

我的系列讲座的第 6 篇关于一种名为自定义特性的 .NET 技术。自定义特性使您能够向源代码添加更多内容,并为程序集元数据表中的几乎所有记录添加更多信息。我们可以在运行时处理这些元数据,它可能会影响应用程序的流程。WPF、WCF 等最著名的 .NET 技术大量使用特性。每个 .NET 工程师都应该熟悉这项技术,以便在 .NET 和 C# 的工作中效率更高。

特性领域

特性指定有关程序中声明的实体的声明性信息。每个人都知道方法或成员修饰符,例如:privatepublicprotectedinternal。这些是特性,标准特性。除了标准特性之外,C# 还赋予程序员创建自己的特性的能力。例如,您可以在类上添加 ModelAttribute 特性,并在其中描述类在软件模型和设计中的呈现方式。之后,您可以生成一些包含 ModelAttribute 的模型文档,以查看模型是否由类和结构正确表示。特性信息可以在运行时检索,这对于特定场景非常有用。我们应该知道的关于特性的主要内容是它是元数据。大多数特性对编译器没有任何意义,编译器会忽略它们,只将它们存储到程序集的元数据中。.NET 库中有数百个特性可供您在代码中使用。举几个例子:DllImportConditionalObsoleteSerializable 等,这些是在几乎每个项目中都会使用的特性。

特性内部是什么

您应该知道和记住的第一件事是,特性是特定类型的一个对象。该类型应直接或间接派生自 System.Attribute 抽象类。C# 只支持 CLS 兼容的特性,要实现这一点,您应该从 System.Attribute 派生您的特性类型。由于特性是类型的对象,因此该类型必须具有 public 构造函数。应用特性与通过调用构造函数创建对象相同。在声明特性时,还可以使用的另一个附加项是定义开放字段、属性或类。应用特性的语法是

[attribute(positional_parameters, name_parameter = value, ...)]

特性的名称及其值在方括号内指定,位于应用特性的元素之前。位置参数指定基本信息,命名参数指定可选信息。位置参数通过构造函数传递给特性,命名参数是每个非静态公共读写属性或字段。请参见下面的示例

[AttributeUsage(validon,AllowMultiple=allowmultiple,Inherited=inherited)]

您可以为每个元素应用多个特性。您可以将特性放在单独的 [] 括号中,或者在一个括号中使用逗号逐个指定。单独的特性和用逗号分隔的特性的顺序无关紧要。

特性示例

在 C# 中,自定义特性的名称存储在 [] 括号中。您应该在类名、方法名、对象名等之前直接定义特性。让我们看一下下面的代码,我在注释中解释了所使用的特性的目的

//attribute DebuggerDisplay is used to show information about object of the
//class while debugging once I will move mouse pointer to this object I will
//see string that shows me the status of the object by sharing values of
//string_status and int1 member variables
[DebuggerDisplay("String status is={m_string_status}, second integer is: {m_int2}")]
internal class AttributesSampleClass
{
    private int m_int1 = 1;
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]//using this attribute 
            	//value of Int2 property will not be shown in debugger
     private int m_int2 = 2;
     private string m_string_status;

     [Obsolete("This method is obsolete, 
      use ConsolseOutput2 instead")]//having this attribute 
          	//we will have compiler warning when trying to use this method
      public void ConsoleOutput1()
      {
           Console.WriteLine("Output 1");
      }
      [DebuggerStepThrough]//this parameter says that debugger shouldn't step in this function
      public void ConsoleOutput2()
      {
          Console.WriteLine("Output 2");
      }
      public AttributesSampleClass() 
      {
      }
      //attribute flags indicates that numeration can be treated as a bit field
      [Flags]
      public enum EnumFlags 
      {
          Flag1 = 1,
          Flag2 = 2,
          Flag3 = 4,
          Flag4 = 8,
          Flag5 = 16,
          Flag6 = 32
      }
      [DebuggerNonUserCode]
      [DllImport("User32.dll")]// this attribute from System.Runtime.InteropServices gives
                                    // us ability to load SetForegroundWindow from User32.dll
      public static extern int SetForegroundWindow(IntPtr point);
}

     //SAMPLE
      Console.WriteLine("------------------SAMPLE---------------------");
      AttributesSampleClass sample = new AttributesSampleClass();
      sample.ConsoleOutput1();//when I call it like this i have warning 
      		//while building the project, but code executes
      sample.ConsoleOutput2();//works well

下图显示了我们如何从调试器中隐藏整数成员 m_int2 的值,但在鼠标指针指向类型时显示它。这是使用特性进行的调试

如您所见,特性在日常开发生活中非常重要且常用。要有效地使用 .NET,您首先需要了解其特性以及在需要时如何使用它们。请记住,C# 允许您将特性应用于任何可以表示为元数据的内容。

预定义特性

.NET Framework 提供了三个预定义特性

  • AttributeUsage
  • Conditional
  • Obsolete

创建自己的自定义特性并使用它们

现在我们了解了特性、它们的使用以及所有特性都派生自 System.Attribute 的事实,让我们一步一步创建自己的特性

您可以分析以下内容:AssemblyModuleParameterInfoMemberInfoTypeMethodInfoConstructorInfoFieldInfoEventInfoPropertyInfo 以及与之相关的 *Builder。所有这些都具有 IsDefinedGetCustomAttributes

  1. 首先,我们需要定义一个派生自 System.Attributepublic 类。在我的例子中,这个类名是 MyOwnExcitingAttribute。虽然没有强制要求,但微软建议在您的特性类型名称中使用 Attribute 后缀。
  2. 其次,任何 Attribute 类都必须至少有一个公共构造函数。通过向构造函数添加参数,您可以指定使用您的特性的开发人员需要提供的内容。除此之外,您可以定义非静态的公共字段,开发人员可以选择定义或不定义。
  3. 当您定义自己的 Attribute 类时,您可以使用 AttributeUsageAttribute 来定义您特性的使用范围,它将仅应用于特定的类型或成员。除此之外,您还可以使用 AttributeUsageAttribute 配置您的自定义特性的其他行为
    1. 如果您在 AttributeUsageAttribute 中使用 Inherited=true 属性,则特性可以被继承。
    2. 大多数特性不能应用于同一个元素多次,实际上也没有理由这样做。如果您想将同一个特性应用于实体多次,请使用命名参数 AllowMultiple 并将其值设置为 true
  4. 定义特性本身并将其应用于某些成员是没有用的。最终您在程序集中得到的是额外的元数据,对应用程序的工作流程没有影响。这只是部分正确。运行时代码通过一个称为反射的机制进行分析。本文不深入探讨反射,仅回顾其用法。当您编写的代码根据特定特性的使用而表现不同时,您应该自己检查这些特性。有几种方法可以检查特性是否已定义。我们将回顾其中的两种
    1. 您可以调用 Type 类型的 IsDefined 方法来检查是否有某个特性与 type 相关联。如果 IsDefined 方法返回 true,则表示 type 与请求的特性相关联
      if (user.GetType().IsDefined(typeof(MyOwnExcitingAttribute), false))

      这是我示例中的代码。您可以在随附的源代码存档中看到整个代码。请注意,这种检查特性是否已定义的方法仅适用于类型。

    2. 当我们需要检查特性是否应用于方法、程序集或模块时,我们需要使用不同于上述描述的方法。为此,我们可以使用 System.Reflection.CustomAttributeExtensions 类。该类包含用于获取特性的 static 方法。总的来说,该类具有以下三个方法,它们有许多重载
      • GetCustomAttribute - 如果特性应用于特定模块,则返回请求特性的实例。此实例包含在编译前定义的字段和值。如果未应用此类特性,则返回 null
      • GetCustomAttributes - 返回特定类型特性的实例数组。与上一个类似,每个成员都有在编译前分配的参数。如果未找到任何内容,则返回空集合。
      • IsDefined - 如果应用了请求类型的某些特性到特定元素,则返回 true。这是一个非常快速的方法,因为它不会重新序列化任何内容,只是检查特性是否已应用。
  5. Attribute 类建议
    1. 尝试使用一个构造函数
    2. 尽量避免公共成员,而是使用属性
    3. 尝试为特性成员和字段使用简单类型,不要使用复杂类型或多维数组(您可以在我的文章 这里 中阅读更多关于基本类型的信息)。这是一个建议,您可以在您的 attribute 类中使用更复杂的成员,但不能保证它将是 CLS 兼容的。
    4. 尽量不要在 Attribute 类中有方法 - 这个类的想法是保持简单并保存一些状态,尽量避免在 Attribute 类中构建复杂的逻辑。
    5. 将特性定义为密封。一旦您将类放入 GetCustomAttributes,它将查找特定的特性或派生自它的特性。它可能不会返回您正在寻找的确切类,并且您总是需要检查返回特性的类型。为了避免这种检查,请在定义特性时使用 sealed

下面的代码演示了本节所述内容的示例

[AttributeUsage(AttributeTargets.All,Inherited=false, AllowMultiple=false)]//I want 
     //my attribute to be applied to anything where it is possible - AttributeTargets.All
     //also my attribute can be inherited - Inherited=true
     //also my attribute can't be applied several times for one element - AllowMultiple=false
public sealed class MyOwnExcitingAttribute : System.Attribute
{
    private string m_StringData;
    private int m_IntegerData;
    
    public MyOwnExcitingAttribute()
    {
        m_StringData = "default value";
    }
    public MyOwnExcitingAttribute(string s)
    {
        m_StringData = s;
    }
    public int IntegerData
    {
        get { return m_IntegerData; }
        set { m_IntegerData = value; }
    }
    public string StringData
    {
        get { return m_StringData; }//we encapsulate only reading, 
        		//you can set this variable only in constructor
    }
}
public class AttributeUser
{
    [MyOwnExciting("custom value applied to function", IntegerData = 5)]
    public void FunctionThatDoSomething()
    {
    }
    [MyOwnExciting("custom value applied to member", IntegerData = 10)]
    public int m_IntData;
    [MyOwnExciting(IntegerData = 10)]//here string data will have "default value"
    public int m_IntData2;
}
public class NotAttributeUser
{
}

    //CUSTOM ATTRIBUTES
    AttributeUser user = new AttributeUser();
    //check if attribute is applied
    if (user.GetType().IsDefined(typeof(MyOwnExcitingAttribute), false))
    {
        Console.WriteLine("Attribute is defined");
    }
    NotAttributeUser not_user = new NotAttributeUser();
    //check if attribute is applied
    if (not_user.GetType().IsDefined(typeof(MyOwnExcitingAttribute), false))
    {
    
    }
    else 
    {
        Console.WriteLine("Attribute is not defined");
    }
    //reflection
    var members = from m in typeof(AttributesSampleClass).GetTypeInfo().DeclaredMembers select m;
    foreach (MemberInfo m in members)
    {
        ShowAttributes(m);
    }
    var members2 = from m2 in typeof(AttributeUser).GetTypeInfo().DeclaredMembers select m2;
    foreach (MemberInfo m2 in members2)
    {
        ShowAttributes(m2);
    }
}

public static void ShowAttributes(MemberInfo member)
{
    var attributes = member.GetCustomAttributes<Attribute>();//getting all attributes for specific member
    Console.WriteLine(member.Name + " attributes are:");
    foreach (Attribute at in attributes)
    {
        Console.WriteLine("Attribute type is: " + at.GetType().ToString());
        if (at is MyOwnExcitingAttribute)
        {
            Console.WriteLine("String data is: " + ((MyOwnExcitingAttribute)at).StringData);
            Console.WriteLine("int data is: " + ((MyOwnExcitingAttribute)at).IntegerData);
        }
    }
}

一些有用的标准特性

  • [DebuggerDisplay] - 使用它,您可以在调试时将鼠标指针指向类型时显示类型成员的值
  • [DebuggerBrowsable] - 使用它,您可以控制成员或属性在调试器中显示的方式
  • [DebuggerTypeProxy] - 当您需要在调试器中对类型进行重大更改而不更改原始类型时使用
  • [DebuggerStepThrough] - 用于避免在调试器中单步进入方法
  • [DebuggerNonUserCode] - 指示类型或成员不是应用程序的用户代码
  • [Obsolete] - 用于通知开发人员不应使用此版本的代码,而应使用他们已使用的版本。这会导致编译警告。
  • [Conditional] - 应用于方法或类。可用于启用或禁用诊断信息的显示
  • [Flags] - 适用于 enum,表示枚举可以被视为位字段

您可以在 这里 找到标准特性的完整列表。

来源

 

© . All rights reserved.