Think about it if there is a messaging system that has a method to send a short message to someone:
// title: 标题;author:作者;content:内容;receiverId:接受者Id public bool SendMsg(string title, string author, string content, int receiverId){ // Do Send Action }
We soon discovered that listing the parameters one by one in the parameter list of the method is scalable It's terrible. We'd better define a Message class to encapsulate the short message, and then pass a Message object to the method:
public class Message{ private string title; private string author; private string content; private int receiverId; // 略 } public bool SendMsg(Messag msg){ // Do some Action }
At this time, we should probably delete the old method and replace it with this more scalable SendMsg method . Unfortunately, we often can't, because this set of programs may be released as a set of APIs, and many client programs are already using the old version of the SendMsg() method. If we simply delete the old SendMsg() when updating the program method, then the client program using the old version of SendMsg() method will not work.
What should we do at this time? Of course we can do it through method overloading, so we don’t have to delete the old SendMsg() method. But if the new SendMsg() not only optimizes the passing of parameters, but also comprehensively optimizes the algorithm and efficiency, then we will be eager to inform the client program that there is now a new high-performance SendMsg() method available. , but at this time the client program does not know that a new SendMsg method already exists, so what should we do? We can call the programmer who maintains the client program or send him an email, but this is obviously not convenient enough. There is a way that once he compiles the project, as long as there is a call to the old version of the SendMsg() method, the compiler will be notified.
There are features available in .Net to do this. A property is an object that can be loaded into an assembly and its objects. These objects include the assembly itself, modules, classes, interfaces, structures, constructors, methods, method parameters, etc. Objects loaded with properties are called properties. The English name of the target
attribute is called Attribute. In some books, it is translated as "attribute"; in other books, it is translated as "attribute"; because usually we will contain get and/or set accessors The class members are called "properties" (English Property), so in this article I will use the term "property" to distinguish "properties" (Property).
Let’s take a look at this example to see how features solve the above problem: We can add the Obsolete feature to the old SendMsg() method to tell the compiler that this method is obsolete, and then when the compiler finds that there is a place in the program When using this method marked with Obsolete, a warning message will be given.
namespace Attribute { public class Message {} public class TestClass { // 添加Obsolete特性 [Obsolete("请使用新的SendMsg(Message msg)重载方法")] public static void ShowMsg() { Console.WriteLine("这是旧的SendMsg()方法"); } public static void ShowMsg(Message msg) { Console.WriteLine("新SendMsg()方法"); } } class Program { static void Main(string[] args) { TestClass.ShowMsg(); TestClass.ShowMsg(new Message()); } } }
Now run this code, we will find that the compiler gives a warning: warning CS0618: "Attribute.TestClass.ShowMsg()" is obsolete: "Please use the new SendMsg (Message msg) overloaded method". By using the attribute, we can see that the compiler gives a warning message to tell the client program that a new method is available for use. In this way, after the programmer sees this warning message, he will consider using the new SendMsg() method. .
Through the above example, we have roughly seen how to use the feature: first there is a pair of square brackets "[]", followed by the left square bracket "[" followed by the name of the feature, such as Obsolete, followed by a round bracket " ()". Unlike ordinary classes, these parentheses can not only write the parameters of the constructor, but also assign values to the attributes of the class. In the example of Obsolete, only the constructor parameters are passed.
Use constructor parameters. The order of parameters must be the same as the order in which the constructor is declared. All parameters are also called positional parameters (Positional Parameters) in the properties. Correspondingly, attribute parameters are also called named parameters (Named Parameters).
If you can’t define a feature yourself and use it, I don’t think you can understand the feature well. Let’s build a feature ourselves now. Suppose we have such a very common requirement: when we create or update a class file, we need to indicate when and by whom the class was created. In future updates, we also need to indicate when and by whom it was updated. You can You don’t have to record the updated content. What would you do in the past? Do you add comments to the class like this:
//更新:jayce, 2016-9-10, 修改 ToString()方法 //更新:pop, 2016-9-18 //创建:code, 2016-10-1 public class DemoClass{ // Class Body }
This can indeed be recorded, but if one day we want to record these Save it to the database for backup? Do you want to view the source files one by one, find these comments, and then insert them into the database one by one?
Through the definition of the above characteristics, we know that characteristics can be used to give types Add metadata (data that describes the data, including whether the data was modified, when it was created, and who created it. This data can be a class, method, or attribute). These metadata can be used to describe the type. Well this is where properties should come in handy. So in this example, the metadata should be: comment type ("update" or "create"), modifier, date, remark information (optional). The target type of the attribute is the DemoClass class.
According to the understanding of the metadata attached to the DemoClass class, we first create a class RecordAttribute that encapsulates the metadata:
public class RecordAttribute { private string recordType; // 记录类型:更新/创建 private string author; // 作者 private DateTime date; // 更新/创建 日期 private string memo; // 备注 // 构造函数,构造函数的参数在特性中也称为“位置参数”。 public RecordAttribute(string recordType, string author, string date) { this.recordType = recordType; this.author = author; this.date = Convert.ToDateTime(date); } // 对于位置参数,通常只提供get访问器 public string RecordType { get { return recordType; } } public string Author { get { return author; } } public DateTime Date { get { return date; } } // 构建一个属性,在特性中也叫“命名参数” public string Memo { get { return memo; } set { memo = value; } } }
Note that the parameter date of the constructor must be a constant, Type type, or constant array, So the DateTime type cannot be passed directly.
这个类不光看上去,实际上也和普通的类没有任何区别,显然不能它因为名字后面跟了个Attribute就摇身一变成了特性。那么怎样才能让它称为特性并应用到一个类上面呢?进行下一步之前,我们看看.Net内置的特性Obsolete是如何定义的:
namespace System { [Serializable] [AttributeUsage(6140, Inherited = false)] [ComVisible(true)] public sealed class ObsoleteAttribute : Attribute { public ObsoleteAttribute(); public ObsoleteAttribute(string message); public ObsoleteAttribute(string message, bool error); public bool IsError { get; } public string Message { get; } } }
首先,我们应该发现,它继承自Attribute类,这说明我们的 RecordAttribute 也应该继承自Attribute类。 (一个特性类与普通类的区别是:继承了Attribute类)
其次,我们发现在这个特性的定义上,又用了三个特性去描述它。这三个特性分别是:Serializable、AttributeUsage 和 ComVisible。Serializable特性我们前面已经讲述过,ComVisible简单来说是“控制程序集中个别托管类型、成员或所有类型对 COM 的可访问性”(微软给的定义)这里我们应该注意到:特性本身就是用来描述数据的元数据,而这三个特性又用来描述特性,所以它们可以认为是“元数据的元数据”(元元数据:meta-metadata)。(从这里我们可以看出,特性类本身也可以用除自身以外的其它特性来描述,所以这个特性类的特性是元元数据。)
因为我们需要使用“元元数据”去描述我们定义的特性 RecordAttribute,所以现在我们需要首先了解一下“元元数据”。这里应该记得“元元数据”也是一个特性,大多数情况下,我们只需要掌握 AttributeUsage就可以了,所以现在就研究一下它。我们首先看上面AttributeUsage是如何加载到ObsoleteAttribute特性上面的。
[AttributeUsage(6140, Inherited = false)]
然后我们看一下AttributeUsage的定义:
namespace System { public sealed class AttributeUsageAttribute : Attribute { public AttributeUsageAttribute(AttributeTargets validOn); public bool AllowMultiple { get; set; } public bool Inherited { get; set; } public AttributeTargets ValidOn { get; } } }
可以看到,它有一个构造函数,这个构造函数含有一个AttributeTargets类型的位置参数(Positional Parameter) validOn,还有两个命名参数(Named Parameter)。注意ValidOn属性不是一个命名参数,因为它不包含set访问器,(是位置参数)。
这里大家一定疑惑为什么会这样划分参数,这和特性的使用是相关的。假如AttributeUsageAttribute 是一个普通的类,我们一定是这样使用的:
// 实例化一个 AttributeUsageAttribute 类 AttributeUsageAttribute usage=new AttributeUsageAttribute(AttributeTargets.Class); usage.AllowMultiple = true; // 设置AllowMutiple属性 usage.Inherited = false;// 设置Inherited属性
但是,特性只写成一行代码,然后紧靠其所应用的类型(目标类型),那么怎么办呢?微软的软件工程师们就想到了这样的办法:不管是构造函数的参数 还是 属性,统统写到构造函数的圆括号中,对于构造函数的参数,必须按照构造函数参数的顺序和类型;对于属性,采用“属性=值”这样的格式,它们之间用逗号分隔。于是上面的代码就减缩成了这样:
[AttributeUsage(AttributeTargets.Class, AllowMutiple=true, Inherited=false)]
可以看出,AttributeTargets.Class是构造函数参数(位置参数),而AllowMutiple 和 Inherited实际上是属性(命名参数)。命名参数是可选的。将来我们的RecordAttribute的使用方式于此相同。(为什么管他们叫参数,我猜想是因为它们的使用方式看上去更像是方法的参数吧。)假设现在我们的RecordAttribute已经OK了,则它的使用应该是这样的:
[RecordAttribute("创建","张子阳","2008-1-15",Memo="这个类仅供演示")] public class DemoClass{ // ClassBody } //其中recordType, author 和 date 是位置参数,Memo是命名参数。
从AttributeUsage特性的名称上就可以看出它用于描述特性的使用方式。具体来说,首先应该是其所标记的特性可以应用于哪些类型或者对象。从上面的代码,我们看到AttributeUsage特性的构造函数接受一个 AttributeTargets 类型的参数,那么我们现在就来了解一下AttributeTargets。
AttributeTargets 是一个位标记,它定义了特性可以应用的类型和对象。
public enum AttributeTargets { Assembly = 1, //可以对程序集应用属性。 Module = 2, //可以对模块应用属性。 Class = 4, //可以对类应用属性。 Struct = 8, //可以对结构应用属性,即值类型。 Enum = 16, //可以对枚举应用属性。 Constructor = 32, //可以对构造函数应用属性。 Method = 64, //可以对方法应用属性。 Property = 128, //可以对属性 (Property) 应用属性 (Attribute)。 Field = 256, //可以对字段应用属性。 Event = 512, //可以对事件应用属性。 Interface = 1024, //可以对接口应用属性。 Parameter = 2048, //可以对参数应用属性。 Delegate = 4096, //可以对委托应用属性。 ReturnValue = 8192, //可以对返回值应用属性。 GenericParameter = 16384, //可以对泛型参数应用属性。 All = 32767, //可以对任何应用程序元素应用属性。 }
因为AttributeUsage是一个位标记,所以可以使用按位或“|”来进行组合。所以,当我们这样写时:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)
意味着既可以将特性应用到类上,也可以应用到接口上。
AllowMutiple 属性用于设置该特性是不是可以重复地添加到一个类型上(默认为false),就好像这样:
[RecordAttribute("更新","jayce","2016-1-20")] [RecordAttribute("创建","pop","2016-1-15",Memo="这个类仅供演示")] public class DemoClass{ // ClassBody }
Inherited 就更复杂一些了,假如有一个类继承自我们的DemoClass,那么当我们将RecordAttribute添加到DemoClass上时,DemoClass的子类也会获得该特性。而当特性应用于一个方法,如果继承自该类的子类将这个方法覆盖,那么Inherited则用于说明是否子类方法是否继承这个特性。
现在实现RecordAttribute应该是非常容易了,对于类的主体不需要做任何的修改,我们只需要让它继承自Attribute基类,同时使用AttributeUsage特性标记一下它就可以了(假定我们希望可以对类和方法应用此特性):
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple=true, Inherited=false)] public class RecordAttribute:Attribute { // 略 }
我们已经创建好了自己的自定义特性,现在是时候使用它了。
[Record("更新", "code", "2016-1-20", Memo = "修改 ToString()方法")] [Record("更新", "jayce", "2016-1-18")] [Record("创建", "pop", "2016-1-15")] public class DemoClass { public override string ToString() { return "This is a demo class"; } } class Program { static void Main(string[] args) { DemoClass demo = new DemoClass(); Console.WriteLine(demo.ToString()); } }
利用反射来查看 自定义特性信息 与 查看其他信息 类似,首先基于类型(本例中是DemoClass)获取一个Type对象,然后调用Type对象的GetCustomAttributes()方法,获取应用于该类型上的特性。当指定GetCustomAttributes(Type attributeType, bool inherit) 中的第一个参数attributeType时,将只返回指定类型的特性,否则将返回全部特性;第二个参数指定是否搜索该成员的继承链以查找这些属性。
class Program { static void Main(string[] args) { Type t = typeof(DemoClass); Console.WriteLine("下面列出应用于 {0} 的RecordAttribute属性:" , t); // 获取所有的RecordAttributes特性 object[] records = t.GetCustomAttributes(typeof(RecordAttribute), false); foreach (RecordAttribute record in records) { Console.WriteLine(" {0}", record); Console.WriteLine(" 类型:{0}", record.RecordType); Console.WriteLine(" 作者:{0}", record.Author); Console.WriteLine(" 日期:{0}", record.Date.ToShortDateString()); if(!String.IsNullOrEmpty(record.Memo)){ Console.WriteLine(" 备注:{0}",record.Memo); } } } }