CS4L21——特性

本章代码关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Attribute                            // 特性基类,声明特性类时需要继承此类
[特性名()] // 添加特性语法
type.IsDefined() // 判断类似是否添加了某个特性
type.GetCustomAttribute() // 获取类型添加的特定特性信息
type.GetCustomAttributes() // 获取类型添加的所有特性信息
memberInfo.GetCustomAttribute() // 获取类型添加的特定特性信息
memberInfo.GetCustomAttributes() // 获取类型添加的所有特性信息
[AttributeUsage()] // 限制自定义特性的使用范围
[Obsolete] // 过时特性,被添加此特性的类或者方法等调用时会被警告或者报错
[CallerFilePath] // 调用者信息特性,添加到需要得到哪个文件调用的参数(string类型,且需要初始化默认值)
[CallerLineNumber] // 调用者信息特性,添加到需要得到哪个行号调用的参数(int类型,且需要初始化默认值)
[CallerMemberName] // 调用者信息特性,添加到需要得到哪个函数调用的参数(string类型,且需要初始化默认值)
[Conditional()] // 条件编译特性,需要传入#define的符号,如果此符号未定义,则调用添加此特性的方法不会执行
[DllImport()] // 外部引用dll特性,需要传入外部的dll文件路径,修饰一个extern函数,一般用来调用C和C++的写好的方法

特性

特性是一种允许我们向程序的程序集添加元数据的语言结构,它是用于保存程序结构信息的某种特殊类型的类
特性提供功能强大的方法以将声明信息与 C# 代码(类型、方法、属性等)相关联

特性与程序实体关联后,即可在运行时使用反射(Type​、MemberInfo​ 等等)查询特性信息,
特性的目的是告诉编译器把程序结构的某种元数据嵌入程序集中,它可以放置在几乎所有的声明中(类、变量、函数等等声明语句前)

特性的作用包括:

  1. 标记和描述代码元素特性可以用来为代码提供额外信息。

  2. 控制编译器行为特性可以用于影响编译器的某些行为,

    例如:[Obsolete]​:标记某个方法或类已被弃用,调用时会产生警告或错误。[Conditional]​:决定某段代码是否编译。

简单来说就是,特性本质上就是个类,我们可以利用特性类为 元数据 添加额外信息
比如一个类、成员变量、成员方法等等为他们添加更多的额外信息,之后可以通过反射来获取这些额外信息

声明自定义特性

声明特性类需要继承特性基类 Attribute​,同时,特性类的命名需要添加后缀 Attribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CustomAttribute : Attribute
{
// 特性中的成员一般根据需求来写
public string info;
public int number;

public CustomAttribute(string info)
{
this.info = info;
}

public void TestFun()
{
Console.WriteLine("特性的方法被执行了!");
}
}

特性的使用

特性的添加

基本语法:在类、变量、函数声明语句前加上:[特性名(参数列表)]​,表示它们具有该特性信息,它的本质就是在调用特性类的构造函数

添加特性时,特性类的后缀 Attribute​ 在使用特性时可以省略掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Custom("这是 TestClass 类")]
class TestClass
{
[Custom("这是 TestClass.number 变量")]
public int number = 0;

[Custom("这是 TestClass.MyClass 方法")]
public void TestFun() {}

[Custom("这是 TestClass.Add 方法")]
public int Add([Custom("这是 TestClass.Add 方法的参数 a")] int a, int b)
{
return a + b;
}
}

这样,TestClass​ 这个类型就被添加了特性,之后可以通过其对应的 Type​ 对象获取这些类型

对于特性类的公开变量,可以在添加特性时直接对特性类的某个公开变量赋值,注意,它和构造函数传参不一样!
在参数列表左边赋值即可,可以赋值多个变量,即: [特性名(参数列表, 公开变量 = 值, ...)]

1
2
[Custom("这是 TestClass 类", number = 10)]
class TestClass {}

判断某个类型是否使用了某个特性

type.IsDefined()​ 可以判断某个类型是否使用了某个特性

  • 参数一:特性的类型 Type
  • 参数二:是否要顺着继承链向上搜索父类是否使用参数一传入的特性(属性和事件忽略此参数)
  • 返回值:是否使用特性
1
2
3
4
5
6
7
TestClass obj = new TestClass();
Type type = obj.GetType();
// 判断是否使用了某个特性
if (type.IsDefined(typeof(CustomAttribute), false))
{
Console.WriteLine("该类型应用了Custom特性");
}

输出:

