Bootstrap

详解C#中的特性(Attribute)

一 特性概述

1.1 特性概念

在C#中,特性(Attributes)是一种向程序元素(如类、方法、属性等)添加元数据的方式。特性可以用来提供关于程序元素的附加信息,这些信息可以在编译和运行时被访问。.Net 框架提供了两种类型的特性:预定义特性和自定义特性。

1.2 特性特点

特性语法有以下特点:

  1. 按照惯例,所有特性名称都以“Attribute”结尾。但是,在代码中使用特性时不需要指定属性后缀。例如,[DllImport]相当于[DllImportAttribute],但是DllImportAttribute属性在 .NET 类库中的实际名称。
  2. 可以将多个特性应用于程序元素(例如类和属性)
  3. 特性可以像方法和属性一样接受参数
  4. 某些特性可以多次指定。这种多用途属性的一个示例是ConditionalAttribute
[Conditional("DEBUG"), Conditional("TEST1")]
void TraceMethod()
{
    // ...
}

1.3 特性类中的位置参数和命名参数

特性类可以有位置参数命名参数。特性类的每个公共构造函数都定义该特性类的有效位置参数序列。特性类的每个非静态公共读写字段和属性都为该特性类定义一个命名参数。对于定义命名参数的属性,该属性应具有公共 get 访问器和公共 set 访问器。

示例:以下示例定义了一个名为 的特性HelpAttribute,它具有一个位置参数url和一个命名参数Topic。虽然它是非静态和公共的,但该属性Url没有定义命名参数,因为它不是读写的。还显示了该特性的两种用途:

[AttributeUsage(AttributeTargets.Class)]
public class HelpAttribute : Attribute
{
    public HelpAttribute(string url) // url is a positional parameter
    { 
        ...
    }

    // Topic is a named parameter
    public string Topic
    { 
        get;
        set;
    }

    public string Url { get; }
}

[Help("http://www.mycompany.com/xxx/Class1.htm")]
class Class1
{
}

[Help("http://www.mycompany.com/xxx/Misc.htm", Topic ="Class2")]
class Class2
{
}

1.4 特性参数

任何位置参数都必须按一定顺序指定,并且不能省略。命名参数是可选的,并且可以按任何顺序指定。首先指定位置参数。例如,这三个特性是等效的:

[DllImport("user32.dll")]
[DllImport("user32.dll", SetLastError=false, ExactSpelling=false)]
[DllImport("user32.dll", ExactSpelling=false, SetLastError=false)]

第一个参数,即 DLL 名称,是位置参数,并且始终位于第一位;在这种情况下,两个命名参数都默认为 false,因此可以省略它们。位置参数对应于特性构造函数的参数。命名参数或可选参数对应于特性的属性或字段。

1.5 特性目标

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

要显式标识性目标,请使用以下语法:

[target : attribute-list]

下表显示了target的值和对应的目标

assembly整个装配
module当前装配模块
field类或结构中的字段
event事件
method方法或get属性set访问器
param方法参数或set属性访问器参数
property属性
return方法、属性索引器或get属性访问器的返回值
type结构体、类、接口、枚举或委托

以下示例演示如何将性应用到程序集和模块

using System;
using System.Reflection;
[assembly: AssemblyTitleAttribute("Production assembly 4")]
[module: CLSCompliant(true)]

以下示例演示如何将性应用于 C# 中的方法、方法参数和方法返回值。

// default: applies to method
[ValidatedContract]
int Method1() { return 0; }

// applies to method
[method: ValidatedContract]
int Method2() { return 0; }

// applies to parameter
int Method3([ValidatedContract] string contract) { return 0; }

// applies to return value
[return: ValidatedContract]
int Method4() { return 0; }

二 .C# 编译器有关的特性

2.1 [Conditional]特性

Conditional特性使得方法执行依赖于预处理标识符,需引入命名空间:System.Diagnostics;

当调用标记为条件的方法时,指定的预处理符号是否存在将决定编译器是包含还是省略对该方法的调用。 如果定义了符号,则将包括调用;否则,将忽略该调用。

