深入浅出Microsoft.Net定制特性

本文详细介绍了Microsoft.Net平台上的定制特性机制,允许开发者创建并应用自定义特性到类型和方法上。通过AttributeUsageAttribute定义特性应用的目标,讨论了AllowMultiple和Inherited参数的作用。此外,还展示了如何在运行时通过反射查询和使用自定义特性,包括查询程序集、类、方法及成员的特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Microsoft.Net平台上,publicprivatestatic等特性可以被应用于各种类型和成员。如果允许创建用户自定义的特性,并将它们应用于类型和方法上对于编程工作显然是一件非常美妙和方便的事情。微软推出的定制特性机制就是用来允许开发人员使用用户自定义的特性。该机制能够保证所有面向通用语言运行时(CLR)的编译器都能够识别定制特性,并将它们存放在生成的元数据中。在本文中,我将解析Microsoft.Net定制特性的相关知识。

一、特性的实质

.NET框架类库(FCL)提供了很多预定义特性。例如,System.FlagsAttribute特性允许我们将一个枚举类型看作一组标记,System.SerializableAttribute特性允许一个类型的字段被序列化和反序列化,几个安全相关的特性可以确保方法在试图进行某种特殊访问的时候具有要求的特权级别,许多互操作相关的特性可以帮助托管代码调用非托管代码,等等。
对于定制特性,我们首先需要知道的是它们仅仅是为目标元素提供关联附加信息的一种方式。编译器的工作只是将这些附加的信息存放在托管模块的元数据中而已。按理说CLR允许将特性应用于任何可以在一个文件的元数据中表示的元素,但是大多数语言只允许将它们应用到各种元数据定义表中的条目上,其中包括微软的C#
实际上,一个特性仅仅是一个类型的实例,添加一个特性就是构造一个特性类的对象实例。下面以常用的DllImport特性为例分析一下特性实例的构造:
[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]

每次使用该特性就相当于调用DllImportAttribute类型的公有构造器创建了一个实例。如果我们查看DllImportAttribute类型的文档,我们会发现它的构造器只要求一个String参数。例子中,"Kernel32"被作为这样的参数传递进去。一个特性类型的构造器参数被称为定位参数,该参数是强制性的,也就是说在应用特性时必须制定这样的参数。那么其他两个参数呢?特殊的语法允许我们在对象构造之后设置它的公有字段或属性。因此,当DllImportAttribute对象被构造时,实际上发射个三件事情:"Kernel32"被传入DllImportAttribute构造器,CharSet被设为CharSet.AutoSetLastError被设为true。设置字段或属性的参数被称作命名参数,它们往往是可选的,并且必须有缺省值。
依据通用语言规范(CLS),定制特性的类型必须直接或间接继承自System.Attribute,并且所有特性类型的名称都应该有一个“Attribute”后缀。另为所有的非抽象特性都必须具有public访问限制,并且至少包含一个公有构造器。下面是FCLAttributeUsageAttribute类型的实现源码: 
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[Serializable]
public sealed class AttributeUsageAttribute : Attribute
{
    
internal AttributeTargets m_attributeTarget = AttributeTargets.All;
    
internal Boolean m_allowMultiple = false;
    
internal Boolean m_inherited = true;

    
public AttributeUsageAttribute(AttributeTargets validOn)
    {
        m_attributeTarget 
= validOn;
    }

    
public AttribteTargets ValidOn
    {
        
get { return m_attributeTarget; }
    }

    
public Boolean AllowMultiple
    {
        
get { return m_allowMultiple; }
        
set { m_allowMultiple = value; }
    }

    
public Boolean Inherited
    {
        
get { return m_inherited; }
        
set { m_inherited = value; }
    }
}

AttributeUsageAttribute类型的构造函数需要传入一个AttributeTargets类型的定位参数来表示该特性可以应用的目标对象。两个命名参数AllowMultiple和Inherited,分别标示该特性是否可以被多次应用于同一个目标元素上和该特性是否可以被继承应用于派生类或派生方法上。 

二、反射技术

