技术经验解读:【CLRviaC#】18.Attribute

简介: 技术经验解读:【CLRviaC#】18.Attribute

  attribute可以说是Microsoft .NET Framework提出的最具创意的//代码效果参考:http://hnjlyzjd.com/xl/wz_24435.html

技术之一了。利用attribute,可以声明性的为自己的代码构造添加注解,从而实现一些特殊的功能。attribute允许将定义的信息应用于几乎每一个元数据表的记录项。这种可扩展的元数据信息能在运行时查询,从而动态改变代码的执行方式。

  一、使用attribute


  attribute可运用于类型和成员。Microsoft采取了一种机制提供对用户自定义的attribute的支持。这种机制叫做定制attribute。


  关于自定义attribute,首先应该知道:它们只是将一些附加信息与某个目标元素关联起来的方式。编译器会在托管模块中生成这些额外的信息。


  CLR允许将attribute应用于可在文件的元数据中表示的几乎所有元素。不过,最常应用attribute的还是以下定义表中的记录项:TypeDef(类、结构、枚举、接口和委托),MethodDef(含构造器)、ParamDef(方法参数)、FiledDef(字段)、PropertyDef(属性)、EventDef(事件),AssemblyDef(程序集)和MouduleDef(模块)。


  具体的说,在C#中只允许将attribute应用于对以下任何一个目标元素进行定义的源代码:程序集、模块、类型(类、结构、枚举、接口、委托)、字段、方法(含构造器)、方法参数、方法返回值、属性、事件和泛型类型参数。


  应用一个attribute时,C#允许用一个前缀明确指定attribute要应用于的目标元素。以下代码展示了所有可能的前缀。在许多情况下,即使省略前缀,编译器一样能判断一个attribute要应用于的目标元素。但在另一些情况下,必须指定前缀向编译器清楚表达我们的移除。下面倾斜显示的前缀是必须的。


【assembly: MyAttr(1)】 // 应用于程序集


【module: MyAttr(2)】 // 应用于模块


【type: MyAttr(3)】 // 应用于类型


internal sealed class SomeType<【typevar: MyAttr(4)】 T> { // 应用于泛型类型变量


【field: MyAttr(5)】 // 应用于字段


public Int32 SomeField = 0;


【return: MyAttr(6)】 // 应用于返回值


【method: MyAttr(7)】 // 应用于方法


public Int32 SomeMethod(


【param: MyAttr(8)】 // 应用于方法参数


Int32 SomeParam) { return SomeParam; }


【property: MyAttr(9)】 // 应用于属性


public String SomeProp {


【method: MyAttr(10)】 // 应用于get访问器方法


get { return null; }


}


【event: MyAttr(11)】 // 应用于事件


【field: MyAttr(12)】 // 应用于编译器生成的字段


【method: MyAttr(13)】 // 应用于编译器生成的add&&remove方法


public event EventHandler SomeEvent;


}


【AttributeUsage(AttributeTargets.All)】


public class MyAttr : Attribute {


public MyAttr(Int32 x) { }


}


  前面介绍了如何应用一个attribute,接下来看看attribute到底是什么?


  attribute实际是一个类的实例。为了符合"公共语言规范"(CLS)的要求,attribute类必须直接或间接地从公共抽象类System.Attribute派生。C#只允许使用符合CLS规范的attribute。attribute类是可以在任何命名空间中定义的。


  如前所述,attribute是类的一个实例。类必须有一个公共构造器,这样才能创建它的实例。所以,将一个attribute应用于一个目标元素时,语法类似于调用类的某个实例构造器。除此之外,语言可能支持一些特殊的语法,允许你设置于attribute类关联的公共字段或属性。比如,我们将DllImport这个attribute应用于GetVersionEx方法:


【DllImport("kernel32",CharSet = CharSet.Auto, SetLastError = true)】


  这一行代码语法表面上很奇怪,因为调用构造器时永远不会出现这样的语法。查阅DllImportAttbute类的文档,会发现它只接受一个String类型的参数。在这个例子中,"Kernel32"这个String类型的参数已经传给它了。构造器的参数称为"定位参数",而且是强制性的。也就是说,应用attribute时,必须指定参数。


  那么,另外两个"参数"是什么?这种特殊的语法允许在DllImportAttbute对象构造好之后,设置对象的任何公共字段和属性。在这个例子中,当DllImportAttbute对象构造好,而且将"Kernel32"传给构造器之后,对象的公共实例字段CharSet和SetListError被分别设置为CharSet.Auto和true。用于设置字段或属性的"参数"被称为"命名参数"。这种参数是可选的,因为在应用attribute的一个实例时,不一定要指定命名参数。


  另外,还可以将多个attribute应用于一个目标元素。将多个attribute应用一个目标元素时,attribute的顺序是无关紧要的。在C#中,可将每个attribute都封闭到一对方括号中,也可以在一对方括号中封闭多个以逗号分隔的attribute。


  二、定义自己的attibude类


  现在我们已经知道attribute是从System.Attribute派生的一个类的实例,并知道如何应用一个attribute。接着我们来研究下如何定制attribute。假定你是Microsoft的一位员工,并负责为枚举类型添加位标志(biit flag)支持。为此,我们,要做的第一件事情是定义一个FlagAttribute类:


namespace System {


public class FlagsAttribute : System.Attribute {


public FlagsAttribute(){


}


}


}


  注意,FlagsAttribute类是从Attribute继承的。所以,才使FlagsAttribute类成为符合CLS要求的一个attribute。除此之外,注意类名有一个Attribute后缀;这是为了保持于标准的相容性,但并不是必须的。最后,所有的非抽象attribute都至少要包括一个公共构造器。


  提示:attribute类型是一个类,但这个类应该非常简单。这个类应该只提供一个公共构造器,它接受attribute的强制性状态消息,而且这个类可以提供公共字段和属性,以接受attribute的可选状态信息。这个类不应提供任何公共方法、事件或其他成员。


  三、attribute的构造器和字段/属性的数据类型


  定义一个attribute类时,可定义构造器来获取参数。开发人员在应用该attribute类型的一个实例时,必须指定这些参数。除此之外,可在自己的类型中定义非静态公共字段和属性,使开发人员能够为attribute类的实例选择恰当的设置。


  定义attribute类的实例构造器、字段和属性时,数据类型只能限制在一个小的子集内。具体的说,合法的数据类型只有:Boolean,Char,Byte,Sbyte,Int16,UInt16,Int32,Uint32,Int64,Uint64,Single,Double,String,Type,Object或枚举类型。除此之外,还可使用上述任意类型的一维0基数组。然而,要尽量避免使用数组,因为对于attribute类来说,如果它的构造器要获取一个数组作为参数,就会失去与CLS的相容性。


  应用一个attribute时,必须传递一个编译时常量表达式,它与attribute类定义的类型相匹配。在attribute类定义一个Type参数,Type字段或者Type属性的任何地方,都必须使用C#的typeof操作符。在attribute类定义一个Object参数、Object字段或Object属性的任何地方,都可以传递一个Int32、String或者其他任何常量表达式(包括null)。如果常量表达式代表一个值类型,那么在运行时构造一个attribute的实例时,会对值类型进行装箱。以下是一个示例attribute及用法:


public enum Color { Red }


【AttributeUsage(AttributeTargets.All)】


internal sealed class SomeAttribute : Attribute


{


public SomeAttribute(String name, Object o, Type【】 types)


{


// 'name' 引用了一个String类型


// 'o' 引用了一个合法类型(如有必要,就进行装箱)


// 'types' 引用一个一维0基Type数组


}


}


【Some("Jeff", Color.Red, new Type【】 { typeof(Math), typeof(Console) })】


internal sealed class SomeType