条件方法必须是类或结构声明中的方法,而且必须具有void返回类型,返回非void类型会报错。

//#define TRACE_ON
using System;
using System.Diagnostics;

namespace LearnCS1
{
    public class Trace
    {
        [Conditional("TRACE_ON")]
        public static void Msg(string msg)
        {
            Console.WriteLine(msg);
        }
    }
    class MainClass
    {
        static void Main()
        {
            Trace.Msg("Now in Main...");
            Console.WriteLine("Done.");
        }
    }
}

Conditional 特性通常与 DEBUG 标识符一起使用,以启用调试生成(而非发布生成)中的跟踪和日志记录功能,如下例所示:

[Conditional("DEBUG")]
static void DebugMethod()
{
}

如果某个方法具有多个 Conditional 特性,则如果定义了一个或多个条件符号,编译器会包含对该方法的调用。 在以下示例中,存在 A 或 B 将导致方法调用:

[Conditional("A"), Conditional("B")]
static void DoIfAorB()
{
    // ...
}

2.2 [Obsolete]特性

用途:标记已过时的代码,使得在使用过时成员后,编译时发出警告或编译报错。

namespace LearnCS1
{
    
    class MainClass
    {
        // Mark OldProperty As Obsolete.
        [ObsoleteAttribute("This property is obsolete.", false)]
        public static string OldProperty
        { get { return "The old property value."; } }


        // Mark CallOldMethod As Obsolete.
        [ObsoleteAttribute("This method is obsolete.", true)]
        public static string CallOldMethod()
        {
            return "You have called CallOldMethod.";
        }

        static void Main()
        {
            Console.WriteLine(OldProperty);
            Console.WriteLine();
            Console.WriteLine(CallOldMethod());

        }
    }
}

参考:C# 编译器解释的杂项属性

参考:特性

三.其他预定义特性

3.1 [Serializable]特性

将Serializable特性应用于类,以指示可以使用二进制或 XML 序列化对此类型的实例进行序列化。

示例需引入程序集System.Runtime.Serialization.Formatters.Soap

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Soap;

namespace Console01
{
    public class Test
    {
        public static void Main()
        {

            // Creates a new TestSimpleObject object.
            TestSimpleObject obj = new TestSimpleObject();

            Console.WriteLine("Before serialization the object contains: ");
            obj.Print();

            // Opens a file and serializes the object into it in binary format.
            Stream stream = File.Open("data.xml", FileMode.Create);
            SoapFormatter formatter = new SoapFormatter();

            formatter.Serialize(stream, obj);
            stream.Close();

            // Empties obj.
            obj = null;

            // Opens file "data.xml" and deserializes the object from it.
            stream = File.Open("data.xml", FileMode.Open);
            formatter = new SoapFormatter();

            obj = (TestSimpleObject)formatter.Deserialize(stream);
            stream.Close();

            Console.WriteLine("");
            Console.WriteLine("After deserialization the object contains: ");
            obj.Print();
        }
    }

    // A test object that needs to be serialized.
    [Serializable()]
    public class TestSimpleObject
    {

        public int member1;
        public string member2;
        public string member3;
        public double member4;

        // A field that is not serialized.
        [NonSerialized()] public string member5;

        public TestSimpleObject()
        {

            member1 = 11;
            member2 = "hello";
            member3 = "hello";
            member4 = 3.14159265;
            member5 = "hello world!";
        }

        public void Print()
        {

            Console.WriteLine("member1 = '{0}'", member1);
            Console.WriteLine("member2 = '{0}'", member2);
            Console.WriteLine("member3 = '{0}'", member3);
            Console.WriteLine("member4 = '{0}'", member4);
            Console.WriteLine("member5 = '{0}'", member5);
        }
    }
}

3.2 [DllImport]特性

用于指示要在程序中调用非托管代码(通常是 DLL)的方法

using System;
using System.Runtime.InteropServices;

