特性

特性(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");
}

特性可以在没有破坏类型封装的前提下,可以加点额外的信息和行为。
以下提供两个实例说明其应用场景:

  1. 绑定枚举
    假如,有如下的枚举类:
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());
  1. 数据检查
    通过特性,我们也可以对对象添加规则,实现数据检查,从而解决数据合法性。
    可以看下面一个例子:
    在实现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字段长度的区间范围。

参考:
特性 (C#)
C#特性

posted @ 2020-11-05 20:34  Jamest  阅读(257)  评论(0编辑  收藏  举报