Denken Sie darüber nach, ob es ein Nachrichtensystem gibt, das über eine Methode zum Senden einer Kurznachricht an jemanden verfügt:
// title: 标题;author:作者;content:内容;receiverId:接受者Id public bool SendMsg(string title, string author, string content, int receiverId){ // Do Send Action }
Wir haben bald festgestellt, dass die Parameter einzeln wie folgt aufgelistet sind. Die Skalierbarkeit des Die Parameterliste der Methode ist sehr dürftig. Wir sollten besser eine Message-Klasse definieren, um die Kurznachricht zu kapseln, und dann ein Message-Objekt an die Methode übergeben:
public class Message{ private string title; private string author; private string content; private int receiverId; // 略 } public bool SendMsg(Messag msg){ // Do some Action }
Zu diesem Zeitpunkt sollten wir wahrscheinlich ersetzen Die alte Methode wird gelöscht und durch diese skalierbarere SendMsg-Methode ersetzt. Leider ist dies oft nicht möglich, da diese Gruppe von Programmen möglicherweise als eine Reihe von APIs veröffentlicht wird und viele Client-Programme bereits die alte Version der SendMsg()-Methode verwenden. Wenn wir beim Aktualisieren einfach die alte SendMsg()-Methode löschen Wenn Sie die Programmmethode verwenden, funktioniert das Client-Programm, das die alte Version der SendMsg()-Methode verwendet, nicht.
Was sollen wir zu diesem Zeitpunkt tun? Natürlich können wir dies durch Methodenüberladung tun, sodass wir die alte SendMsg()-Methode nicht löschen müssen. Wenn das neue SendMsg() jedoch nicht nur die Parameterübergabe optimiert, sondern auch den Algorithmus und die Effizienz umfassend optimiert, werden wir das Client-Programm gerne darüber informieren, dass jetzt eine neue leistungsstarke SendMsg()-Methode verfügbar ist. Aber zu diesem Zeitpunkt weiß das Client-Programm nicht, dass bereits eine neue SendMsg-Methode vorhanden ist. Was sollen wir also tun? Wir können den Programmierer anrufen, der das Client-Programm verwaltet, aber das ist offensichtlich nicht bequem genug Auf diese Weise wird der Compiler benachrichtigt, sobald er das Projekt kompiliert und die alte Version der SendMsg()-Methode aufgerufen wird. Um dies zu erreichen, stehen in
.Net Funktionen zur Verfügung. Eine Eigenschaft ist ein Objekt, das in eine Assembly geladen werden kann, und zu seinen Objekten gehören die Assembly selbst, Module, Klassen, Schnittstellen, Strukturen, Konstruktoren, Methoden, Methodenparameter usw. Mit Eigenschaften geladene Objekte werden als Eigenschaften bezeichnet Der Name des Zielattributs
heißt Attribut. In einigen Büchern wird er mit „Attribut“ übersetzt, in anderen Büchern wird er mit „Eigenschaft“ übersetzt Klassenmitglieder werden „Eigenschaften“ (englisch Property) genannt, daher werde ich in diesem Artikel den Begriff „Eigenschaft“ verwenden, um „Eigenschaften“ zu unterscheiden.
Nehmen wir dieses Beispiel, um zu sehen, wie die Funktion das obige Problem löst: Wir können die Obsolete-Funktion zur alten SendMsg()-Methode hinzufügen, um dem Compiler mitzuteilen, dass diese Methode veraltet ist, und dann, wenn der Compiler es herausfindet Wenn es eine Stelle im Programm gibt, die diese Methode verwendet und mit „Veraltet“ gekennzeichnet ist, wird eine Warnmeldung ausgegeben.
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()); } } }
Führen Sie nun diesen Code aus. Wir werden feststellen, dass der Compiler eine Warnung ausgibt: warning CS0618: „Attribute.TestClass.ShowMsg()“ ist veraltet: „Bitte verwenden Sie die neue SendMsg(Message msg) überlastet Verfahren". Durch die Verwendung des Attributs können wir sehen, dass der Compiler eine Warnmeldung ausgibt, um dem Client-Programm mitzuteilen, dass eine neue Methode zur Verwendung verfügbar ist. Auf diese Weise wird der Programmierer, nachdem er diese Warnmeldung gesehen hat, die Verwendung des neuen SendMsg() in Betracht ziehen. Methode.
Anhand des obigen Beispiels haben wir grob gesehen, wie die Funktion verwendet wird: Zuerst gibt es ein Paar eckiger Klammern „[]“, gefolgt von der linken eckigen Klammer „[“, gefolgt vom Namen der Funktion , wie „Obsolete“, gefolgt von einer Klammer „()“. Im Gegensatz zu gewöhnlichen Klassen können diese Klammern nicht nur die Parameter des Konstruktors schreiben, sondern auch den Attributen der Klasse Werte zuweisen. Im Beispiel von Obsolete werden nur die Konstruktorparameter übergeben.
Verwenden Sie Konstruktorparameter. Die Reihenfolge der Parameter muss mit der Reihenfolge übereinstimmen, in der der Konstruktor deklariert wird. Alle werden in den Eigenschaften auch als Positionsparameter bezeichnet werden auch benannte Parameter (Benannte Parameter) genannt.
Wenn Sie eine Funktion nicht selbst definieren und verwenden können, können Sie die Funktion meiner Meinung nach nicht gut verstehen. Lassen Sie uns jetzt selbst eine Funktion erstellen. Angenommen, wir haben eine so häufige Anforderung: Wenn wir eine Klassendatei erstellen oder aktualisieren, müssen wir angeben, wann und von wem die Klasse erstellt wurde. Bei zukünftigen Aktualisierungen müssen wir auch angeben, wann und von wem sie aktualisiert wurde Bei der Aufzeichnung müssen keine aktualisierten Inhalte aufgezeichnet werden. Was würden Sie in der Vergangenheit so tun? Möchten Sie diese Datensätze zur Sicherung in der Datenbank speichern? Möchten Sie die Quelldateien einzeln anzeigen, diese Kommentare finden und sie dann einzeln in die Datenbank einfügen? Wissen Sie, dass Attribute zum Hinzufügen von Metadaten zu Typen verwendet werden können (Daten, die die Daten beschreiben, einschließlich der Frage, ob die Daten geändert wurden, wann sie erstellt wurden und wer sie erstellt hat. Diese Daten können eine Klasse, Methode oder ein Attribut sein. Diese Metadaten). kann als Beschreibungstyp verwendet werden. Nun, hier sollten Immobilien nützlich sein. In diesem Beispiel sollten die Metadaten also sein: Kommentartyp („Aktualisieren“ oder „Erstellen“), Modifikator, Datum, Bemerkungsinformationen (optional). Der Zieltyp des Attributs ist die DemoClass-Klasse.
//更新:jayce, 2016-9-10, 修改 ToString()方法 //更新:pop, 2016-9-18 //创建:code, 2016-10-1 public class DemoClass{ // Class Body }Nach dem Verständnis der an die DemoClass-Klasse angehängten Metadaten erstellen wir zunächst eine Klasse RecordAttribute, die Metadaten kapselt:
Beachten Sie, dass der Parameter date des Konstruktors eine Konstante sein muss , Typtyp oder konstantes Array, daher kann der DateTime-Typ nicht direkt übergeben werden.
这个类不光看上去,实际上也和普通的类没有任何区别,显然不能它因为名字后面跟了个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); } } } }