class Example
{
    // Use DllImport to import the Win32 MessageBox function.
    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);

    static void Main()
    {
        // Call the MessageBox function using platform invoke.
        MessageBox(new IntPtr(0), "Hello World!", "Hello Dialog", 0);
    }
}

四.自定义特性

可以通过定义特性类(Attribute派生的类)来创建自定义特性。假设您想使用编写该类型的程序员的姓名来标记类型。可以定义自定义Author特性类:

public class AuthorAttribute : System.Attribute
{
    private string Name;
    public double Version;

    public AuthorAttribute(string name)
    {
        Name = name;
        Version = 1.0;
    }
}

类名AuthorAttribute是特性的名称Author加上Attribute后缀。它派生自System.Attribute,因此它是一个自定义特性类。构造函数的参数是自定义特性的位置参数。在此示例中,name是位置参数。任何公共读写字段或属性都是命名参数。在本例中,version是唯一的命名参数。

可以按如下方式使用这个特性:

[Author("P. Ackerman", Version = 1.1)]
class SampleClass
{
    // P. Ackerman's code goes here...
}

五.[AttributeUsage]特性

AttributeUsage特性确定自定义特性类的使用方式。换言之,AttributeUsage是应用到自定义特性的特性。 AttributeUsage特性帮助控制:

  • 可对哪些程序元素应用特性。 除非使用限制,否则特性可能应用到以下任意程序元素:
    • 程序集
    • 模块
    • 字段
    • 事件
    • 方法
    • 参数
    • properties
    • 返回
    • 类型
  • 某特性是否可多次应用于单个程序元素。
  • 派生类是否继承特性。

显式应用时,默认设置如以下示例所示:

[AttributeUsage(AttributeTargets.All,
                   AllowMultiple = false,
                   Inherited = true)]
class NewAttribute : Attribute { }

第一个  参数必须是 AttributeTargets枚举的一个或多个元素。 可将多个目标类型与 OR 运算符链接在一起,如下例所示:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
class NewPropertyOrFieldAttribute : Attribute { }

六.通过反射访问自定义特性

如果没有某种方式访问自定义特性,将自定义特性放入代码中将没有价值。通过使用反射,可以访问自定义特性的信息,关键方法是GetCustomAttributes。

这是一个完整的例子。定义自定义属性,将其应用于多个实体,并通过反射检索。

// Multiuse attribute.
[System.AttributeUsage(System.AttributeTargets.Class |
                       System.AttributeTargets.Struct,
                       AllowMultiple = true)  // Multiuse attribute.
]
public class AuthorAttribute : System.Attribute
{
    string Name;
    public double Version;

    public AuthorAttribute(string name)
    {
        Name = name;

        // Default value.
        Version = 1.0;
    }

    public string GetName() => Name;
}

// Class with the Author attribute.
[Author("P. Ackerman")]
public class FirstClass
{
    // ...
}

// Class without the Author attribute.
public class SecondClass
{
    // ...
}

// Class with multiple Author attributes.
[Author("P. Ackerman"), Author("R. Koch", Version = 2.0)]
public class ThirdClass
{
    // ...
}

class TestAuthorAttribute
{
    public static void Test()
    {
        PrintAuthorInfo(typeof(FirstClass));
        PrintAuthorInfo(typeof(SecondClass));
        PrintAuthorInfo(typeof(ThirdClass));
    }

    private static void PrintAuthorInfo(System.Type t)
    {
        System.Console.WriteLine($"Author information for {t}");

        // Using reflection.
        System.Attribute[] attrs = System.Attribute.GetCustomAttributes(t);  // Reflection.

        // Displaying output.
        foreach (System.Attribute attr in attrs)
        {
            if (attr is AuthorAttribute a)
            {
                System.Console.WriteLine($"   {a.GetName()}, version {a.Version:f}");
            }
        }
    }
}
/* Output:
    Author information for FirstClass
       P. Ackerman, version 1.00
    Author information for SecondClass
    Author information for ThirdClass
       R. Koch, version 2.00
       P. Ackerman, version 1.00
*/

;