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. (转)Making 1 million requests with python-aiohttp

    转自:https://pawelmhm.github.io/asyncio/python/aiohttp/2016/04/22/asyncio-aiohttp.html Making 1 millio ...

  2. scala连接数据库

    scala连接数据库 使用JDBC即可: 在sbt中添加对应依赖 libraryDependencies ++= Seq( "mysql" % "mysql-connec ...

  3. yum卸载失败Error in PREUN scriptlet in rpm package postgresql-server

    yum --setopt=tsflags=noscripts remove 参考 https://serverfault.com/questions/613256/yum-error-in-preun ...

  4. 多媒体文件格式之AVI

    [时间:2016-07] [状态:Open] AVI(Audio Video Interleaved的缩写)是一种RIFF(Resource Interchange File Format的缩写)文件 ...

  5. ActiveMQ入门实例(转)

    转载自:http://www.cnblogs.com/xwdreamer/archive/2012/02/21/2360818.html 1.下载ActiveMQ 去官方网站下载:http://act ...

  6. Namespace declaration statement has to be the very first statement or after any declare call in the script

    0x00缘起 代码部署在windows上,出现了一个bug,临时用记事本打开修改了一下,于是出现了500错误 0x01排错 查看log,提示如下 "Namespace declaration ...

  7. wcf会话、实例化、并发

    在asp.net中含有会话,是保存值,供所有的程序使用,同样在wcf中也有会话,供多个客户端使用. 会话的支持通常在契约定义的开始标出,如下 [ServiceContract(Namespace = ...

  8. WCF数据契约

    当使用DataMember时,和访问符无关,及时使用了private,成员都是可见的.相反如果使用static,为不可见. 上述的两个数据成员是等效的,如果是等效的话 数据成员的顺序也必须是相同的. ...

  9. ython strip lstrip rstrip使用方法

    Python中的strip用于去除字符串的首尾字符,同理,lstrip用于去除左边的字符,rstrip用于去除右边的字符. 这三个函数都可传入一个参数,指定要去除的首尾字符. 需要注意的是,传入的是一 ...

  10. u-boot2016.05 有关 4096page size , oob == 224 nand 的移植支持

    大致介绍一下这个 nand 的基础属性 pagesize == 4096 byte oob == 224 byte block size == 256 Kbyte u-boot configs/xxx ...