特性
特性(attribute)是一种允许我们向程序的程序集增加元数据的语言结构。它是用于保存程序结构信息的某种特殊类型的类。
- 将应用了特性的程序结构(program construct)叫做目标(target)。
- 设计用来获取和使用元数据的程序(比如对象浏览器)叫做特性的消费者( consumer)
- .NET预定了很多特性,我们也可以声明自定义特性
下图是使用特性中相关组件的概览,并且也演示了如下有关特性的要点。
- 我们在源代码中将特性应用于程序结构。
- 编译器获取源代码并且从特性产生元数据,然后把元数据放到程序集中。
- 消费者程序可以获取特性的元数据以及程序中其他组件的元数据。注意,编译器同时生产和消费特性。
应用特性
特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集。我们可以通过把特性应用到结构(类,方法,参数,返回值等)来实现。
- 在结构前放置特性片段来应用特性。
- 特性片段被方括号包围,其中是特性名和特性的参数列表。
.NET有几个预定义特性,需要我们了解。
Obsolete特性
可以使用0bsolete特性将程序结构标注为过期的,并且在代码编译时显示有用的警告消息。以下代码给出了一个使用的示例:
[Obsolete("Use method SuperPrintOut")]
在编译的过程中,编译器会产生警告信息来通知我们正在使用一个过期的结构。
Conditional特性
Conditional特性允许我们包括或排斥特定方法的所有调用。为方法声明应用Conditional特性并把编译符作为参数来使用。
- 如果定义了编译符号,那么编译器会包含所有调用这个方法的代码,这和普通方法没有什么区别。
- 如果没有定义编译符号,那么编译器会忽略代码中这个方法的所有调用。
定义方法的CIL代码本身总是会包含在程序集中。只是调用代码会被插入或忽略。
Conditional调用者信息特性
调用者信息特性可以访问文件路径、代码行数、调用成员的名称等源代码信息。
- 这三个特性名称为CallerFilePath、CallerLineNumber和CallerMemberName。
- 这些特性只能用于方法中的可选参数。
DebuggerstepThrough特性
我们在单步调试代码时,常常希望调试器不要进入某些方法。我们只想执行该方法,然后继续调试下一行。DebuggerstepThrough特性告诉调试器在执行目标代码时不要进入该方法调试。在我自己的代码中,这是最常使用的特性。有些方法很小并且毫无疑问是正确的,在调试时对其反复单步调试只能徒增烦恼。但使用该特性时要十分小心,因为你并不想排除那些可能含有bug的代码。
关于DebuggerStepThrough要注意以下两点:
- 该特性位于System.Diagnostics命名空间;
- 该特性可用于类、结构、构造函数、方法或访问器。
自定义特性
特性其实只是某个特殊类型的类。有关特性类的一些要点如下。
- 用户自定义的特性类叫做自定义特性。
- 所有特性类都派生自System.Attribute。
- 安全起见,通常会声明一个sealed的特性类。
和其他类一样,我们不能显式调用构造函数。特性的实例创建后,只有特性的消费者访问特性时才能调用构造函数。这一点与其他类的实例很不相同,这些实例都创建在使用对象创建表达式的位置。应用一个特性是一条声明语句,它不会决定什么时候构造特性类的对象。其实际意义是:“这个特性和这个目标相关联,如果需要构造特性,使用这个构造函数。”
如下,我们构造了一个CustomAttribute:
[AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)]
public class CustomAttribute : Attribute
{
public CustomAttribute()
{ }
public CustomAttribute(int id)
{
Console.WriteLine("********************");
}
public string Description { get; set; }
public string Remark = null;
public void Show()
{
Console.WriteLine($"This is {nameof(CustomAttribute)}");
}
}
限制特性的使用
可以使用Attributeusage特性来限制特性使用在某个目标类型上。
例如,如果我们希望自定义特性MAttribute只能应用到方法上,那么可以以如下形式使用AttributeUsage:
[ AttributeUsage(AttributeTarget.Method) ]
AttributeUsage有三个重要的公共属性,如下:
- ValidOn 保存特性能应用到的目标类型的列表。构造函数的第一个参数必须是AttributeTarget类型的枚举值
- Inherited 保存特性能应用到的目标类型的列表。构造函数的第一个参数必须是AttributeTarget类型的枚举值。默认true。
- AllowMutiple 一个指示目标是否被应用多个特性的实例的布尔值。默认false
因此,默认的Attributeusage特性如下:
[AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)]
访问特性
实际上特性添加后,编译会在元素内部产生IL,但是我们是没办法直接使用的,程序运行的过程中,我们能找到特性,而且也能应用任何一个可以生效的特性,都是因为有地方主动使用了的。
通常可以通过反射来访问特性。假设,我们在类Student设置了多个特性,如下:
[Custom(123, Description = "1234", Remark = "2345")]
public class Student
{
[CustomAttribute]
public int Id { get; set; }
[Leng(5,10)]
public string Name { get; set; }
[Leng(20, 50)]
public string Accont { get; set; }
/// <summary>
/// 10001~999999999999
/// </summary>
[Long(10001, 999999999999)]
public long QQ { get; set; }
[CustomAttribute]
public void Study()
{
Console.WriteLine($"{this.Name}");
}
[Custom()]
[return: Custom()]
public string Answer([Custom]string name)
{
return $"This is {name}";
}
}
则,我们可以通过反射创建特性类的实例,并进行调用方法。
public static void Show(Student student)
{
Type type = typeof(Student); //student.GetType();
if (type.IsDefined(typeof(CustomAttribute), true))//检查是否存在该特性
{
CustomAttribute attribute = (CustomAttribute)type.GetCustomAttribute(typeof(CustomAttribute), true);//实例化
Console.WriteLine($"{attribute.Description}_{attribute.Remark}");
attribute.Show();
}
PropertyInfo property = type.GetProperty("Id");//字段特性
if (property.IsDefined(typeof(CustomAttribute), true))
{
CustomAttribute attribute = (CustomAttribute)property.GetCustomAttribute(typeof(CustomAttribute), true);
Console.WriteLine($"{attribute.Description}_{attribute.Remark}");
attribute.Show();
}
MethodInfo method = type.GetMethod("Answer");//方法特性
if (method.IsDefined(typeof(CustomAttribute), true))
{
CustomAttribute attribute = (CustomAttribute)method.GetCustomAttribute(typeof(CustomAttribute), true);
Console.WriteLine($"{attribute.Description}_{attribute.Remark}");
attribute.Show();
}
ParameterInfo parameter = method.GetParameters()[0];//参数特性
if (parameter.IsDefined(typeof(CustomAttribute), true))
{
CustomAttribute attribute = (CustomAttribute)parameter.GetCustomAttribute(typeof(CustomAttribute), true);
Console.WriteLine($"{attribute.Description}_{attribute.Remark}");
attribute.Show();
}
ParameterInfo returnParameter = method.ReturnParameter;//返回值特性
if (returnParameter.IsDefined(typeof(CustomAttribute), true))
{
CustomAttribute attribute = (CustomAttribute)returnParameter.GetCustomAttribute(typeof(CustomAttribute), true);
Console.WriteLine($"{attribute.Description}_{attribute.Remark}");
attribute.Show();
}
student.Validate();
student.Study();
string result = student.Answer("Eleven");
}
特性可以在没有破坏类型封装的前提下,可以加点额外的信息和行为。
以下提供两个实例说明其应用场景:
- 绑定枚举
假如,有如下的枚举类:
public enum UserState
{
Normal = 0,
Frozen = 1,
Deleted = 2
}
如何通过枚举类输出相应的中文含义呢?一种常见的做法是通过if...else语句,如下:
UserState userState = UserState.Normal;
if (userState == UserState.Normal)
{
Console.WriteLine("正常状态");
}
else if (userState == UserState.Frozen)
{ }
else
{ }
但明显的是,这种方法通过静态绑定并不灵活,且当多个字段存在时,会造成维护困难。一种更灵活的方式是通过特性,实现枚举绑定。
声明自定义特性如下:
public sealed class RemarkAttribute : Attribute
{
public RemarkAttribute(string remark)
{
this._Remark = remark;
}
private string _Remark = null;
public string GetRemark()
{
return this._Remark;
}
}
我们在UserState上增加一个Remark特性,如下:
public enum UserState
{
[Remark("正常")]
Normal = 0,
[Remark("冻结")]
Frozen = 1,
[Remark("删除")]
Deleted = 2
}
并且,可以通过扩展方法实现UserState的GetRemark方法。
public static class RemarkExtension
{
public static string GetRemark(this Enum value)
{
Type type = value.GetType();
FieldInfo field = type.GetField(value.ToString());
if (field.IsDefined(typeof(RemarkAttribute), true))
{
RemarkAttribute attribute = (RemarkAttribute)field.GetCustomAttribute(typeof(RemarkAttribute), true);
return attribute.GetRemark();
}
else
{
return value.ToString();
}
}
}
这样我们就可以直接通过GetRemark方法来得到特性中的中文含义信息。这也是MVC中Display的实现思想。
Console.WriteLine(userState.GetRemark());
- 数据检查
通过特性,我们也可以对对象添加规则,实现数据检查,从而解决数据合法性。
可以看下面一个例子:
在实现Student类时,我们提前加了一个Long特性,可以进行对QQ字段进行数据检查,确认其在某个区间范围内。
LongAttribute定义如下:
public abstract class AbstractValidateAttribute : Attribute
{
public abstract bool Validate(object value);
}
public class LongAttribute : AbstractValidateAttribute
{
private long _Min = 0;
private long _Max = 0;
public LongAttribute(long min, long max)
{
this._Min = min;
this._Max = max;
}
public override bool Validate(object value)
{
if (value != null && !string.IsNullOrWhiteSpace(value.ToString()))
{
if (long.TryParse(value.ToString(), out long lResult))
{
if (lResult > this._Min && lResult < this._Max)
{
return true;
}
}
}
return false;
}
}
同样的,我们也可以通过一个扩展方法来为Student类添加Validate方法:
public static class ValidateExtension
{
public static bool Validate(this object oObject)
{
Type type = oObject.GetType();
foreach (var prop in type.GetProperties())
{
if (prop.IsDefined(typeof(AbstractValidateAttribute), true))
{
object[] attributeArray = prop.GetCustomAttributes(typeof(AbstractValidateAttribute), true);
foreach (AbstractValidateAttribute attribute in attributeArray)
{
if (!attribute.Validate(prop.GetValue(oObject)))
{
return false;
}
}
}
}
return true;
}
}
那么,我们就可以直接通过Validate方法实现了对字段进行检测。这样做的好处是,不会改变原有代码逻辑,并且便于扩展,同样的我们也可以添加Leng等特性,检查Name,Accont字段长度的区间范围。