C#基础知识梳理系列八:定制特性Attribute

 
摘 要

设计类型的时候可以使用各种成员来描述该类型的信息,但有时候我们可能不太愿意将一些附加信息放到类的内部,因为这样,可能会给类型本身的信息描述带来麻烦或误解。我们想为类型、属性、方法及返回值附加额外的信息,这些附加信息可以更明确的表达类及其对象成员的状态,怎么办?定制特性Attribute可以做到。

为了避免Attribute与Property翻译性误解,我们以下的讨论中将以特性表示Attribute。

细心的读者可能会发现如下类似定义:

//项目的AssemblyInfo.cs文件内有:
[assembly: Guid("df510f85-e549-4999-864d-bb892545690b")]
[assembly: AssemblyVersion("1.0.0.0")]
//也可能会发现有些类前面也有类似的语句:
[Serializable, ComVisible(true)]
public sealed class String
{}
//在我们开发WCF项目时,定义接口契约时接口前面也有类似的语句:
[ServiceContract]
public interface IService
{}

这些放在方括弧[]中的Serializable、ServiceContract就是.NET Framework提供的特性Attribute。它们有的作用于程序集,有的作用于类和接口,也有的作用于属性和方法等其他成员。

第一节 定制特性Attribute

特性Attribute是指给声明性对象附加一些声明性描述信息,这些信息在经过编译器编译后相当于目标对象的自描述信息被编译进托管模块的元数据中,很显然,如果这些描述信息太多,会大大增加元数据的体积,这一点要注意。编译器只是将这些描述信息编译生成到元数据中,而对Attribute的“逻辑”并不关注。

前面提到的AssemblyVersion 、Serializable、ServiceContrac等都是继承于System.Attribute类,CLS要求定制Attribute必须继承于System.Attribute类,为了符合规范,所有的定制特性都要以Attribute后缀,这只是一个规范,也可以不使用此后缀,并没有强制。即使采用了后缀,为了方便编码,C#编译器也是允许在编码时省略后缀的,而VS智能提示对此也有很好的支持。

如下我们定义了一个有关国家的定制特性:

    public class CountryAttribute : Attribute
{
public CountryAttribute(string name)
{
this.Name = name;
}
public int PlayerCount { get; set; }
public string Name { get; set; }
}

来看一下编译干了什么:

可以看到,定制特性就是一个普通的类,继承了System.Attribute类,没有什么特殊的地方。非抽象的定制特性类必须有至少一个公共构造函数,因为在将一个定制特性应用于其他目标元素时是以定制特性的实例起作用的。定制特性应该注意以下几点:

(1) 可以在定制特性内提供公共字段和属性,但不应该提供任何公共方法、事件等成员,像上面代码中的属性Name和PlayerCount都是被允许的。

(2) 公共实例构造函数可以有参数,也可以无参数,也可以同时提供多个构造函数。如上面的CountryAttribute类可以增加一个无参的构造函数:

        public CountryAttribute()
{ }

(3)定义Attribute类的实例构造函数的参数、字段和属性时,只能使用以下数据类型:object,type,string,Boolean,char,byte,sbyte,Int16,int,UInt16,UInt32,Int64,UInt64,Single,double,枚举和一维0基数组。

前面的定制特性CountryAttribute可以应用于任何目标元素?如果我们希望它只应用于类类型或方法时怎么办呢?.NET Framework当然提供了这一方面的支持:System. AttributeUsageAttribute类。AttributeUsageAttribute是.NET Framework提供的一个定制特性,它主要是作用于其他定制特性来限制目标定制特性的作用目标。看一下其定义:

    public sealed class AttributeUsageAttribute : Attribute
{
public AttributeUsageAttribute(AttributeTargets validOn);
public bool AllowMultiple { get; set; }
public bool Inherited { get; set; }
public AttributeTargets ValidOn { get; }
}

(1)该类只提供了一个公共实例构造器,其接收的参数validOn是枚举类型AttributeTargets,它指定了定制特性的作用范围,比如:程序集、模块、结构、类等。

    public enum AttributeTargets
{
Assembly = 1,
Module = 2,
Class = 4,
Struct = 8,
Enum = 16,
Constructor = 32,
Method = 64,
Property = 128,
Field = 256,
Event = 512,
Interface = 1024,
Parameter = 2048,
Delegate = 4096,
ReturnValue = 8192,
GenericParameter = 16384,
All = 32767,
}

(2)AttributeUsageAttribute有一个附加属性AllowMultiple,它表示是否允许将定制特性的实例多次应用于同一个目标元素。

(3)AttributeUsageAttribute还有一个附加属性Inherited,它表示定制特性应用于基类时,是否将该特性同时应用于派生类及重写的的成员。

我们对CountryAttribute类进行了改造,同时定义了两个类使用定制特性CountryAttribute,如下代码:

 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.ReturnValue | AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class CountryAttribute : Attribute
{
public CountryAttribute()
{ }
public CountryAttribute(string name)
{
this.Name = name;
}
public int PlayerCount { get; set; }
public string Name { get; set; }
}
[Country("China")]
[Country("America")]
public class Sportsman
{
public string Name { get; set; }
[Country(PlayerCount = 5)]
public virtual void Play()
{ }
}
public class Hoopster : Sportsman
{
public override void Play()
{ }
}