我们可以把定制特性看作是一些被序列化为字节流的类型实例,这些字节流在编译时被存储在生成模块的元数据中(这只会导致额外的元数据被写入到托管模块中——应用程序代码的行为不会因此有任何改变)。到了运行时,CLR便通过对元数据中的字节流执行反序列化操作该构造特性类型的实例。构造出来的特性实例,怎样才能影响到添加了该特性的类的实际使用呢?程序又是如何获悉被检测目标元素上应用了那些特性呢?为了解答这些问题,必须首先弄明白.NET的反射(reflection)技术原理。
我们知道,元数据本质上就是一大堆的表。当我们生成一个程序集或者模块时,编译器会创建一个类型定义表、一个字段定义表、一个方法定义表、等等。FCL中的System.Type类以及System.Reflection命名空间包含的一些类型,允许我们编写代码来反射(或者说分析)这些元数据表。通过这些类的帮助我们可以分析相关的元数据表来查询一个类型的字段、方法、属性、以及事件。当然,我们还可以查找应用在任何元数据实体上的任何定制特性。
下面的示例程序演示了使用反射来访问并调用类型的各种成员(包括字段、构造器、方法和属性)。
using System;
using System.Reflection;
using System.Threading;

class SomeType{
    Int32 someField;
    
public SomeType(ref Int32 x){
        x 
*= 2;
    }
    
public override string ToString()
    {
        
return someField.ToString();
    }
    
public Int32 SomeProp{
        
get{
            
return someField;
        }
        
set{
            
if (value < 1)
                
throw new ArgumentOutOfRangeException("value", value, "value must be > 0");
            someField 
= value;
        }
    }
    
public event ThreadStart SomeEvent;
    
private void  NoCompilerWarnings(){
        SomeEvent.ToString();
    }
}

class App{
    
static void Main(){
        Type t 
= typeof(SomeType);
        BindingFlags bf 
= BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;

        
//构造一个对象实例
        Object[] args = new Object[]{12};
        Console.WriteLine(
"x before constructor called: " + args[0]);
        Object obj 
= t.InvokeMember(null, bf | BindingFlags.CreateInstance, nullnull, args);
        Console.WriteLine(
"Type: " + obj.GetType().ToString());
        Console.WriteLine(
"x after constructor returns: " + args[0]);

        
//读取或者设置一个字段
        t.InvokeMember("someField", bf | BindingFlags.SetField, null, obj, new Object[] { 5 });
        Int32 v 
= (Int32)t.InvokeMember("someField", bf | BindingFlags.GetField, null, obj, null);
        Console.WriteLine(
"someField: " + v);

        
//调用一个方法
        String s = (String)t.InvokeMember("ToString", bf | BindingFlags.InvokeMethod, null, obj, null);
        Console.WriteLine(
"ToString: " + s);

        
//读取或者设置一个属性
        try{
            t.InvokeMember(
"SomeProp", bf | BindingFlags.SetProperty, null, obj, new Object[] { 0 });
        }
        
catch(TargetInvocationException e){
            
if (e.InnerException.GetType() != typeof(ArgumentOutOfRangeException)) throw;
            Console.WriteLine(
"Property set catch.");
        }

        t.InvokeMember(
"SomeProp", bf | BindingFlags.SetProperty, null, obj, new Object[] { 2 });
        v 
= (Int32)t.InvokeMember("SomeProp", bf | BindingFlags.GetProperty, null, obj, null);
        Console.WriteLine(
"SomeProp: " + v);
    }
}
编译并运行上面的代码,我们将会得到以下输出:
x before constructor called: 12
Type: SomeType
x after constructor returns: 24
someField: 5
ToString: 5
Property set catch.
SomeProp: 2

三、开发自定义Attribute

有了以上的技术储备,我们就可以定义并使用自己的特性了。

using System;  

public class HelpAttribute : Attribute  
{  

不管你是否相信我,就这样我们就已经创建了一个自定义attribute。现在就可以用它来装饰我们的类了。
[Help()]
public class AnyClass  
{  

注意:按惯例我们是用”Attribute“作为attribute类名的后缀,然而,当我们当我们把attribute绑定到某语言元素时,是不包含“Attribute“后缀的。编译器首先在System.Attribute 的继承类中查找该attribute,如果没有找到,编译器会把“Attribute“追加到该attribute的名字后面,然后查找它。 
但是迄今为止,该attribute没有任何用处。为了使它有点用处,让我们在它里面加点东西吧。 

using System; 
using System.Reflection; 
using System.Diagnostics; 

//attaching Help attribute to entire assembly 
[assembly : Help("This Assembly demonstrates custom attributes creation and their run-time query.")]   

//our custom attribute class 
public class HelpAttribute : Attribute 

    
public HelpAttribute(String Description_in) 
    { 
       
// TODO: Add constructor logic here 
        this.description = Description_in; 
    } 

    
protected String description; 

    
public String Description 
    { 
        
get  
        { 
            
return this.deescription;   
        }  
    }   


//attaching Help attribute to our AnyClass 
[Help("This is a do-nothing Class.")] 
public class AnyClass 

    
//attaching Help attribute to our AnyMethod 
    [Help("This is a do-nothing Method.")] 
    
public void AnyMethod() 
    { 
    } 

    
//attaching Help attribute to our AnyInt Field 
    [Help("This is any Integer.")] 
    
public int AnyInt; 


class QueryApp 

    
public static void Main() 
    { 
    } 
}

在上面的例子中,我们在HelpAttribute类中添加了一个属性,并把它应用到多种元数据对象上。

接下来,我们将在运行时查询该属性。

1)查询程序集的Attributes