{


}


  逻辑上,当编译器检测到一个目标元素应用了一个attribute时,编译器会调用attribute类的构造器,向它传递任何指定的参数,从而构造attribute类的一个实例。然后,编译器会采用增强型构造器语法所指定的值,对任何公共字段和属性进行初始化。在构造并初始化好定制attribute类的对象之后,编译器会将这个attribute对象序列化到目标元素的元数据表记录项中。


  提示:所谓"attribute",就是一个类的实例,它被序列化成驻留在元数据中的一个字节流。在运行时,可以对元数据中包含的字节进行反序列化,从而构造类的一个实例。实际发生的事情是:编译器在元数据中生成创建attribute类的一个实例所需的信息。每个构造器参数都采取这样的格式写入:一个1字节长度的类型ID,后跟具体的值。在对构造器参数进行"序列化"之后,编译器写入字段/属性名称,后跟一个1字节的类型ID,再跟上具体的值,从而生成指定的每个字段和属性的值。对于数组,会先保存数组元素的个数,后跟每个单独的元素。


  四、检测定制的attribute


  可利用反射的技术来检查attribute的存在。以后我们会完整探讨这种技术。


  假定你是Microsoft的员工,负责实现Enum的Format方法(Format方法会更根据是否有FlagsAttribute输出不同的值),你会像下面这样实现它:


public static String Format(Type enumType, Object value, String format) {


// 枚举类型是否应用了FlagsAttrobute类型的一个实例


if(enumType.IsDefined(typeof(FlagsAttribute),false)){


//如果是,就执行代码,将值视为一个位标志枚举类型


...


}else {


// 如果不是,就执行代码,将值视为一个普通枚举类型


}


}


  上述代码调用Type的IsDefined方法,要求系统查看枚举类型的元数据,检查是否关联了FlagsAttribute类的一个实例。如果IsDefined返回true,表面FlagsAttribute的一个实例已于枚举类型关联,Format方法会认为值包含了一个位标志集合。如果IsDefined放回false,Format方法会认为值是一个普通的枚举类型。


  定义定制attribute时,也必须实现一些代码来检查某个目标上是否存在该attribute类的实例,然后执行一些逻辑分支代码。正因为能做到这一点,定制attribute才如此有用。


  FCL提供了多种方式检查一个attribute的存在。我们知道所有于CLS相容的attribute都是从System.Attribute派生的。这个类定义了三个静态方法来获取与一个目标关联的attribute:IsDefined,GetCustomAttribute和GetCustomAttributes。每个方法都有几个重载版本。


方法名称


说明


IsDefined


如果至少有一个指定的Attribute派生类的实例如目标关联,就放回true。这个方法效率很高,因为它不构造(反序列化)attribute类的任何实例


GetCustomAttribute


返回引用于目标的指定attribute类的一个实例。实例使用编译时指定的参数、字段和属性来构造。如果目标没有引用任何attribute类的实例,就返回null。如果目标应用了指定attribute的多个实例,就抛出异常。方法通常用于已将AllowMultiple设为false的attribute。


GetCustomAttributes


返回一个数组,其中每个元素都是应用于目标的指定attribute类的一个实例。如果不为这个方法指定具体的attribute类,数组中包含的就是已应用的所有attribute的实例,不管它们是什么类。每个实例都使用编译时指定的参数、字段和属性来构造(反序列化)。如果目标没有应用任何attribute类的实例,就返回一个空数组。该方法通常用于已将AllowMultiple设为true的attribute,或者用于列出已应用的所有attribute。


  如果只想知道一个attribute是否应用于一个目标,那么应该调用IsDefined,因为它的效率比另外两个方法高的多。我们知道,将一个attribute应用于一个目标时,可以为attribute的构造器指定参数,并可以选择设置字段或属性。使用IsDefined不会构造一个attribute对象,不会调用它的构造器,也不会设置它的字段和属性。


  每次使用GetCustomAttribute和GetCustomAttributes方法时,都会为构造attribute对象的新实例,并根据源代码中指定的值来设置每个实例的字段和属性。这个两个方法返回的都是一个引用,执行完全构造好的attribute类的实例。


  调用上述任何一个方法时,它们内部必须扫描托管模块的元数据,执行字符串比较来定位指定的attribute类。显然,这些操作会耗费一定的事件。假如对性能要求高,可以考虑缓存这些方法调用的返回结果。


  System.Reflection 命名空间定义了几个类允许你检查一个模块的元数据的内容。这些类包括Assmbly,Module,ParameterInfo,MemberInfo,Type,MethodInfo,ConstructorInfo,FiledInfo,EeventInfo,PropertyInfo及其各自的Builder类。所以这些方法还提供了IsDefined和GetCustomAttributes方法。只有System.Attribute提供了非常方便的GetCustomAttribute方法。


  反射类提供的那个版本的GetCustomAttributes方法返回的是有Object实例构成的一个数据(Object【】),而不是由Attribute实例构成的一个数组(Attribute【】)。


  注意:将一个类传给IsDefined,GetCustomAttribute或GetCustomAttributes方法时,这些方法会搜索指定的attribute类或它的派生类,如果代码要搜索一个具体的attribute类,应该针对返回值执行一个额外的检查,确保这些方法返回的正是向搜索的莪累。还可以考虑将自己的attribute类定义成sealed,减少可能存在的混淆,并避免这个检查。