我们将CountryAttibute特性限定只能用于类、方法、方法返回值和属性:

AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.ReturnValue | AttributeTargets.Property

并且允许该定制特性的实例多次应用于同一个目标元素:

AllowMultiple = true

同时还要求将该特性不仅用于基类,也应用于派生类及重写成员:

Inherited = true

在Sportsman类我们应用了两次定制特性 [Country("China")]和[Country("America")]。对基类Sportsman及其成员方法Play()我们使用了定制特性,在其派生类Hoopster同样可以得到这些定制特性,下面讨论中我们将验证这一点。

细心的你可能会发现[Country("China")]和[Country(PlayerCount = 5)]有点相似但又有些不同,为什么?

(1) 我们在定义定制特性的时候是CountryAttribute,这里可以简写为Country。在将Country应用于某目标元素时,编译器进行编译时已经确认它是一个定制特性,接着它会在Attribute继承类中查找Country,如果没找到,则会加上Attribute后缀继续查找,再找不到,就会“啪”的一声报错了!

(2) Country("China")是在为实例构造器传递参数”China”,这没什么好解释的,问题是[Country(PlayerCount = 5)],我们并没有为County的构造函数设置参数PlayerCount啊。先来看一下在VS中编写代码时的智能提示:

这种特殊的语法将定制特性的字段和属性认定为命名参数,它允许定制特性对象构造完了之后,使用命名参数设置对象的公共字段和属性。这就提供了很大的灵活性,你可以将实例构造器的参数设为公共的字段或属性,也可以将公共的字段和属性设为私有,然后在实例构造函数处接收参数再设置它们。当然,有一点,如果使用实例构造函数,则该函数的参数都必须提供值,如果使用公共字段和属性(命名参数),则可以部分提供值,其他字段和属性可以维持默认值。建议使用属性而不是字段,可以对其进行更多的控制。

最后我们再来看一下编译器对使用了定制特性的类是干了什么?

定制特性的America和China是被写入到元数据中的。

第二节 应用Attribute

如果仅仅是对目标元素应用了定制特性,好像意义并不大,更重要的是在应用了特性之后,我们要使用这些特性附带的信息。通常是通过反射(Reflection)配合System.Attribute的静态方法来使用特性信息。先来看一下System.Attribute的三个重要的静态方法:

IsDefined 判断指定的目标元素是否应用了System.Attribute的派生类(定制特性),它有多个重载。

GetCustomAttribute 返回应用于目标元素的与指定类型一致的特性对象,如果目标元素没有应用特性实例则返回null;如果目标元素应用了指定特性类型的多个实例,则抛出异常,它也有多个重载。

GetCustomAttributes 返回应用于目标元素的特性数组,在其重载方法中,也可以指定特性类型,它也有多个重载。

我们新定义一个定制特性:

    [AttributeUsage(AttributeTargets.All)]
public class TestAttribute : Attribute
{ }

AttributeTargets.All指出可以将该特性应用于任何目标元素。

改造一下Sportsman类:

    [Country("China")]
[Country("America")]
public class Sportsman
{
public string Name { get; set; }
[Country("Sports")]
public virtual void Play()
{
Console.WriteLine("Play");
}
}

对方法Play()应用了[Country("Sports")],表明了运动类型为体育运动Sports。接着我们改造Hoopster类的Play()方法:

    public class Hoopster : Sportsman
{
public override void Play()
{
MemberInfo[] members = this.GetType().GetMembers();
foreach (MemberInfo member in members)
{
Attribute testAttr = Attribute.GetCustomAttribute(member, typeof(TestAttribute));
if (testAttr != null && testAttr is TestAttribute)
{
Console.WriteLine(((TestAttribute)testAttr).Message);
}
if (Attribute.IsDefined(member, typeof(CountryAttribute)))
{
Attribute[] attributes = Attribute.GetCustomAttributes(member);
foreach (Attribute item in attributes)
{
CountryAttribute attr = item as CountryAttribute;
if (attr != null)
{
Console.WriteLine(string.Format("运动类型:{0} 运动员人数:{1}", attr.Name, attr.PlayerCount));
}
}
}
}
}
}

获取当前对象的全部成员后,接着循环每一个成员。

无论是Sportsman类还是Hoopster类都没有应用TestAttribute特性,所以testAttr将一直保持为null。

接着我们应用Attribute.IsDefined方法判断每一个成员是否应用了定制特性CountryAttribute。如果应用了,接着获取所有应用于该成员的特性。然后循环特性,如果特性是定制特性CountryAttribute类型,则打印出我们定义的运动类型和运动员人数。运行结果如下:

很明显,我们对基类Sportsman方法Play的定义[Country("Sports")]已经影响到了子类Hoopster,这验证了我们前面所说的Inherited = true的作用。由于我们未给PlayerCount赋值,所以它依然是默认值0。