1
该类型应用了Custom特性

获取某个类型使用的所有特性

type.GetCustomAttributes()​ 可以获取此类型所有特性对象

  • 参数一:是否要顺着继承链向上搜索父类使用的特性(属性和事件忽略此参数)
  • 返回值:使用的所有特性对象数组

默认返回的是 object[]​,可以转成对应的特性类型对象,并执行特性中的成员变量和方法

1
2
3
4
5
6
7
8
9
10
11
12
TestClass obj = new TestClass();
Type type = obj.GetType();
object[] attributes = type.GetCustomAttributes(true);
for (int i = 0; i < attributes.Length; i++)
{
if (attributes[i] is CustomAttribute)
{
// 获取此类型拥有的特性的信息
Console.WriteLine((attributes[i] as CustomAttribute)?.info);
(attributes[i] as CustomAttribute)?.TestFun();
}
}

输出:

1
2
这是 MyClass 类
特性的方法被执行了!type.GetCustomAttribute()

获取某个类型使用的某个特性

type.GetCustomAttribute()​ 可以获取此类型特定的特性对象,它有泛型参数重载和 Type​ 参数重载

  • 如果使用泛型函数,则直接在泛型参数内填入特性名即可,返回值也是对应的特性
  • 如果使用 Type​ 参数函数,需要传入特性对应的 Type​,返回值是 object
  • 泛型参数重载和 Type​ 参数重载都有一个 bool​ 参数,即是否要顺着继承链向上搜索父类使用的特性(属性和事件忽略此参数)

警告!如果这个特性被重复添加到这个类型上,那么使用此函数获取特性将会报错!

1
2
3
4
5
6
7
8
9
10
11
12
13
TestClass obj = new TestClass();
Type type = obj.GetType();
// 使用泛型尝试获取某个特性
CustomAttribute? attributeObj1 = type.GetCustomAttribute<CustomAttribute>(true);
Console.WriteLine(attributeObj1?.info);
attributeObj1?.TestFun();
// 使用Type尝试获取某个特性
object? attributeObj2 = type.GetCustomAttribute(typeof(CustomAttribute), true);
if (attributeObj2 != null && attributeObj2 is CustomAttribute)
{
Console.WriteLine((attributeObj2 as CustomAttribute)?.info);
(attributeObj2 as CustomAttribute)?.TestFun();
}

输出:

1
2
3
4
这是 TestClass 类
特性的方法被执行了!
这是 TestClass 类
特性的方法被执行了!

获取某个成员使用的特性

如果要获取类型中某个成员使用的特性,需要通过 Type​ 来获取 MemberInfo​(也可以是 FieldInfo​、MethodInfo​ 等等)
然后通过 MemberInfo​ 来调用 GetCustomAttribute()​ 或者 GetCustomAttributes()​,即可得到某个成员使用的特性

参数和 type.GetCustomAttribute()​ 与 type.GetCustomAttributes()​ 一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TestClass obj = new TestClass();
Type type = obj.GetType();
MemberInfo[] infos = type.GetMembers();
for (int i = 0; i < infos.Length; i++)
{
object[] attributes = infos[i].GetCustomAttributes(true);
for (int j = 0; j < attributes.Length; j++)
{
if (attributes[j] is CustomAttribute)
{
// 获取此类型拥有的特性的信息
Console.WriteLine((attributes[j] as CustomAttribute)?.info);
(attributes[j] as CustomAttribute)?.TestFun();
}
}
}

输出:

1
2
3
4
5
6
这是 TestClass.MyClass 方法
特性的方法被执行了!
这是 TestClass.Add 方法
特性的方法被执行了!
这是 TestClass.number 变量
特性的方法被执行了!

限制自定义特性的使用范围