以下示例代码列出了一个类型中定义的所有方法,并显示应用于每个方法的attribute代码。


【assembly: CLSCompliant(true)】


【Serializable】


【DefaultMemberAttribute("Main")】


【DebuggerDisplayAttribute("Richter", Name = "Jeff", Target = typeof(Program))】


public sealed class Program


{


【Conditional("Debug")】


【Conditional("Release")】


public void DoSomething() { }


public Program()


{


}


【assembly: CLSCompliant(true)】


【STAThread】


public static void Main()


{


// 显示应用于这个类型的attribute集


ShowAttributes(typeof(Program));


// 获取与类型关联的方法集


MemberInfo【】 members = typeof(Program).FindMembers(


MemberTypes.Constructor | MemberTypes.Method,


BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.Static,


Type.FilterName, "");


foreach (MemberInfo member in members)


{


// 显示应用于这个成员的attribute集


ShowAttributes(member);


}


Console.Read();


}


private static void ShowAttributes(MemberInfo attributeTarget)


{


Attribute【】 attributes = Attribute.GetCustomAttributes(attributeTarget);


Console.WriteLine("Attributes applied to {0}: {1}",


attributeTarget.Name, (attributes.Length == 0 ? "None" : String.Empty));


foreach (Attribute attribute in attributes)


{


// 显示已应用的每个attribute的类型


Console.WriteLine(" {0}", attribute.GetType().ToString());


if (attribute is DefaultMemberAttribute)


Console.WriteLine(" MemberName={0}",


((DefaultMemberAttribute)attribute).MemberName);


if (attribute is ConditionalAttribute)


Console.WriteLine(" ConditionString={0}",


((ConditionalAttribute)attribute).ConditionString);


if (attribute is CLSCompliantAttribute)


Console.WriteLine(" IsCompliant={0}",


((CLSCompliantAttribute)attribute).IsCompliant);


DebuggerDisplayAttribute dda = attribute as DebuggerDisplayAttribute;


if (dda != null)


{


Console.WriteLine(" Value={0}, Name={1}, Target={2}",


dda.Value, dda.Name, dda.Target);


}


}


Console.WriteLine();


}


}


输出结果为:


Attributes applied to Program:


System.SerializableAttribute


System.Diagnostics.DebuggerDisplayAttribute


Value=Richter, Name=Jeff, Target=ConsoleTest.Program


System.Reflection.DefaultMemberAttribute


MemberName=Main


Attributes applied to DoSomething:


System.Diagnostics.ConditionalAttribute


ConditionString=Debug


System.Diagnostics.ConditionalAttribute


ConditionString=Release


Attributes applied to Main:


System.STAThreadAttribute


Attributes applied to .ctor: None


  五、两个attribute实例的相互匹配


  现在,我们的代码能判断是否将一个attribute的实例应用于一个目标了。除此之外,可能还需要检查attribute的字段来确定它们的值。为此,一个方法就是老老实实地写代码来检查attribute类的字段的值。System.Attribute重写了Object的Equals方法。这个方法内部会比较两个对象的类型。如果 不一致,Equals会返回false。如果类型一致,Equals会利用反射来比较两个attribute对象中的字段值(为每个字段调用Equals)。如果所有字段都匹配,就返回true;否则返回false。但我们可以在自己attribute类中重写Equals来移除反射的使用,从而提升性能。


  System.Attribute还公开了虚方法Match,可重写它来提供更丰富的语义。Match的默认实现只是调用的Equals方法并返回它的结果。下面演示了如何重写Equals和Match,后者在一个attribute代码另一个attribute的子集的前提下返回true。另外,还演示了如何使用Match。


