特性是一种允许我们向程序的程序集添加元数据的语言结构。它是用于保存程序结构信息的特殊类型的类。
或者说
特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。
由方括号,中间包裹着特性名和参数列表(也可以无参)。放置在它所要应用的元素之前(类前,方法前等等)。可以参考下面3个特性。
[Serializable]
[XmlRoot("MyClass")]
public class MyClass
{
public int id;
[NonSerialized]
public string name;
}
声明特性能够应用于什么类型的程序结构,仅可用在特性声明上,也就是说只能在Attribute的派生类上生效。如下是Serializable特性源码,其在特性声明时使用了AttributeUsage特性。
namespace System
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Delegate, Inherited = false)]
public sealed class SerializableAttribute : Attribute
{
public SerializableAttribute();
}
}
关于AttributeTargets对应的字段可以参考官方文档
AttributeUsage的公有属性:
名字 | 意义 |
---|---|
ValidOn | 保存能应用特性的目标类型的列表。构造函数的第一个参数是 AttributeTargets类型的枚举值 |
Inherited | 指示特性是否可被装饰类型的派生类所继承 |
AllowMultiple | 指示目是否可以给目标上多次指定指定的特性。 |
使用举例,如下是一个自定义特性:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Field,Inherited =true,AllowMultiple =true)]
public sealed class MyCustomAttribute : Attribute
{
public MyCustomAttribute()
{
}
public MyCustomAttribute(string name)
{
}
}
[MyCustom("有参构造")]
[MyCustom("有参构造2")]
public class MyClass
{
[MyCustom]
public int id;
}
当某些旧方法过时了,你不想要在使用这个方法了,可以用Obsolete特性来将程序结构标注为“过时”,并且在代码编译时会显示警告信息。下面是Obsolete特性的源码。
namespace System
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)]
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; }
}
}
从源码中,我们可以指定这个特性能够用在类,结构,枚举,构造,方法,属性,字段,事件,接口上。通过它的构造方法,我们可以知道它有三个重载。这个特性可以不传入参数,可以传入一个参数,也可以传入两个参数。
下面是对这个特性的举例使用。
例如使用一个参数的重载时,在MyClass类上标注[Obsolete("类已经过时")]
,代码编译时会显示警告“类已过时”。在NewMyClass的Sum方法上标注[Obsolete("方法已经过时,请使用NewSum()")]
,在调用Sum方法时,也会有相应的警告显示。
[Obsolete("类已经过时")]
public class MyClass
{
public void Sum()
{
}
}
public class NewMyClass
{
[Obsolete("方法已经过时,请使用NewSum()")]
public void Sum()
{
}
public void NewSum()
{
}
}
public class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
NewMyClass newMyClass = new NewMyClass();
newMyClass.Sum();
newMyClass.NewSum();
}
}
虽然上面的代码,代码编译时显示了警告,但是代码仍然可以执行。而在使用两个参数的重载时,并将Obsolete的第二个参数设置为true时,将会标记为错误,如下所示。
public class NewMyClass
{
[Obsolete("num", true)]
public int num;
}
public class Program
{
static void Main(string[] args)
{
NewMyClass newMyClass = new NewMyClass();
newMyClass.num = 1;
}
}
Conditional特性允许我们包括或排斥特定方法的所有调用。简单的讲它可以根据条件编译来进行方法调用。
#define MyClass
using System;
using System.Diagnostics;
namespace AttributesConditional
{
public class MyClass
{
[Conditional("MyClass")]
public void MethodA(string msg)
{
Console.WriteLine(msg);
}
public void MethodB()
{
Console.WriteLine("进行中");
}
}
class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
myClass.MethodA("开始");
myClass.MethodB();
myClass.MethodA("结束");
Console.ReadKey();
}
}
}
如上方代码所示,我们在代码的第一行定义一个编译符号(MyClass
),即是#define MyClass
,而在方法上使用这个特性[Conditional("MyClass")]
并指定编译符号(MyClass
)。运行程序,其结果如下所示。
运行结果:
开始
进行中
结束
而当我们将第一行#define MyClass
注释掉后,再次运行,结果如下:
进行中
通过观察,我们发现,当在代码第一行定义编译符号(如:#define MyClass
后,使用特性指定编译符号(如:[Conditional("MyClass")]
),运行时会调用相应的方法(如:MethodA),而如果没有在第一行定义编译符号,那么相应的方法会被忽略,不会被调用。
Conditional特性可以应用在方法和类上。当然这需要符合一定的规则。
需要注意的是定义编译符号必须是在程序入口代码的文件的第一行。
[特性1(参数...)]
[特性2(参数...)]
[特性1(参数...),特性2(参数...)]
如代码下面所示:
[Serializable,Obsolete]
public class MyClass
{
private int num;
}
[Serializable]
[Obsolete]
public class MyClass
{
private int num;
}
除了预定义的一些特性,我们也可以根据需求来自定义一些特性。
System.Attribute
。例如下面代码:
那么如何使用呢?
可以这样写[MyCustomAttribute]
,也可以简写为[MyCustom]
。当然这里推荐是简写就行了。
特性可以有公有成员,但是公有成员只能是:
如果在声明自定义特性的时候不声明构造函数,那么编译器会为你产生一个默认的无参构造。
自定义特性的构造函数可以被重载。声明自定义特性构造和声明其他类一样,使用类名。例如下面所示。
public sealed class MyCustomAttribute : Attribute
{
public MyCustomAttribute()
{
}
public MyCustomAttribute(string name)
{
}
}
当我们使用特性时,其实就是在指定使用哪个构造函数来创建特性的实例。使用如下。
[MyCustom("有参构造")]
public class MyClass
{
[MyCustom]
public int id;
}
有些预定义的特性限制了其使用范围,例如有的特性只能在类上使用,有的特性只能在字段上使用,这种限制是通过AttributeUsage来实现的,这在上面已经介绍过了。
就像在类中定义了属性,但是属性没有被调用,那么这个属性所蕴含的信息我们并不会知道。同理,仅仅声明了自定义特性,并且应用在某些程序结构上使程序结构和所应用的特性相关联,但是着仅仅相当于一个标识,我们并不没有检索这些特性蕴含的信息。所以,我们就需要通过某种反射的方式来访问自定义特性。
IsDefined(Type, Boolean) :检测某个特性是否应用到某个类上。
下方示例是一个检测MyClass
类上是否应用了一个MyCustom
的特性,这就用到了IsDefined
方法,其第一个参数就是要接受检查的MyCustomAttribute
的Type对象,第二个参数就是指示是否搜索MyClass
的继承链来查找这个特性。
[AttributeUsage(AttributeTargets.Class)]
public sealed class MyCustomAttribute : Attribute
{
public MyCustomAttribute()
{
}
}
[MyCustom]
public class MyClass
{
}
class Program
{
static void Main(string[] args)
{
MyClass myClass = new MyClass();
Type type = myClass.GetType();
bool isDefined = type.IsDefined(typeof(MyCustomAttribute), false);
if (isDefined)
Console.WriteLine(type.Name+"上使用了特性MyCustom");
Console.ReadKey();
}
}
运行结构:
MyClass上使用了特性MyCustom
GetCustomAttributes(Boolean):返回应用到结构上的特性的数组。参数指定是否搜索继承链查找特性。
GetCustomAttributes(Type, Boolean):返回应用到结构上的特性的数组。第一个参数,要搜索的特性类型。第二个参数,指定是否搜索继承链查找特性。
[AttributeUsage(AttributeTargets.Class)]
public sealed class MyCustomAttribute : Attribute
{
public string Name { get; set; }
public int Num { get; set; }
public MyCustomAttribute(string name,int num)
{
Name = name;
Num = num;
}
}
[MyCustom("MyCustomDemo",1)]
public class MyClass
{
}
class Program
{
static void Main(string[] args)
{
Type type = typeof(MyClass);
object[] attArr = type.GetCustomAttributes(false);
foreach (var att in attArr)
{
MyCustomAttribute myCustomAttribute = att as MyCustomAttribute;
if (myCustomAttribute != null)
{
Console.WriteLine("name:"+myCustomAttribute.Name);
Console.WriteLine("num:"+myCustomAttribute.Num);
}
}
Console.ReadKey();
}
}
运行结果:
name:MyCustomDemo
num:1
上面示例仅仅是针对类上的,那么针对字段的如何实现?
[AttributeUsage(AttributeTargets.Class| AttributeTargets.Field)]
public sealed class MyCustomAttribute : Attribute
{
public string Name { get; set; }
public int Num { get; set; }
public MyCustomAttribute(string name,int num)
{
Name = name;
Num = num;
}
}
[MyCustom("MyCustomDemo",1)]
public class MyClass
{
[MyCustom("id", 2)]
public string id;
}
class Program
{
static void Main(string[] args)
{
Type type = typeof(MyClass);
FieldInfo[] fieldInfos = type.GetFields();
foreach (var fieldInfo in fieldInfos)
{
object[] attArr = fieldInfo.GetCustomAttributes(false);
foreach (var att in attArr)
{
MyCustomAttribute myCustomAttribute = att as MyCustomAttribute;
if (myCustomAttribute != null)
{
Console.WriteLine("name:" + myCustomAttribute.Name);
Console.WriteLine("num:" + myCustomAttribute.Num);
}
}
}
Console.ReadKey();
}
}
通过上面代码,我们可以发现,字段的也是用GetCustomAttributes方法,不过不是通过type直接调了,而是通过先通过反射获取到字段,然后通过获取的字段引用调用GetCustomAttributes,接下来就和签名的一样了。
那么同理,方法上也可以用类似的方法获取到数据。
[AttributeUsage(AttributeTargets.Class| AttributeTargets.Field|AttributeTargets.Method)]
public sealed class MyCustomAttribute : Attribute
{
public string Name { get; set; }
public int Num { get; set; }
public MyCustomAttribute(string name,int num)
{
Name = name;
Num = num;
}
}
[MyCustom("MyCustomDemo",1)]
public class MyClass
{
[MyCustom("id", 2)]
public string id;
[MyCustom("method",3)]
public void Method()
{
}
}
class Program
{
static void Main(string[] args)
{
Type type = typeof(MyClass);
MethodInfo[] methodInfos = type.GetMethods();
foreach (var methodInfo in methodInfos)
{
object[] attArr = methodInfo.GetCustomAttributes(false);
foreach (var att in attArr)
{
MyCustomAttribute myCustomAttribute = att as MyCustomAttribute;
if (myCustomAttribute != null)
{
Console.WriteLine("name:" + myCustomAttribute.Name);
Console.WriteLine("num:" + myCustomAttribute.Num);
}
}
}
Console.ReadKey();
}
}
总结:我们可以通过反射的GetCustomAttributes方法来获取应用到结构上的特性的数组,来获取数据。无论是类,字段或是方法等等结构,我们都可以通过反射得到相应的结构引用,在通过结构的引用调用GetCustomAttributes方法来得到结构上的特性的数组,以此来得到相应的数据。
我的学习总结就到这里,更多特性相关的可以参考官方文档