通过为特性类添加 [AttributeUsage()]​ 特性,可以限制其使用范围

  • 参数一:validOn​ —— 特性能够添加在哪些地方(注意这里用位或 |​ 来表示两个条件都可以)

    此参数需要传入 AttributeTargets​ 枚举类型参数,可以传入多个,所有位或 |​ 连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    [Flags]
    public enum AttributeTargets
    {
    Assembly = 1, // 特性可以添加在程序集前
    Module = 2, // 特性可以添加在模块前
    Class = 4, // 特性可以添加在类声明前
    Struct = 8, // 特性可以添加在结构体声明前
    Enum = 16, // 特性可以添加在枚举声明前
    Constructor = 32, // 特性可以添加在构造函数声明前
    Method = 64, // 特性可以添加在方法声明前
    Property = 128, // 特性可以添加在属性声明前
    Field = 256, // 特性可以添加在成员变量声明前
    Event = 512, // 特性可以添加在事件声明前
    Interface = 1024, // 特性可以添加在接口声明前
    Parameter = 2048, // 特性可以添加在方法的参数前
    Delegate = 4096, // 特性可以添加在委托前
    ReturnValue = 8192, // 特性可以添加在方法的返回值前
    GenericParameter = 16384, // 特性可以添加在泛型参数前
    All = 32767 // 特性可以添加在所有的目标前
    }
    1
    2
    3
    4
    5
    6
    7
    8
    [AttributeUsage(AttributeTargets.Class)]
    class CustomAttribute : Attribute {}

    [Custom]
    class TestClass {}

    [Custom] // error: 特性“Custom”对此声明类型无效。它仅对“类”声明有效。
    struct TestStruct {}

此外,还有两个公共变量可以赋值:

  • 变量一:AllowMultiple​ —— 是否允许多个特性实例用在同一个目标上

    如果此变量为 true​,则此特性可以一次在同一目标上重复添加多个

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    [Custom("这是 TestClass 类", number = 10)]
    [Custom("重复添加的类")]
    class TestClass {}

    [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
    class CustomAttribute : Attribute
    {
    // 特性中的成员一般根据需求来写
    public string info;
    public int number;

    public CustomAttribute(string info)
    {
    this.info = info;
    }
    }
  • 变量二:Inherited​ —— 特性是否能被派生类和重写成员继承

    如果此变量为 true​,父类添加的特性,在子类同样有效

系统自带特性

过时特性

过时特性 [Obsolete]​,用于提示用户使用的方法等成员已经过时,建议使用新方法,一般加在函数前的特性

  • 参数一:message​,调用过时内容时提示内容
  • 参数二:error​,调用目标时是否报错(默认是警告)
1
2
[Obsolete]
class OldClass {}
1
OldClass old = new OldClass();        // warning: “OldClass”已过时

调用者信息特性

以下特性只能为参数使用,且使用调用者信息特性的参数必须初始化为默认值!调用者信息特性会自动将对应参数的默认值改为相应的数据

  • 得到哪个文件调用需要对参数添加 [CallerFilePath]​ 特性
  • 得到哪一行调用需要对参数添加 [CallerLineNumber]​ 特性
  • 得到哪个函数调用对参数添加 [CallerMemberName]​ 特性

需要 using System.Runtime.CompilerServices;​,以上的特性一般作为函数参数的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System.Runtime.CompilerServices;

class Program
{
static void Main()
{
TestClass obj = new TestClass();
obj.SpeakCaller("测试调用者信息特性");
}
}

class TestClass
{
public void SpeakCaller(
string str,
[CallerFilePath] string fileName = "",
[CallerLineNumber] int line = 0,
[CallerMemberName] string target = "")
{
Console.WriteLine("说话内容:" + str);
Console.WriteLine("调用者所在文件:" + fileName);
Console.WriteLine("调用者所在行数:" + line);
Console.WriteLine("调用者函数:" + target);
}
}

输出:

1
2
3
4
说话内容:测试调用者信息特性
调用者所在文件:e:\CodeField\CSharpProjects\CSharpTest\Program.cs
调用者所在行数:8
调用者函数:Main

条件编译特性

条件编译特性 [Conditional]​,它会和预处理指令 #define​ 配合使用,
此特性需要传入 #define​ 定义的符号,如果此符号未定义,则该函数不调用
需要引用 using System.Diagnostics;​,主要可以用在一些调试代码上,有时想执行有时不想执行的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define Debug
using System.Runtime.CompilerServices;

class Program
{
static void Main()
{
DebugFunc();
}

[Conditional("Debug")]
static void DebugFunc()
{
Console.WriteLine("调试函数执行");
}
}

输出(如果 Debug​ 未定义,则不会输出此内容):

1
调试函数执行

外部Dll包特性

[DllImport]​ 用来标记非 .NET(C#) 的函数,表明该函数在一个外部的 dll​ 文件中定义,此特性需要传入 dll​ 文件所在路径
一般用来调用 C 和 C++ 的 dll​ 包写好的方法,需要引用 using System.Runtime.InteropServices;

固定写法如下(假设同级目录下存在一个 Test.dll​ 文件):

1
2
3
4
5
class Program
{
[DllImport("Test.dll")]
public static extern int Add(int a, int b);
}