【Flags】


internal enum Accounts


{


Savings = 0x0001,


Checking = 0x0002,


Brokerage = 0x0004


}


【AttributeUsage(AttributeTargets.Class)】


internal sealed class AccountsAttribute : Attribute


{


private Accounts m_accounts;


public AccountsAttribute(Accounts accounts)


{


m_accounts = accounts;


}


public override Boolean Match(Object obj)


{


// 如果基类实现了Match,而且基类


// 不是Attribute,就取消对下面这行的注释


//if (!base.Match(obj))


//{


// return false;


//}


if (obj == null)


{


return false;


}


if (this.GetType() != obj.GetType())


{


return false;


}


AccountsAttribute other = (AccountsAttribute)obj;


// 比较字段,判断它们是否有相同的值


if ((other.m_accounts & m_accounts) != m_accounts)


{


return false;


}


return true; // 对象匹配


}


public override Boolean Equals(Object obj)


{


//如果基类实现了Equals,而且基类不能Object


//就取消对下面的注释


//if (!base.Equals(obj))


//{


// return false;


//}


if (obj == null)


{


return false;


}


if (this.GetType() != obj.GetType())


{


return false;


}


AccountsAttribute other = (AccountsAttribute)obj;


// 比较字段,判断它们是否有相同的值


if (other.m_accounts != m_accounts)


{


return false;


}


return true; // Objects are equal


}


// 还需要重写GetHashCode,因为我们重写了Equals


public override Int32 GetHashCode()


{


return (Int32)m_accounts;


}


}


【Accounts(Accounts.Savings)】


internal sealed class ChildAccount { }


【Accounts(Accounts.Savings | Accounts.Checking | Accounts.Brokerage)】


internal sealed class AdultAccount { }


pu

相关文章
|
2月前
技术经验分享:comparisonmethodviolates必现
技术经验分享:comparisonmethodviolates必现
12 0
技术经验分享:comparisonmethodviolates必现
|
2月前
|
XML JSON 监控
技术经验分享:Axis2实践
技术经验分享:Axis2实践
15 0
|
3月前
|
存储 C++ 容器
c++的学习之路:14、list(1)
c++的学习之路:14、list(1)
30 0
|
3月前
|
C++
c++的学习之路:15、list(2)
c++的学习之路:15、list(2)
19 0
|
3月前
|
C++
c++的学习之路:16、list(3)
c++的学习之路:16、list(3)
22 0
|
3月前
|
机器学习/深度学习 语音技术
AIGC工具——Resemble.AI
【1月更文挑战第18天】AIGC工具——Resemble.AI
139 1
AIGC工具——Resemble.AI
|
SQL 缓存 Java
JPA学习之路(基础篇)
📒 程序员小王的博客:(https://www.wolai.com/wnaghengjie/ahNwvAUPG2Hb1Sy7Z8waaF) 🎉 欢迎点赞 👍 收藏 ⭐留言 📝 😊 如有编辑错误联系作者,如果有比较好的文章欢迎分享给我,我会取其精华去其糟粕 ———————————————— 版权声明:本文为CSDN博主「程序员小王java」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_44385486/article/details/123046499
147 0
JPA学习之路(基础篇)
|
关系型数据库 MySQL 应用服务中间件
Apache的安装与使用经验| 学习笔记
快速学习Apache的安装与使用经验。
93 0
|
SQL 开发框架 前端开发
C# BS方向 该如何规划学习?【学习路线指南】
C# BS方向 该如何规划学习?【学习路线指南】
377 0
C# BS方向 该如何规划学习?【学习路线指南】
|
消息中间件 Java 关系型数据库
根据自己的学习经验,推荐给大家一些Java方向的“技术提升秘籍”
  我根据自己过往的学习经验,及现在对于Java技术了解、掌握的情况,整理出这篇文章。看完之后,希望能帮你解决:
根据自己的学习经验,推荐给大家一些Java方向的“技术提升秘籍”