特性
特性(C# - vs2010)
特性具有以下属性:
-
特性可向程序中添加元数据。 元数据是有关在程序中定义的类型的信息。 所有的 .NET 程序集都包含指定的一组元数据,这些元数据描述在程序集中定义的类型和类型成员。 可以添加自定义特性,以指定所需的任何附加信息。 有关更多信息,请参见 创建自定义特性(C# 和 Visual Basic)。
-
可以将一个或多个特性应用到整个程序集、模块或较小的程序元素(如类和属性)。
-
特性可以与方法和属性相同的方式接受参数。
-
程序可以使用反射检查自己的元数据或其他程序内的元数据。 有关更多信息,请参见 使用反射访问特性(C# 和 Visual Basic)。
使用特性
特性可以放置在几乎所有的声明中(但特定的特性可能限制在其上有效的声明类型)。 在 C# 中,特性的指定方法为:将括在方括号中的特性名置于其应用到的实体的声明上方。 在 Visual Basic 中,特性括在尖括号 (< >) 内。 它必须位于所应用于的元素的紧前面并与该元素在同一行。
在本例中,使用特性 SerializableAttribute 将特定特征应用于类:
[System.Serializable] public class SampleClass { // Objects of this type can be serialized. }
具有 DllImportAttribute 特性的方法的声明如下:
using System.Runtime.InteropServices; ... [System.Runtime.InteropServices.DllImport("user32.dll")] extern static void SampleMethod();
一个声明上可放置多个特性:
using System.Runtime.InteropServices; ... void MethodA([In][Out] ref double x) { } void MethodB([Out][In] ref double x) { } void MethodC([In, Out] ref double x) { }
某些特性对于给定实体可以指定多次。 例如,ConditionalAttribute 就是一个可多次使用的特性:
[Conditional("DEBUG"), Conditional("TEST1")] void TraceMethod() { // ... }
注意 |
---|
根据约定,所有特性名称都以单词“Attribute”结束,以便将它们与“.NET Framework”中的其他项区分。 但是,在代码中使用特性时,不需要指定 attribute 后缀。 例如,[DllImport] 虽等效于 [DllImportAttribute],但 DllImportAttribute 才是该特性在 .NET Framework 中的实际名称。 |
特性参数
许多特性都有参数,而这些参数可以是定位参数、未命名参数或命名参数。 任何定位参数都必须按特定顺序指定并且不能省略,而命名参数是可选的且可以按任意顺序指定。 首先指定定位参数。 例如,这三个特性是等效的:
[DllImport("user32.dll")] [DllImport("user32.dll", SetLastError=false, ExactSpelling=false)] [DllImport("user32.dll", ExactSpelling=false, SetLastError=false)]
第一个参数(DLL 名称)是定位参数并且总是第一个出现,其他参数为命名参数。 在这种情况下,两个命名参数均默认为 false,因此可将其省略。 有关默认参数值的信息,请参考各个特性的文档。
特性目标
特性的目标是应用该特性的实体。 例如,特性可以应用于类、特定方法或整个程序集。 默认情况下,特性应用于它后面的元素。 但是,您也可以显式标识要将特性应用于方法还是它的参数或返回值。
若要显式标识特性目标,请使用下面的语法:
[target : attribute-list]
下表显示了可能的 target 值的列表。
C# |
Visual Basic |
适用对象 |
---|---|---|
assembly |
Assembly |
整个程序集 |
module |
Module |
当前程序集模块(不同于 Visual Basic 模块) |
field |
不支持 |
在类或结构中的字段 |
event |
不支持 |
event |
method |
不支持 |
方法或 get 和 set 属性访问器 |
param |
不支持 |
方法参数或 set 属性访问器参数 |
property |
不支持 |
Property |
return |
不支持 |
方法、属性索引器或 get 属性访问器的返回值 |
type |
不支持 |
结构、类、接口、枚举或委托 |
下面的示例演示如何将特性应用于程序集和模块。 有关更多信息,请参见 常用特性(C# 和 Visual Basic)。
using System; using System.Reflection; [assembly: AssemblyTitleAttribute("Production assembly 4")] [module: CLSCompliant(true)]
下面的示例演示如何在 C# 中将特性应用于方法、方法参数和方法返回值。
// default: applies to method [SomeAttr] int Method1() { return 0; } // applies to method [method: SomeAttr] int Method2() { return 0; } // applies to return value [return: SomeAttr] int Method3() { return 0; }
注意 |
---|
无论规定 SomeAttr 应用于什么目标,都必须指定 return 目标,即使 SomeAttr 被定义为仅应用于返回值也是如此。 换言之,编译器将不使用 AttributeUsage 信息解析不明确的特性目标。 有关更多信息,请参见 AttributeUsage(C# 和 Visual Basic)。 |
特性的常见用途
以下列表包含特性的几个常见用途:
-
在 Web 服务中,使用 WebMethod 特性来标记方法,以指示该方法应该可通过 SOAP 协议进行调用。 有关更多信息,请参见 WebMethodAttribute。
-
描述当与本机代码进行交互操作时如何封送方法参数。 有关更多信息,请参见 MarshalAsAttribute。
-
描述类、方法和接口的 COM 属性。
-
使用 DllImportAttribute 类调用非托管代码。
-
在标题、版本、说明或商标方面描述您的程序集。
-
描述要持久性序列化类的哪些成员。
-
描述如何映射类成员和 XML 节点以便进行 XML 序列化。
-
描述方法的安全要求。
-
指定用于强制安全性的特性。
-
由实时 (JIT) 编译器控制优化,以便易于调试代码。
创建自定义特性
通过定义一个特性类,可以创建您自己的自定义特性。该特性类直接或间接地从 Attribute 派生,有助于方便快捷地在元数据中标识特性定义。 假设您要用编写类型的程序员的名字标记类型。 可以定义一个自定义 Author 特性类:
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct) ] public class Author : System.Attribute { private string name; public double version; public Author(string name) { this.name = name; version = 1.0; } }
类名是特性的名称,即 Author。 它由 System.Attribute 派生而来,因此是自定义特性类。 构造函数的参数是自定义特性的定位参数。 本示例中 name 是定位参数。 任何公共的读写字段或属性都是命名参数。 在本例中,version 是唯一的命名参数。 请注意 AttributeUsage 特性的用法,它使得 Author 特性仅在类和 struct(在 Visual Basic 中是 Structure)声明中有效。
可以按如下所示使用此新特性:
[Author("P. Ackerman", version = 1.1)] class SampleClass { // P. Ackerman's code goes here... }
AttributeUsage 有一个命名参数 AllowMultiple,使用它可以使自定义特性成为一次性使用或可以使用多次的特性。 在下面的代码示例中,创建了一个使用多次的特性。
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct, AllowMultiple = true) // multiuse attribute ] public class Author : System.Attribute
在下面的代码示例中,向某个类应用了同一类型的多个特性。
[Author("P. Ackerman", version = 1.1)] [Author("R. Koch", version = 1.2)] class SampleClass { // P. Ackerman's code goes here... // R. Koch's code goes here... }
注意 |
---|
如果特性类包含一个属性,则该属性必须为读写属性。 |
AttributeUsage
确定可以如何使用自定义特性类。 AttributeUsage 是一个可应用于自定义特性定义,以便控制如何应用新特性的特性。 显式应用时默认设置如下所示:
[System.AttributeUsage(System.AttributeTargets.All, AllowMultiple = false, Inherited = true)] class NewAttribute : System.Attribute { }
在本例中,NewAttribute 类可应用于任何支持特性的代码实体,但是对每个实体只可应用一次。 当应用于基类时,它可由派生类继承。
AllowMultiple 和 Inherited 参数是可选的,所以此代码具有相同的效果:
[System.AttributeUsage(System.AttributeTargets.All)]
class NewAttribute : System.Attribute { }
第一个 AttributeUsage 参数必须是 AttributeTargets 枚举的一个或多个元素。 多个目标类型可使用 OR 运算符链接在一起,如下例所示:
using System; ... [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] class NewPropertyOrFieldAttribute : Attribute { }
如果 AllowMultiple 参数设置为 true,则返回特性可对单个实体应用多次,如下例所示:
using System; ... [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] class MultiUseAttr : Attribute { } [MultiUseAttr] [MultiUseAttr] class Class1 { } [MultiUseAttr, MultiUseAttr] class Class2 { }
在这种情况下,由于 AllowMultiple 设置为 true,MultiUseAttr 可反复应用。 所示的应用多个特性的这两种格式均有效。
如果 Inherited 设置为 false,则该特性不由从特性化的类派生的类继承。 例如:
using System; ... [AttributeUsage(AttributeTargets.Class, Inherited = false)] class Attr1 : Attribute { } [Attr1] class BClass { } class DClass : BClass { }
在这种情况下,Attr1 不通过继承应用于 DClass。
备注
AttributeUsage 特性是一个单用途特性,它无法对相同的类应用多次。 AttributeUsage 是 AttributeUsageAttribute 的别名。
有关更多信息,请参见 使用反射访问特性(C# 和 Visual Basic)。
示例
下面的示例将阐释 Inherited 参数和 AllowMultiple 参数对 AttributeUsage 特性的效果,以及如何才能枚举应用于类的自定义特性。
using System; ... // Create some custom attributes: [AttributeUsage(System.AttributeTargets.Class, Inherited = false)] class A1 : System.Attribute { } [AttributeUsage(System.AttributeTargets.Class)] class A2 : System.Attribute { } [AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] class A3 : System.Attribute { } // Apply custom attributes to classes: [A1, A2] class BaseClass { } [A3, A3] class DerivedClass : BaseClass { } public class TestAttributeUsage { static void Main() { BaseClass b = new BaseClass(); DerivedClass d = new DerivedClass(); // Display custom attributes for each class. Console.WriteLine("Attributes on Base Class:"); object[] attrs = b.GetType().GetCustomAttributes(true); foreach (Attribute attr in attrs) { Console.WriteLine(attr); } Console.WriteLine("Attributes on Derived Class:"); attrs = d.GetType().GetCustomAttributes(true); foreach (Attribute attr in attrs) { Console.WriteLine(attr); } } }
输出
Attributes on Base Class: A1 A2 Attributes on Derived Class: A3 A3 A2
使用反射访问特性
如果没有检索自定义特性的信息和对其进行操作的方法,则定义自定义特性并将其放置在源代码中就没有意义。 使用反射,可检索用自定义特性定义的信息。 主要方法是 GetCustomAttributes,它返回对象数组,这些对象在运行时等效于源代码特性。 此方法具有多个重载版本。 有关更多信息,请参见 Attribute。
特性规范,如:
[Author("P. Ackerman", version = 1.1)] class SampleClass
在概念上等效于:
Author anonymousAuthorObject = new Author("P. Ackerman"); anonymousAuthorObject.version = 1.1;
但是,直到查询 SampleClass 来获取特性后才会执行此代码。 对 SampleClass 调用 GetCustomAttributes 会导致按上述方式构造并初始化一个 Author 对象。 如果该类具有其他特性,则按相似的方式构造其他特性对象。 然后 GetCustomAttributes 返回 Author 对象和数组中的任何其他特性对象。 之后就可以对此数组进行迭代,确定根据每个数组元素的类型所应用的特性,并从特性对象中提取信息。
示例
下面是一个完整的示例。 定义一个自定义特性,将其应用于若干实体并通过反射进行检索。
// Multiuse attribute. [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct, AllowMultiple = true) // Multiuse attribute. ] public class Author : System.Attribute { string name; public double version; public Author(string name) { this.name = name; // Default value. version = 1.0; } public string GetName() { return 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 { 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 {0}", t); // Using reflection. System.Attribute[] attrs = System.Attribute.GetCustomAttributes(t); // Reflection. // Displaying output. foreach (System.Attribute attr in attrs) { if (attr is Author) { Author a = (Author)attr; System.Console.WriteLine(" {0}, version {1:f}", a.GetName(), a.version); } } } } /* 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 */
如何:使用特性创建 C/C++ 联合
通过使用特性可以自定义结构在内存中的布局方式。 例如,可以使用 StructLayout(LayoutKind.Explicit) 和 FieldOffset 特性创建在 C/C++ 中称为联合的布局。
示例
在上一个代码段中,TestUnion 的所有字段都从内存中的同一位置开始。
[System.Runtime.InteropServices.StructLayout(LayoutKind.Explicit)] struct TestUnion { [System.Runtime.InteropServices.FieldOffset(0)] public int i; [System.Runtime.InteropServices.FieldOffset(0)] public double d; [System.Runtime.InteropServices.FieldOffset(0)] public char c; [System.Runtime.InteropServices.FieldOffset(0)] public byte b; }
以下是字段从其他显式设置的位置开始的另一个示例。
[System.Runtime.InteropServices.StructLayout(LayoutKind.Explicit)] struct TestExplicit { [System.Runtime.InteropServices.FieldOffset(0)] public long lg; [System.Runtime.InteropServices.FieldOffset(0)] public int i1; [System.Runtime.InteropServices.FieldOffset(4)] public int i2; [System.Runtime.InteropServices.FieldOffset(8)] public double d; [System.Runtime.InteropServices.FieldOffset(12)] public char c; [System.Runtime.InteropServices.FieldOffset(14)] public byte b; }
两个整数字段 i1 和 i2 共享与 lg 相同的内存位置。 使用平台调用时,这种结构布局控制很有用。
常用特性
全局特性
大多数特性适用于特定的语言元素,如类或方法;但是有些特性是全局的,它们适用于整个程序集或模块。 例如,AssemblyVersionAttribute 特性可用于向程序集中嵌入版本信息,如下例所示:
[assembly: AssemblyVersion("1.0.0.0")]
全局特性在源代码中出现在任何顶级 using 指令(在 Visual Basic 中为 Imports)之后、任何类型、模块或命名空间声明之前。 全局特性可显示在多个源文件中,但这些文件必须在单一编译传递中编译。 对于 Visual Basic 项目,全局特性通常放在随 Visual Basic 项目自动创建的 AssemblyInfo.vb 文件中。 在 C# 项目中,它们放在 AssemblyInfo.cs 文件中。
程序集特性是提供程序集相关信息的值。 它们分成以下类别:
-
程序集标识特性
-
信息性特性
-
程序集清单特性
-
强名称特性
程序集标识特性
三个特性(如果适用,还有强名称)可确定程序集的标识:名称、版本和区域性。 这些特性构成程序集的完整名称,在代码中引用程序集时需要这些特性。 可以使用特性来设置程序集的版本和区域性。 但是,名称值由编译器、“程序集信息”对话框中的 Visual Studio IDE 或程序集链接器 (Al.exe) 在创建程序集时根据包含程序集清单的文件进行设置。 AssemblyFlagsAttribute 特性指定程序集的多份副本是否可以共存。
下表显示标识特性:
特性 |
用途 |
---|---|
详细描述程序集的标识。 |
|
指定程序集的版本。 |
|
指定程序集支持哪个区域性。 |
|
指定程序集是否支持在同一台计算机上、同一进程中或同一应用程序域中并行执行。 |
信息性特性
您可以使用信息性特性为程序集提供其他的公司或产品信息。 下表显示在 System.Reflection 命名空间中定义的信息性特性。
特性 |
用途 |
---|---|
定义为程序集清单指定产品名称的自定义特性。 |
|
定义为程序集清单指定商标的自定义特性。 |
|
定义为程序集清单指定信息性版本的自定义特性。 |
|
定义为程序集清单指定公司名称的自定义特性。 |
|
定义为程序集清单指定版权的自定义特性。 |
|
指示编译器使用 Win32 文件版本资源的特定版本号。 |
|
指示程序集是否符合公共语言规范 (CLS)。 |
程序集清单特性
可以使用程序集清单特性提供程序集清单中的信息。 其中包括标题、说明、默认别名和配置。 下表显示在 System.Reflection 命名空间中定义的程序集清单特性。
特性 |
用途 |
---|---|
定义为程序集清单指定程序集标题的自定义特性。 |
|
定义为程序集清单指定程序集说明的自定义特性。 |
|
定义为程序集清单指定程序集配置(如发布或调试)的自定义特性。 |
|
为程序集清单定义友好默认别名 |
强名称特性
在 Visual Studio 的早期版本中,使用强名称对程序集进行签名是使用以下程序集级特性执行的:
现在仍支持这样做,但是给程序集签名的首选方法是使用项目设计器中的“签名页”。 有关更多信息,请参见“项目设计器”->“签名”页和如何:对程序集进行签名 (Visual Studio)。
已过时的特性
Obsolete 特性将某个程序实体标记为一个建议不再使用的实体。 每次使用被标记为已过时的实体时,随后将生成警告或错误,这取决于特性是如何配置的。 例如:
[System.Obsolete("use class B")] class A { public void Method() { } } class B { [System.Obsolete("use NewMethod", true)] public void OldMethod() { } public void NewMethod() { } }
在此例中,Obsolete 特性应用于类 A 和方法 B.OldMethod。 因为应用于 B.OldMethod 的特性构造函数的第二个参数设置为 true,所以此方法将导致编译器错误,而使用类 A 只会产生警告。 但是,调用 B.NewMethod 既不产生警告也不产生错误。
向特性构造函数提供的作为第一个参数的字符串将显示为警告或错误的一部分。 例如,当将它与前面的定义一起使用时,下面的代码将生成两个警告和一个错误:
// Generates 2 warnings: // A a = new A(); // Generate no errors or warnings: B b = new B(); b.NewMethod(); // Generates an error, terminating compilation: // b.OldMethod();
为类 A 产生两个警告:一个用于声明类引用,一个用于类构造函数。
可在不使用参数的情况下使用 Obsolete 特性,但要包括此项已过时的原因及改用什么项的建议。
Obsolete 特性是一种单用途特性,并且可应用于允许特性的任何实体。 Obsolete 是 ObsoleteAttribute 的别名。
条件特性
Conditional 特性根据预处理标识符执行方法。 Conditional 特性是 ConditionalAttribute 的别名,可应用于方法或特性类。
在本示例中,Conditional 应用于方法以启用或禁用程序特定的诊断信息的显示:
#define TRACE_ON using System; using System.Diagnostics; public class Trace { [Conditional("TRACE_ON")] public static void Msg(string msg) { Console.WriteLine(msg); } } public class ProgramClass { static void Main() { Trace.Msg("Now in Main..."); Console.WriteLine("Done."); } }
如果未定义 TRACE_ON 标识符,则将不会显示跟踪输出。
Conditional 特性经常与 DEBUG 标识符一起使用以启用调试版本的跟踪和日志记录功能(在发行版本中没有这两种功能),如下例所示:
[Conditional("DEBUG")] static void DebugMethod() { }
当调用标记为条件的方法时,指定的预处理符号的存在或不存在决定是否包含或省略此调用。 如果定义了该符号,则包含调用;否则省略调用。 使用 Conditional 是将方法包括在 #if…#endif 块内的替代方法,它更整洁、更别致,也减少了出错的机会,如下例所示:
#if DEBUG void ConditionalMethod() { } #endif
条件方法必须是类或结构声明中的方法,而且不能有返回值。
使用多个标识符
如果某个方法具有多个 Conditional 特性,且至少定义了其中一个条件符号(换言之,这些符号在逻辑上是使用 OR 运算符连接在一起的),则将包含对该方法的调用。 在本例中,A 或 B 的存在将导致方法调用:
[Conditional("A"), Conditional("B")] static void DoIfAorB() { // ... }
若要使用 AND 运算符获得在逻辑上链接符号的效果,可以定义序列条件方法。 例如,仅当 A 和 B 均已定义时,才能执行下面的第二种方法:
[Conditional("A")] static void DoIfA() { DoIfAandB(); } [Conditional("B")] static void DoIfAandB() { // Code to execute when both A and B are defined... }
使用具有特性类的条件
还可将 Conditional 特性应用于特性类定义。 在本例中,仅当定义了 DEBUG 时,自定义特性 Documentation 才向元数据添加信息。
[Conditional("DEBUG")] public class Documentation : System.Attribute { string text; public Documentation(string text) { this.text = text; } } class SampleClass { // This attribute will only be included if DEBUG is defined. [Documentation("This method displays an integer.")] static void DoWork(int i) { System.Console.WriteLine(i.ToString()); } }