上篇博文[C#]Attribute特性介绍了特性的定义,类的特性,字段的特性,这篇博文将介绍方法的特性及特性参数相关概念。
3.方法的特性
之所以将这部分单列出来进行讨论,是因为对方法的特性查询的反射代码不同于对类的特性查询的反射代码。在这个例子里,我们将使用一个特性用来定义一种可进行事务处理的方法。
public class TransactionableAttribute : Attribute
{
public TransactionableAttribute() { }
}
public class TestClass
{
[Transactionable]
public void Foo()
{ }
public void Bar()
{ }
[Transactionable]
public void Baz()
{ }
}
class Program
{
static void Main(string[] args)
{
Type type = typeof(TestClass);
foreach (MethodInfo method in type.GetMethods())
{
foreach (Attribute attr in method.GetCustomAttributes())
{
if (attr is TransactionableAttribute)
{
Console.WriteLine("{0} is transactionable.", method.Name);
}
}
}
Console.Read();
}
}
代码输出结果为:
在这个特殊的例子,仅仅凭借TransactionableAttribute就可以让代码知道具有这种特性的方法可以参与事务处理。这也是为什么在这里只有一个简单的、不带参数的构造函数,而没有其他成员。然后TestClass定义三种方法:Foo方法、Bar方法和Baz方法;其中Foo方法和Baz方法方法被定义为具有处理事务能力的方法。请注意您附加一个带构造函数的特性时,如果这个构造函数不带参数,您就不需要把左括号和右括号包括进去了。
现在让我们看看这个程序当中指的关注的部分,看看怎样通过方法的特性来查询类的方法。我们开始先用typeof来获得TestClass类的System.Type对象。
Type type = typeof(TestClass);
然后我们使用Type.GetMethods方法来得到一个MethodInfo对象数组。每一个这样的对象包括TestClass类的一个方法信息。我们用foreach语句来循环处理我们的每一个方法。
foreach (MethodInfo method in type.GetMethods())
现在我们有一个MethodInfo对象,我们就可以使用MethodInfo.GetCustomAttributes()方法来得到所有的用户创建的方法特性。我们还是使用foreach语句来循环处理返回的对象数组。
foreach (Attribute attr in method.GetCustomAttributes())
在代码的这个地方,我们的方法有了特性。现在,通过使用is操作符,我们来判断一个特性是不是一个TransactionableAttribute,如果他是,就打印出这个方法的名字。
if (attr is TransactionableAttribute)
{
Console.WriteLine("{0} is transactionable.", method.Name);
}
特性的参数
在上面的例子中,通过构造函数我们讨论了附加特性的使用。现在我们要来看看在前面没有谈到的特性的构造函数的一些方面。
定位参数和命名参数
在上一篇博文字段的特性的例子中,您看到一个名为RefistryKeyAttribute的特性。它的构造函数形式如下:
public RegistryKeyAttribute(RegistryHives Hive, string valueName)
在这个构造函数声明之后,通过如下这种形式特性就附加给了一个字段:
[RegistryKey(RegistryHives. HKEY_CURRENT_USER,"Foo")]
public string Foo;
到此为止,这些都很容易理解。这个构造函数有两个参数,这两个参数都是在把一个特性附加给一个字段时用到的。不过,我们可以让这种编程更简单。如果这个参数大多数时候都不变,那为什么每次都要让使用这个类的用户再费劲地输入这些参数呢?我们可以使用定位参数(position parameter)和命名参数来给这些参数设置默认值。
定位参数是用在构造函数中的参数。在每次使用特性时它们是必须的参数,并且要必须指明这些参数。在上面的RegistryKeyAttribute例子中,Hive和ValueName都是定位参数。命名参数在特性的构造函数中实际上并没有定义,更确切地说,它们是非静态的字段和属性。因此,在一个特性被实例化时,命名参数让客户端能够设置这个特性的字段和属性,而不必让您为客户端要设置的每一种字段和属性的可能的组合而创建构造函数。
每一个公共的构造函数都可以定义一系列的定位参数,就像所有类型的类一样。但是,对于特性来说,一旦它的定位参数被确定,用户就可以使用FieldOrPropertyName=Value来对某个字段或属性进行引用。下面我们通过对特性RegistryKeyAttribute的修改来解释一下这种情况。在这个例子里,我们取RegistryKeyAttribute.ValueName作为一个定位参数,而RegistryKeyAttribute.Hive就成了可选的命名参数。接下来,问题就是“您怎样才能把一些参数定义为命名参数?”因为只有定位参数——即必需性的——参数才包括在构造函数的定义中,因此我们只需简单地把这个可选参数从构造函数的定义中删除即可。然后用户就可以引用如下部分作为命名参数:非只读、静态或常量的任何字段,或包括设置存取器方法或非静态的setter的任何属性。因此,为了使RegistryKeyAttribute.Hive成为一个命名参数,我们要把它从构造函数的定义中删除,因为它作为一个公共的读/写属性已经存在了。
public RegistryKeyAttribute(string valueName)
用户现在就可以用下面的任一种方法来附加特性了:
[RegistryKey(“Foo”)]
[RegistryKey(“Foo”,Hive=RegistryHives.HKEY_LOCAL_MACHINE)]
采用这种方式具有很好的灵活性,您既可以使用字段的默认值,同时,又可以让用户能够在需要的时候用其他值覆盖原来的默认值。但是要记住:如果用户没有设置RegistryKeyAttribute.Hive字段的值,我们怎样来默认它?您也许会想到,“哦,我们来检查一下看它是不是在构造函数中设置了。”但是,问题是RegistryKeyAttribute.Hive是一个enum型的,它的底层数据类型是int型——它是一个数值。这就意味着在定义时编译器已经把它初始化为0了!如果我们测试一下构造函数中RegistryKeyAttribute.Hive的值就会发现它等于0,我们不知道,是由调用程序通过命名参数设置的那个值,还是由编译器在编译时,因为它是一个数值型才给它设置了该值。不幸的是,现在能解决这个问题的唯一的途径是改变这段代码,让它的值为0时无效。这可以通过如下改变RegistryHives enum的方式实现:
public enum RegistryHives
{
HKEY_CLASSES_ROOT=,
HKEY_CURRENT_USER,
HKEY_LOCAL_MACHINE,
HKEY_USERS,
HKEY_CURRENT_CONFIG
}
现在我们知道,使RegistryKeyAttribute.Hive为0的唯一途径是编译器把它初始化为0,并且用户没有通过一个命名参数来覆盖它的初值。我们可以用类似下面的代码来将其初始化:
public RegistryKeyAttribute(string valueName)
{
this.valueName = valueName;
if (this.Hive == )
{
this.hive = RegistryHives.HKEY_CURRENT_USER;
}
}
使用命名参数时的常见错误
当您使用命名参数时,您必须首先要指定定位参数。之后,由于命名参数是放在字段名或属性之前的,因此命名参数之间没有先后顺序。下面的例子将导致一个编译错误。
//This is an error because postional parameters can't follow
//named parameters
[RegistryKey(Hive=RegistryHives.HKEY_LOCAL_MACHINE, "Foo")]
另外,您不能命名定位参数。编译器在编译特性的使用时,它会试着先去解析那些命名参数。然后再试着根据方法特性去解析剩下的——定位参数值。本段下面的代码无法通过编译,因为在要解析的参数当中必须半酣至少一个定位参数,如果全部都是命名参数,就会出现提示“No overload for method 'Registrykey Attribute 'takes ' O' arguments”(RegistrykeyAttribute方法没有参数值,无法进行重载)。
[RegistryKey(ValueName="Foo",Hive=RegistryHives.HKEY_LOCAL_MACHINE)]
最后,命名参数可以是任何公共的、可存取的字段或属性——包括setter方法——只要不是静态的或常量即可。
有效的特性参数类型
特性类的定位参数和命名参数的类型仅限于特性参数类型,这些包括:
- bool,byte,char,double,float,int,long,short,string
- System.Type
- object
- enum类型,前提是它或任何有它嵌套在里面的类型必须是公共的可存取类型——就像在那个使用RegistryHives枚举的例子中一样。
- 由上述的任何类型组成的一维数组。
因为有效的参数类型仅局限于上述列出来的类型,因此您不能把一个像类那样的数据结构传递给特性构造函数作为参数。这种限制很有意义,因为特性是在程序设计时附加上的,此时您并没有这个类(对象)的实例化的实例。使用上面列出来的这些有效类型,您就可以在程序设计时把他们的值固定下来,就是为什么能使用他们的原因。
结语
方法的特性和特性参数就介绍到这里,您如果想了解更多请参考《c#技术内幕》这本书,本文也是摘自这本书,记录在此,方便回顾,也分享给大家,希望能对您有所帮助。下篇将学习AttributeUsage特性和特性标识符。敬请期待......