接下来我们继续改造子类Hoopster的方法Play:

        [Country("Ball", PlayerCount = 5)]
public override void Play()
{
//...
}

再来看看它的运行结果:

这一次不仅打印出了Sports/0,还打印出了Ball/5,这是因为我们为子类Hoopster的Play方法应用了[Country("Ball", PlayerCount = 5)]特性,方法Play不仅得到了基类的特性信息,也拥有自己的特性信息。

小 结
本博客文章版权归博客园和solan3000共同所有。

C#提高------------------------Attribute自定制概念的更多相关文章

  1. 搜索引擎中index、attribute和summary概念

    index:倒排索引 attribute: 正排索引 summary:数据集合,用于数据结果展示.

  2. (转)Attribute在.net编程中的应用

    Attribute在.net编程中的应用(一)Attribute的基本概念 经常有朋友问,Attribute是什么?它有什么用?好像没有这个东东程序也能运行.实际上在.Net中,Attribute是一 ...

  3. Attribute在.net编程中的应用(一)

    Attribute的基本概念 经常有朋友问,Attribute是什么?它有什么用?好像没有这个东东程序也能运行.实际上在.Net中,Attribute是一个非常重要的组成部分,为了帮助大家理解和掌握A ...

  4. [转]Attribute在.net编程中的应用

    Attribute在.net编程中的应用(一) Attribute的基本概念 经常有朋友问,Attribute是什么?它有什么用?好像没有这个东东程序也能运行.实际上在.Net中,Attribute是 ...

  5. .NET特性-Attribute

    两篇文章有助于学习Attribute特性的概念. http://blog.csdn.net/byondocean/article/details/6802111 http://www.cnblogs. ...

  6. 从PowerDesigner概念设计模型(CDM)中的3种实体关系说起

    转:http://www.cnblogs.com/xingyukun/archive/2007/08/02/840293.html CDM是大多数开发者使用PD时最先创建的模型,也是整个数据库设计最高 ...

  7. Attribute(两)——定义自己的特色+Asp.net MVC中间filter详细解释

    部分博客是预先定义的有关特性的一些基本特征,同时还Attribute这一概念的一个宏观上的认识,在上篇博客结尾介绍了有关为自己定义特性服务的AttributeUsage,这篇博客主要是通过filter ...

  8. elasticsearch基本概念与查询语法

    序言 后面有大量类似于mysql的sum, group by查询 elk === elk总体架构 https://www.elastic.co/cn/products Beat 基于go语言写的轻量型 ...

  9. js完整教程一 : 基本概念和数组操作

    文章提纲 JS相关常识 JS基本概念 实践 总结 JS相关常识 js是一种可以与HTML标记语言混合使用的脚本语言,其编写的程序可以直接在浏览器中解释执行. 一.组成 js是一种专门为网页交互设计的脚 ...

随机推荐

  1. Android使用binder访问service的方式(一)

    binder机制是贯穿整个android系统的进程间访问机制,经常被用来访问service,我们结合代码看一下binder在访问service的情形下是怎么具体使用的. service 你可以理解成没 ...

  2. android alipay

    "java.security.spec.InvalidKeySpecException" KeyFactory keyFactory =KeyFactory.getInstance ...

  3. SpringBoot整合SpringKafka实现消费者史上最简代码实现

    该项目是使用的技术:SpringBoot  + SpringKafka + Maven 先看pom.xml文件中引入的依赖: <?xml version="1.0" enco ...

  4. shell 全局剔除标点符号

    vim打开文件 []如果是单个字符的话,加上中括号就代表“或”了 :%s/[`~!@#$^&*()=|{}':;',\[\].<>?�/¥……——|[]‘::”“'.,.]//g ...

  5. 推荐20款JavaScript框架给前端开发者

    下面,我们给大家提供了一个用于 HTML5 开发的各种用途的 JavaScript 库列表.这些框架能够给前端开发人员提供更好的功能实现的解决方案.如果你有收藏优秀的框架,也可以在后面的评论中分享给我 ...

  6. LeetCode: Search in Rotated Sorted Array II 解题报告

    Search in Rotated Sorted Array II Follow up for "LeetCode: Search in Rotated Sorted Array 解题报告& ...

  7. pandas简单应用

    机器学习离不开数据,数据分析离不开pandas.昨天感受了一下,真的方便.按照一般的使用过程,将pandas的常用方法说明一下. 首先,我们拿到一个excel表,我们将之另存为csv文件.因为文件是实 ...

  8. poj1703(种类并查集)

    题意:有两个犯罪集团,现在有两种操作,D [a] [b]表示a和b是属于不同犯罪集团的,A [a] [b] 是询问你a和b的关系,如果ab属于同一个犯罪集团,输出In the same gang.   ...

  9. 移动web开发(四)——X-UA-Compatible

    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> IE=edge告诉 ...

  10. iOS友盟社会化分享U-Share分享面板不显示的问题(基本配置没有错误)

    //要先是window可视化 [self.window makeKeyAndVisible]; //添加友盟分享[[UMSocialManager defaultManager] openLog:YE ...