C#之特性

一、什么是特性?

特性是一种允许我们向程序的程序集添加元数据的语言结构。它是用于保存程序结构信息的特殊类型的类。

或者说

特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。

二、特性的使用

由方括号,中间包裹着特性名和参数列表(也可以无参)。放置在它所要应用的元素之前(类前,方法前等等)。可以参考下面3个特性。

[Serializable]
[XmlRoot("MyClass")]
public class MyClass
{
    
    public int id;

    [NonSerialized]
    public string name;
}

三、特性的属性

  • 在一个元素(如类和属性,方法等等)上可以使用一个或多个特性进行修饰。
  • 特性可以拥有参数。
  • 程序可使用反射来检查自己的元数据或其他程序中的元数据。

四、预定义的特性

1.AttributeUsage特性

声明特性能够应用于什么类型的程序结构,仅可用在特性声明上,也就是说只能在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;
    }
2.Obsolete特性

当某些旧方法过时了,你不想要在使用这个方法了,可以用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();
    }
}

C#之特性_第1张图片
C#之特性_第2张图片

虽然上面的代码,代码编译时显示了警告,但是代码仍然可以执行。而在使用两个参数的重载时,并将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;
    }
}

C#之特性_第3张图片

3.Conditional特性

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特性可以应用在方法和类上。当然这需要符合一定的规则。

  • 方法必须是类或结构体的方法
  • 方法必须为void类型。
  • 方法不能被声明为override,当可以标记为virtual。
  • 方法不能是接口方法的实现。
  • 类上使用必须是Attribute类的派生类。
  • 该特性允许使用多个。

需要注意的是定义编译符号必须是在程序入口代码的文件的第一行。

五、多个特性使用

  • 多层结构:
[特性1(参数...)]
[特性2(参数...)]
  • 逗号分隔
[特性1(参数...),特性2(参数...)]

如代码下面所示:

[Serializable,Obsolete]
public class MyClass
{    
    private int num;
}
[Serializable]
[Obsolete]
public class MyClass
{    
    private int num;
}

六、自定义特性

除了预定义的一些特性,我们也可以根据需求来自定义一些特性。

1.如何声明自定义特性。
  • 定义一个类并且这个类派生自System.Attribute
  • 类的名字以Attribute后缀结构
  • 为了安全性,可以用一个sealed修饰成一个密封类型(非必须的),以防止被其他类所继承(可以参考预定义特性的源码,其都被sealed所修饰)。

例如下面代码:
C#之特性_第4张图片
那么如何使用呢?
可以这样写[MyCustomAttribute],也可以简写为[MyCustom]。当然这里推荐是简写就行了。

特性可以有公有成员,但是公有成员只能是:

  • 字段
  • 属性
  • 构造函数
(1)构造函数

如果在声明自定义特性的时候不声明构造函数,那么编译器会为你产生一个默认的无参构造。
自定义特性的构造函数可以被重载。声明自定义特性构造和声明其他类一样,使用类名。例如下面所示。

public sealed class MyCustomAttribute : Attribute
    {
        public MyCustomAttribute() 
        { 
        
        }

        public MyCustomAttribute(string name)
        { 
        
        }
    }

当我们使用特性时,其实就是在指定使用哪个构造函数来创建特性的实例。使用如下。

[MyCustom("有参构造")]
public class MyClass 
{
     [MyCustom]
     public int id;
}
(2)限制特性的使用

有些预定义的特性限制了其使用范围,例如有的特性只能在类上使用,有的特性只能在字段上使用,这种限制是通过AttributeUsage来实现的,这在上面已经介绍过了。

2.如何访问自定义特性

就像在类中定义了属性,但是属性没有被调用,那么这个属性所蕴含的信息我们并不会知道。同理,仅仅声明了自定义特性,并且应用在某些程序结构上使程序结构和所应用的特性相关联,但是着仅仅相当于一个标识,我们并不没有检索这些特性蕴含的信息。所以,我们就需要通过某种反射的方式来访问自定义特性。

(1)IsDefined方法

IsDefined(Type, Boolean)检测某个特性是否应用到某个类上。

  • 第一个参数是需要检查的特性的Type对象
  • 第二个参数是指示是否搜索此成员的继承链来查找这个特性。

下方示例是一个检测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
(2)GetCustomAttributes方法

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方法来得到结构上的特性的数组,以此来得到相应的数据。

七、最后

我的学习总结就到这里,更多特性相关的可以参考官方文档

你可能感兴趣的:(C#,c#)