在下面的代码中,我们先得到当前的进程名称,然后用Assembly类中的LoadForm()方法加载程序集,再有用GetCustomAttributes()方法得到被绑定至当前程序集的自定义attributes,接下来用foreach语句遍历所有attributes并试图把每个attribute转型为Help attribute(即将转型的对象使用as关键字有一个优点,就是当转型不合法时,我们将不需担心会抛出异常,代之以空值(null)作为结果),接下来的一行就是检查转型是否有效,及是不是为空,跟着就显示Help attribute“Description”属性。

 

class QueryApp
{
    public static void Main()
    {
        HelpAttribute HelpAttr;

        // Get the class type to access its metadata.
        Type clsType = typeof(QueryApp);
        // Get the assembly object.
        Assembly assembly = clsType.Assembly;
        // Store the assembly's name.
        String assemblyName = assembly.GetName().Name;        

        //HelpAttribute hAttr = (HelpAttribute)Attribute.GetCustomAttribute(assembly, typeof(HelpAttribute));
        //Console.WriteLine("Description of {0}: {1}\n", assemblyName, hAttr.Description);

        var attributes = assembly.GetCustomAttributes(false);
        foreach (Attribute attr in attributes)
        {            
            HelpAttr = attr as HelpAttribute;
            if (null != HelpAttr)
            {
                Console.WriteLine("Description of {0}: {1}\n", assemblyName, HelpAttr.Description);               
            }
            else
            {
                //Console.WriteLine("Attribute : {0}\n", attr.ToString());
            }
        }
    }
}

程序输出如下:

Description of QueryAttribute.exe:

This Assembly demonstrates custom attributes creation and their run-time query.

2)查询类、方法、类成员的Attributes

下面的代码中,用typeof操作符得到了一个与我们AnyClass类相关联的Type型对象。首先我们得到所有在类中存在的方法和成员,然后我们查询与它们相关的所有attributes,这就跟我们查询类attributes一样的方式。

class QueryApp  
{  
    
public static void Main()  
    {  
        Type type 
= typeof(AnyClass);  
        HelpAttribute HelpAttr;  

        
//Querying Class Attributes  
        foreach (Attribute attr in type.GetCustomAttributes(true))  
        {  
            HelpAttr 
= attr as HelpAttribute;  
            
if (null != HelpAttr)  
            {  
                Console.WriteLine(
"Description of AnyClass: {0}",  HelpAttr.Description);  
            }  
        }  

        
//Querying Class-Method Attributes    
        foreach(MethodInfo method in type.GetMethods())  
        {  
            
foreach (Attribute attr in method.GetCustomAttributes(true))  
            {  
                HelpAttr 
= attr as HelpAttribute;  
                
if (null != HelpAttr)  
                {  
                    Console.WriteLine(
"Description of {0}: {1}",   method.Name,   HelpAttr.Description);  
                }  
            }  
        }  

        
//Querying Class-Field (only public) Attributes  
        foreach(FieldInfo field in type.GetFields())  
        {  
            
foreach (Attribute attr in field.GetCustomAttributes(true))  
            {  
                HelpAttr
= attr as HelpAttribute;  
                
if (null != HelpAttr)  
                {  
                    Console.WriteLine(
"Description of {0}: {1}",  field.Name, HelpAttr.Description);  
                }  
            }  
        }  
    }  

程序输出如下:

Description of AnyClass:

This is a do-nothing Class.

Description of AnyMethod:

This is a do-nothing Method.

Description of AnyInt:

This is any Integer.

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值