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






4.97/5 (35投票s)
我系列讲座的第 6 讲。本讲座是关于特性的。
全部课程集
- C#Lectures - 第 1 讲:
原始类型 - C# 课程 - 第2课:在 C# 中处理文本:char, string, StringBuilder, SecureString
- C# 课程 - 第3课:C# 类型设计。你必须知道的类基础知识
- C# 课程 - 第4课:面向对象编程基础:C# 示例中的抽象、封装、继承、多态
- C# 课程 - 第5课:C# 示例中的事件、委托、委托链
- C# 课程 - 第6课:C# 中的特性和自定义特性
- C# 讲座 - 第 7 讲:
反射( 通过 C# 示例) - C# 课程 - 第8课:灾难恢复。C# 示例中的异常和错误处理
- C# 讲座 - 第 9 讲:
Lambda 表达式 - C# 课程 - 第10课:LINQ 简介,LINQ to Objects 第一部分
引言
我的系列讲座的第 6 篇关于一种名为自定义特性的 .NET 技术。自定义特性使您能够向源代码添加更多内容,并为程序集元数据表中的几乎所有记录添加更多信息。我们可以在运行时处理这些元数据,它可能会影响应用程序的流程。WPF、WCF 等最著名的 .NET 技术大量使用特性。每个 .NET 工程师都应该熟悉这项技术,以便在 .NET 和 C# 的工作中效率更高。
特性领域
特性指定有关程序中声明的实体的声明性信息。每个人都知道方法或成员修饰符,例如:private
、public
、protected
和 internal
。这些是特性,标准特性。除了标准特性之外,C# 还赋予程序员创建自己的特性的能力。例如,您可以在类上添加 ModelAttribute
特性,并在其中描述类在软件模型和设计中的呈现方式。之后,您可以生成一些包含 ModelAttribute
的模型文档,以查看模型是否由类和结构正确表示。特性信息可以在运行时检索,这对于特定场景非常有用。我们应该知道的关于特性的主要内容是它是元数据。大多数特性对编译器没有任何意义,编译器会忽略它们,只将它们存储到程序集的元数据中。.NET 库中有数百个特性可供您在代码中使用。举几个例子:DllImport
、Conditional
、Obsolete
、Serializable
等,这些是在几乎每个项目中都会使用的特性。
特性内部是什么
您应该知道和记住的第一件事是,特性是特定类型的一个对象。该类型应直接或间接派生自 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
的事实,让我们一步一步创建自己的特性
您可以分析以下内容:Assembly
、Module
、ParameterInfo
、MemberInfo
、Type
、MethodInfo
、ConstructorInfo
、FieldInfo
、EventInfo
、PropertyInfo
以及与之相关的 *Builder
。所有这些都具有 IsDefined
和 GetCustomAttributes
。
- 首先,我们需要定义一个派生自
System.Attribute
的public
类。在我的例子中,这个类名是MyOwnExcitingAttribute
。虽然没有强制要求,但微软建议在您的特性类型名称中使用 Attribute 后缀。 - 其次,任何
Attribute
类都必须至少有一个公共构造函数。通过向构造函数添加参数,您可以指定使用您的特性的开发人员需要提供的内容。除此之外,您可以定义非静态的公共字段,开发人员可以选择定义或不定义。 - 当您定义自己的
Attribute
类时,您可以使用AttributeUsageAttribute
来定义您特性的使用范围,它将仅应用于特定的类型或成员。除此之外,您还可以使用AttributeUsageAttribute
配置您的自定义特性的其他行为- 如果您在
AttributeUsageAttribute
中使用Inherited=true
属性,则特性可以被继承。 - 大多数特性不能应用于同一个元素多次,实际上也没有理由这样做。如果您想将同一个特性应用于实体多次,请使用命名参数
AllowMultiple
并将其值设置为true
。
- 如果您在
- 定义特性本身并将其应用于某些成员是没有用的。最终您在程序集中得到的是额外的元数据,对应用程序的工作流程没有影响。这只是部分正确。运行时代码通过一个称为反射的机制进行分析。本文不深入探讨反射,仅回顾其用法。当您编写的代码根据特定特性的使用而表现不同时,您应该自己检查这些特性。有几种方法可以检查特性是否已定义。我们将回顾其中的两种
- 您可以调用
Type
类型的IsDefined
方法来检查是否有某个特性与type
相关联。如果IsDefined
方法返回true
,则表示type
与请求的特性相关联if (user.GetType().IsDefined(typeof(MyOwnExcitingAttribute), false))
这是我示例中的代码。您可以在随附的源代码存档中看到整个代码。请注意,这种检查特性是否已定义的方法仅适用于类型。
- 当我们需要检查特性是否应用于方法、程序集或模块时,我们需要使用不同于上述描述的方法。为此,我们可以使用
System.Reflection.CustomAttributeExtensions
类。该类包含用于获取特性的static
方法。总的来说,该类具有以下三个方法,它们有许多重载GetCustomAttribute
- 如果特性应用于特定模块,则返回请求特性的实例。此实例包含在编译前定义的字段和值。如果未应用此类特性,则返回null
。GetCustomAttributes
- 返回特定类型特性的实例数组。与上一个类似,每个成员都有在编译前分配的参数。如果未找到任何内容,则返回空集合。IsDefined
- 如果应用了请求类型的某些特性到特定元素,则返回true
。这是一个非常快速的方法,因为它不会重新序列化任何内容,只是检查特性是否已应用。
- 您可以调用
- Attribute 类建议
- 尝试使用一个构造函数
- 尽量避免公共成员,而是使用属性
- 尝试为特性成员和字段使用简单类型,不要使用复杂类型或多维数组(您可以在我的文章 这里 中阅读更多关于基本类型的信息)。这是一个建议,您可以在您的
attribute
类中使用更复杂的成员,但不能保证它将是 CLS 兼容的。 - 尽量不要在
Attribute
类中有方法 - 这个类的想法是保持简单并保存一些状态,尽量避免在Attribute
类中构建复杂的逻辑。 - 将特性定义为密封。一旦您将类放入
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
,表示枚举可以被视为位字段
您可以在 这里 找到标准特性的完整列表。
来源
- Jeffrey Richter - CLR via C#
- Andrew Troelsen - Pro C# 5.0 and the .NET 4.5 Framework
- https://msdn.microsoft.com
- https://tutorialspoint.org.cn/csharp/csharp_attributes.htm