unity的C#学习——特性(添加元数据)和反射(获取元数据)

文章目录

  • C# 中的特性(Attributes)
    • 1、特性的语法
    • 2、预定义特性
      • 2.1 Obsolete 特性
      • 2.2 Serializable 特性
      • 2.3 Conditional 特性
      • 补充:Conditional 特性对比 #if 指令
      • 2.4 DllImport 特性
      • 2.5 AttributeUsage 特性
    • 3、自定义特性
      • 3.1 自定义特性的创建
      • 3.2 自定义特性的使用
      • 3.3 自定义特性的案例
  • C# 反射(Reflection)
    • 1、反射的用途和优缺点
    • 2、相关概念:程序集和元数据
    • 3、Type类及其常用方法
    • 4、审查并实例化各种类型
    • 5、查看特性添加的元数据


C# 中的特性(Attributes)

C# 中的特性(Attributes)是一种用于为类型、成员、程序集等元素添加元数据的机制,如编译器指令和注释、描述、方法、类等其他信息。.Net 框架提供了两种类型的特性:预定义特性自定义特性

使用特性可以给代码添加更多的信息和功能,同时也可以提高代码的可读性和可维护性。在本文中将对 C# 中的特性进行详细介绍。


1、特性的语法

特性是一种声明式编程的机制,它允许开发者在代码中添加元数据。这些元数据可以用来描述类型、成员、程序集等元素的属性、行为和用途等信息

在 C# 中,特性的语法很简单,它由以下几个部分组成:

  • 方括号([]):用于包围特性的名称和属性。
  • 特性名称:用于标识特性的名称,它可以是一个标识符或字符串。
  • 特性参数:用于指定特性的属性值。如果特性没有属性值,那么可以省略这部分内容。
[attribute_name(positional_parameters, name_parameter = value, ...)]
element

其中 attribute_name 表示特性的名称,positional_parameters 表示位置参数,name_parameter = value 表示命名参数。

位置参数和命名参数都可以用来为特性传递参数值,不同之处在于它们的语法形式和使用方式。

  • 位置参数通常用于指定特性的必选参数
  • 命名参数则可以用于指定特性的可选参数
  • 下面是一个使用特性位置参数指定可选参数的例子:
[Obsolete("This method is deprecated.")]
public void MyMethod() { ... }

在这个例子中,我们使用 Obsolete 特性来标记 MyMethod 方法,表示该方法已经被废弃。Obsolete 特性的位置参数指定了废弃信息的文本。当其他代码调用该方法时,编译器会发出警告,提示开发者不要再使用该方法,以免造成错误。

  • 除了位置参数之外,特性还支持命名参数。例如:
[CustomAttribute(Name = "MyCustomAttribute", Version = "1.0")]
public class MyClass { ... }

在这个例子中,我们使用 CustomAttribute 自定义特性,并且为该特性指定了两个命名参数 NameVersion,它们分别对应于 CustomAttribute 类中的两个公共属性。

注意,当使用命名参数时,参数名必须与特性类中(定义的)属性名相同,而且参数值必须与(申明的)属性类型兼容

  • 如果特性没有参数,那么可以直接使用特性名称:
[Serializable]
public class MyClass { ... }

在这个例子中,Serializable 是一个没有参数的特性名称,它被应用到了 MyClass 类上面。这个特性告诉编译器,这个类可以被序列化,因此可以被保存到文件或网络中,然后再进行反序列化。


2、预定义特性

预定义特性是由 .NET 框架提供的、预先定义好的特性,用于标记和描述程序中的各种元素,例如上面使用到的 Serializable 特性和 Obsolete 特性。

通常预定义特性可以在不引入任何其他命名空间的情况下直接使用,因为它们是C#语言本身的一部分。下面是C#中常见预定义特性的详细语法和应用举例:

需要注意的是,虽然这些特性不需要导入命名空间,但是如果要使用特性的参数,还是需要使用相应的命名空间。

  • 例如 DllImport 特性的参数类型需要using System.Runtime.InteropServices命名空间。

2.1 Obsolete 特性

Obsolete,特性用于标记某个程序元素已过时,即标记不应再使用的代码元素。详细语法如下:

[Obsolete("message")]
[Obsolete("message", iserror: true)]

其中包含两个位置参数:

  • message 参数是一个字符串,表示有关已过时元素的消息。
  • iserror 参数是一个可选的布尔值,指示编译器是否应将该警告视为错误。如果该值为 true,编译器应把该项目的使用当作一个错误,默认值是 false(编译器生成一个警告)。

应用举例:

using System;
public class MyClass
{
   [Obsolete("Don't use OldMethod, use NewMethod instead", true)]
   static void OldMethod()
   {
      Console.WriteLine("It is the old method");
   }
   static void NewMethod()
   {
      Console.WriteLine("It is the new method");
   }
   public static void Main()
   {
      OldMethod(); // 在程序入口内使用旧的静态方法
   }
}

当我们尝试编译该程序时,编译器会给出一个错误消息说明:

Don’t use OldMethod, use NewMethod instead

2.2 Serializable 特性

Serializable 特性可以用于标记类、结构体、枚举和委托类型的成员。通过标记这些成员,表示它们可以被序列化,也就是可以被转换成字节流并保存在文件、数据库等地方,便于在不同的程序之间进行数据传输和共享。

需要注意的是,为了让一个对象可以被序列化,它所属的类必须实现 Serializable 接口,即标记为可序列化。

  • 这里所说的实现接口不同于之前自己定义的接口,需要派生类继承并实现。
  • Serializable 是一个标记接口(marker interface),与自定义接口的继承不同,它并不包含任何方法或属性。标记接口的作用是为了告诉编译器和运行时环境,实现该接口的类具有某种特定的行为或属性。
  • 当类实现 Serializable 接口时,编译器会在编译时进行一些特定的检查,以确保该类可以安全地序列化和反序列化。

在使用 Serializable 特性标记某个类时,该类就已经实现了 Serializable 接口。详细语法如下:

[Serializable]

可以看到该特性是没有参数的。应用举例:

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        // 创建一个 Person 类的实例
        Person person = new Person { Name = "Alice", Age = 30 };

        // 将该实例序列化到文件中
        BinaryFormatter formatter = new BinaryFormatter();
        using (FileStream stream = new FileStream("person.bin", FileMode.Create))
        {
            formatter.Serialize(stream, person);
        }

        // 从文件中反序列化出该实例
        using (FileStream stream = new FileStream("person.bin", FileMode.Open))
        {
            Person deserializedPerson = (Person)formatter.Deserialize(stream);
            Console.WriteLine("Name: " + deserializedPerson.Name);
            Console.WriteLine("Age: " + deserializedPerson.Age);
        }
    }
}

2.3 Conditional 特性

Conditional 特性用于标记某个方法或属性只在指定的编译器条件符合时才可用。详细语法如下:

[Conditional("symbol")]

其中 symbol 参数是一个字符串,表示指定的条件值。

  • 比如符号常量 DebugTrace,此时被标记的成员能否被执行取决于上述符号常量是否被定义(通过预处理器指令 #define Debug#define Trace )。

应用举例:

// 通过预处理器指令,定义了一个 DEBUG 符号常量
#define DEBUG

using System;
using System.Diagnostics;

// 定义了一个名为 Myclass 的公共类
public class Myclass
{
    // ConditionalAttribute是一个预定义特性,
    // 它指定了只有在 DEBUG 条件编译指令被定义时才能使用 Message 方法
    [Conditional("DEBUG")]
    public static void Message(string msg)
    {
        // 输出消息
        Console.WriteLine(msg);
    }
}

// 定义了一个名为Test的类
class Test
{
    // 定义一个名为function1的静态方法
    static void function1()
    {
        // 在控制台输出消息"In Function 1.",因为 DEBUG 条件编译指令被定义了
        Myclass.Message("In Function 1.");
        // 调用function2方法
        function2();
    }

    // 定义一个名为function2的静态方法
    static void function2()
    {
        // 在控制台输出消息"In Function 2.",因为DEBUG条件编译指令被定义了
        Myclass.Message("In Function 2.");
    }

    // 程序的入口点
    public static void Main()
    {
        // 在控制台输出消息"In Main function.",因为DEBUG条件编译指令被定义了
        Myclass.Message("In Main function.");
        // 调用function1方法
        function1();
        // 等待用户按下任意键退出程序
        Console.ReadKey();
    }
}

当上面的代码被编译和执行时,它会产生下列结果:

In Main function.
In Function 1.
In Function 2.

补充:Conditional 特性对比 #if 指令

回想起之前预处理器指令部分的内容,你可能会好奇既然 Conditional 特性和 #if 指令都可以用于控制代码的编译和执行,那它们之间有什么不同之处?主要有以下三个方面:

  • 首先,Conditional 特性只能用于方法、属性或事件等成员级别,而 #if 指令可以用于任何代码块级别,包括整个类和命名空间。也就是说,使用 Conditional 特性的方法只有在特定条件下才会被编译,而使用 #if 指令的代码块可以完全排除在编译之外。

  • 其次,Conditional 特性所指定的条件是字符串形式的,而 #if 指令所指定的条件是一个布尔表达式。这意味着,使用 Conditional 特性时,条件通常是一些编译器预定义的符号,如上例中的 DEBUG,而使用 #if 指令时,条件可以是任何布尔表达式,如 #if (DEBUG && !RELEASE)。

  • 最后,Conditional 特性和 #if 指令所起到的作用也有所不同。Conditional 特性用于指示编译器在特定条件下是否应该包含该成员,而 #if 指令用于在编译时包含或排除整个代码块。也就是说,使用 Conditional 特性的成员在编译后仍然存在,只是不能被调用或执行,而使用 #if 指令的代码块在编译后完全消失,不会在最终的可执行程序中出现。

因此,可以根据具体的需求和场景来选择使用 Conditional 特性或 #if 指令。如果需要在编译时动态控制方法或属性是否被包含在可执行程序中,可以使用 Conditional 特性。如果需要在编译时完全排除一些代码块,可以使用 #if 指令。

2.4 DllImport 特性

DllImport 特性用于标记一个静态方法,以指示该方法是在一个外部动态链接库中实现的。使用 DllImport 特性可以方便地将 C# 程序与其他编程语言编写的动态链接库集成在一起,如C、C++、Pascal等。语法如下:

[DllImport(
    "dllname",
    CallingConvention = callingconvention,
    CharSet = charset,
    EntryPoint = entrypoint,
    SetLastError = setlasterror,
    ExactSpelling = exactspelling,
    PreserveSig = preservesig)]

其中:

  • dllname:包含外部函数的库的名称。可以包括完整的路径,或者只是文件名。
  • CallingConvention:指定使用哪种调用约定,如 Cdecl、Stdcall、Thiscall 等。
  • CharSet:指定函数参数和返回值所使用的字符集,如 Ansi、Unicode 或 Auto。
  • EntryPoint:可选参数,是被导入函数的名称。如果不指定,则使用方法的名称作为入口点。
  • ExactSpelling:指示编译器在外部函数名中是否应包含修饰符。
  • PreserveSig:指示编译器是否将外部函数的 HRESULT 作为返回值传递给调用方。
  • SetLastError:指示编译器在执行 Win32 API 调用时是否应设置 Win32 错误代码。
  • ThrowOnUnmappableChar:指示编译器在遇到无法映射到 Unicode 字符集的字符时是否引发异常。

通常用法为以下格式:

[DllImport("<库文件的名称或路径>", CallingConvention=CallingConvention.<调用约定类型>)]
<访问修饰符> static extern <返回类型> <方法名称==外部函数名称>(<参数类型 参数名>,<参数类型 参数名>,...);
  • 相当于在当前程序声明了一个外部实现的方法,我们设置的方法名称是其在内部的代号。但要注意,当没有指定 EntryPoint 参数时,方法名必须和要引入的函数名相同。
  • 注意必须申明为静态方法(因为只需要分配一次内存),且返回类型前要添加 extern 外部修饰符。

例如,假设有一个C语言编写的动态链接库example.dll,其中包含一个名为add的函数,其返回值为int型,有两个int型参数。要在C#程序中调用该函数,可以使用如下代码:

// DllImport比较特殊,在使用参数时需要引入命名空间
using System.Runtime.InteropServices
class Test
{
	[DllImport("example.dll", CallingConvention=CallingConvention.Cdecl)]
	public static extern int add(int a, int b); // 该方法在外部实现,内部可以直接用
	static void Main()
	{
		int result = add(1, 2); // 通过内部定义的方法名,使用外部方法
		Console.WriteLine(result);
	}
}

需要注意,在使用 DllImport 特性时,需要确保指定的 dll_name 与实际动态链接库的名称或路径一致,否则会报错。同时在使用本机(外部)函数时,需要注意传递参数的类型和顺序,以及返回值的类型。

2.5 AttributeUsage 特性

AttributeUsage 特性用于指定自定义特性的用法,例如可以指定自定义特性可以应用于哪些元素、是否允许多个实例等。

详细语法:

[AttributeUsage(
    validon,
    AllowMultiple = allowmultiple,
    Inherited = inherited)]

其中:

  • validon 参数是一个枚举值,表示自定义特性可用于哪些程序元素,不同的元素间用 | 分隔。
  • allowmultiple 参数是一个可选的布尔值,指示是否允许将多个该特性应用于同一程序元素,默认值是 false。
  • inherited 参数是一个可选的布尔值,指示是否允许派生类继承该特性,默认值是 false。

关于自定义特性将会在下一部分详细介绍,下面是一个简单的示例:

[AttributeUsage(
    AttributeTargets.Method | AttributeTargets.Class,
    AllowMultiple = true)]
public class MyAttribute : Attribute { ... }

3、自定义特性

除了使用 .NET Framework 中的现有特性之外,C# 还允许开发者创建自定义特性。自定义特性可以用于为代码添加更多的元数据,同时也可以帮助开发者实现更复杂的功能。

3.1 自定义特性的创建

为了创建自定义特性,需要使用 System.Attribute 类作为基类,并且需要添加上面提到的 AttributeUsage 特性来指定特性的使用方式。例如:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class CustomAttribute : Attribute // 特性其实也是一种类
{
    public CustomAttribute(string message) // 构造函数
    {
        this.Message = message; // 使用this关键字引用只读属性,实现初始化赋值
    }

    public string Message { get; } // 定义只读属性
}

在这个例子中,我们创建了一个名为 CustomAttribute 的自定义特性,并且添加了一个构造函数和一个属性。AttributeUsage 特性指定了该特性可以应用到 ClassStruct 类型上面。

3.2 自定义特性的使用

使用自定义特性和 .NET Framework 中的特性是类似的。例如:

[CustomAttribute("This is a custom attribute.")]
public class MyClass { ... }

在这个例子中,我们使用 Custom 自定义特性来修饰 MyClass 类型,并且指定了一个字符串参数。

3.3 自定义特性的案例

让我们构建一个名为 DeBugInfo 的自定义特性,该特性将存储调试程序获得的信息。它存储下面的信息:

  • bug 的代码编号
  • 辨认该 bug 的开发人员名字
  • 最后一次审查该代码的日期
  • 一个存储了开发人员标记的字符串消息

我们的 DeBugInfo 类将带有三个用于存储前三个信息的私有属性和一个用于存储消息的公有属性。

  • 所以 bug 编号、开发人员名字和审查日期将是 DeBugInfo 类的必需的位置参数
  • 字符串消息将是一个可选的命名参数

每个特性(类)必须至少有一个构造函数,位置参数应通过构造函数传递。下面的代码演示了 DeBugInfo 类的实现:

// 给 DeBugInfo 类添加自定义特性,用于记录调试信息
[AttributeUsage(AttributeTargets.Class | // 可以应用于类
                AttributeTargets.Constructor | // 可以应用于构造函数
                AttributeTargets.Field |   // 可以应用于字段
                AttributeTargets.Method |  // 可以应用于方法
                AttributeTargets.Property, // 可以应用于属性
    			AllowMultiple = true)]     // 允许多次应用此特性
public class DeBugInfo : System.Attribute  // 继承自 System.Attribute
{
    // 构造函数,用于设置调试信息
    public DeBugInfo(int bg, string dev, string d)
    {
        this.bugNo = bg; // 记录 bug 编号
        this.developer = dev; // 记录开发者名字
        this.lastReview = d; // 记录最后一次检查日期
    }

    // 私有字段,记录 bug 编号,开发者和最后一次检查日期
    private int bugNo;
    private string developer;
    private string lastReview;

    // 公共属性,用于访问 bug 编号,开发者和最后一次检查日期
    public int BugNo
    {
        get { return bugNo; } // 获取 bug 编号
    }
    public string Developer
    {
        get { return developer; } // 获取开发者
    }
    public string LastReview
    {
        get { return lastReview; } // 获取最后一次检查日期
    }

    // 公共属性,用于设置和获取调试信息的消息
    public string Message { get; set; }
}

在完成自定义特性的创建后,我们就可以像使用预定义特性一样使用它:

// 对同一个类多次应用自定义的 DeBugInfo 特性
[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")]
[DeBugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")]
// 定义 Rectangle 类
class Rectangle
{
    // 成员变量
    protected double length;
    protected double width;

    // 构造函数
    public Rectangle(double l, double w)
    {
        length = l;
        width = w;
    }

    // 指定 DeBugInfo 特性应用于 GetArea 方法
    [DeBugInfo(55, "Zara Ali", "19/10/2012", Message = "Return type mismatch")]
    // 计算矩形的面积
    public double GetArea()
    {
        return length * width;
    }

    // 指定 DeBugInfo 特性应用于 Display 方法
    [DeBugInfo(56, "Zara Ali", "19/10/2012")]
    // 显示矩形的信息
    public void Display()
    {
        Console.WriteLine("Length: {0}", length);
        Console.WriteLine("Width: {0}", width);
        Console.WriteLine("Area: {0}", GetArea());
    }
}

在上面的代码中,我们成功在创建 Rectangle 类及其方法的过程中,使用自定义特性 DeBugInfo 为这些成员添加了元数据——调试信息。如果想要获取这些信息并输出,则需要使用反射来实现。


C# 反射(Reflection)

C#的反射是一种机制,可以在运行时获取程序集的元数据,以及程序集中定义的类型、方法、属性、字段等信息,而不需要事先知道它们的名称或类型。

换句话说,反射是指 程序可以访问、检测和修改它本身的状态或行为 的一种能力。


1、反射的用途和优缺点

反射在很多场景下都非常有用。比如在开发通用的、可扩展的框架或库时,或者在需要动态地加载、配置或执行程序集的情况下:

  • 它允许在运行时查看特性(attribute)信息,即元数据。(见第5部分)
  • 它允许审查集合中的各种类型,以及实例化这些类型。(见第4部分)
  • 它允许延迟绑定的方法和属性。
  • 它允许在运行时创建新类型,然后使用这些类型执行一些任务。

反射的优点

  1. 反射提高了程序的灵活性和扩展性。
  2. 降低耦合性,提高自适应能力。
  3. 它允许程序创建和控制任何类的对象,无需提前硬编码目标类。

反射的缺点

  1. 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。
  2. 使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。

2、相关概念:程序集和元数据

在C#中,程序集是一组相关的代码和资源文件,可以是可执行文件(.exe)或动态链接库(.dll)。程序集包含元数据,即程序集的描述信息,包括程序集的名称、版本、公共密钥、成员信息等

反射可以通过程序集中的元数据来获取程序集的信息。


3、Type类及其常用方法

C#中的Type类是非常重要的,因为它是反射机制的核心Type 类位于System.Reflection 命名空间中,用于表示类型(类、结构、接口、委托、枚举等)的信息。

通过Type类,可以在运行时获得类型的元数据,包括成员(属性、方法、字段、事件等)信息和继承关系等。以下是 Type 类部分常用的方法和属性:

  • GetType()方法:返回当前对象的Type实例

  • typeof()运算符:获取指定类型的Type实例

  • GetMembers()方法:返回类型的所有成员,如果通过参数指定了成员名称,将返回对应的子类实例

  • GetMethods()方法:返回类型的所有公共方法,如果通过参数指定了方法名称,则返回该方法的MethodInfo实例

  • GetProperties()方法:返回类型的所有公共属性,如果通过参数指定了属性名称,则返回该成员的PropertyInfo实例

  • GetFields()方法:返回类型的所有公共字段,如果通过参数指定了字段名称,则返回该成员的FieldInfo实例

  • IsClass属性:判断类型是否为类。

  • IsInterface属性:判断类型是否为接口。

  • IsValueType属性:判断类型是否为值类型。

可以看出,反射还提供了许多其他类,如 MethodInfoFieldInfoPropertyInfo 等,用于表示方法、字段、属性等成员信息。

  • 这些类都继承自 MemberInfo 类,而 MemberInfo 类又是继承自 type 类的抽象类,因此要注意不存在 MemberInfo 实例
  • 使用反射提供的类,都需要引入 System.Reflection 命名空间。

下面是一些 Type 类中的属性和方法的使用语法(并不完全对照上面):

using System.Reflection

//获取类型的元数据信息,存储在返回的Type对象中
Type type = typeof(MyClass);    // 法 1
Type type = MyClass.GetType();  // 法 2

//获取类的名称
string className = type.Name;

//获取类所在的程序集信息
Assembly assembly = type.Assembly;

//获取类的公共构造函数
ConstructorInfo[] constructors = type.GetConstructors();

//获取类的所有公共成员(字段、属性、方法等)
MemberInfo[] members = type.GetMembers();

//获取类的公共字段信息
FieldInfo[] fields = type.GetFields();

//获取类的公共属性信息
PropertyInfo[] properties = type.GetProperties();

//获取类的公共方法信息
MethodInfo[] methods = type.GetMethods();

//获取类的公共事件信息
EventInfo[] events = type.GetEvents();

//获取类的公共嵌套类型信息
Type[] nestedTypes = type.GetNestedTypes();

//获取类的自定义属性信息
object[] attributes = type.GetCustomAttributes();

需要注意的是,这些语法需要在已知类的类型的情况下才能使用,即需要在编译期知道类的类型。


4、审查并实例化各种类型

以下是一个简单的 C# 程序,演示了如何使用 Type 类的方法生成数据类型的 Type 实例,并通过 Type 实例审查、输出对应数据类型的元数据——方法和属性信息:

using System;
using System.Reflection

namespace TypeExample
{
    class Program
    {
        static void Main(string[] args)
        {
            // 使用typeof()获取string类型的Type实例
            Type stringType = typeof(string);

            // 使用GetType()获取int类型的Type实例
            int num = 10;
            Type intType = num.GetType();

            // 输出类型的名称和是否为值类型
            Console.WriteLine("stringType is a value type: " + stringType.IsValueType);
            Console.WriteLine("intType is a value type: " + intType.IsValueType);

            // 输出类型的成员
            Console.WriteLine("Members of stringType:");
            foreach(var member in stringType.GetMembers())
            {
                Console.WriteLine(member.Name);
            }

            // 输出类型的方法
            Console.WriteLine("Methods of intType:");
            foreach(var method in intType.GetMethods())
            {
                Console.WriteLine(method.Name);
            }

            // 输出类型的属性
            Console.WriteLine("Properties of stringType:");
            foreach(var property in stringType.GetProperties())
            {
                Console.WriteLine(property.Name);
            }

            // 输出类型的字段
            Console.WriteLine("Fields of intType:");
            foreach(var field in intType.GetFields())
            {
                Console.WriteLine(field.Name);
            }
        }
    }
}

运行上述程序,输出如下:

stringType is a value type: False
intType is a value type: True


Members of stringType:
get_Chars
Clone
CompareTo
CompareTo
Concat
Contains
CopyTo
EndsWith
…(太长了后续省略)


Properties of stringType:
Length
Chars
IsNormalized
Normalize
ToLower
ToLowerInvariant
ToUpper
ToUpperInvariant
Trim
TrimEnd
TrimStart


Methods of intType:
GetType
ToString
Equals
GetHashCode
GetTypeCode
CompareTo
CompareTo
Equals
Max
Min
GetType
Parse
Parse
TryParse
TryParse
Fields of intType:
MaxValue
MinValue


5、查看特性添加的元数据

我们已经在上文中提到:使用反射(Reflection)可以查看特性(attribute)信息,即我们为成员添加的元数据。

  • 要获取一个元素的特性,可以使用 Type 实例的 GetCustomAttributes() 方法。这个方法的参数是一个Type类型的参数,用于指定要获取的特性类型。例如,要获取一个类的特性,可以使用以下代码:
using System.Reflection

// 获取 MyClass 类型的 Type实例
Type type = typeof(MyClass);
// 通过 Type 实例的方法获取 MyAttribute 特性,并存储到对象数组
object[] attributes = type.GetCustomAttributes(typeof(MyAttribute), true);

其中,MyClass是要获取特性的类,MyAttribute是特性类型,true表示要搜索派生类中的特性。这个代码将返回一个对象数组,其中包含了所有被 MyClass 类标记的 MyAttribute 特性。

  • 如果要获取成员的特性,可以使用 类成员Info 对象的 GetCustomAttributes() 方法。例如,要获取一个类的属性的特性,可以使用以下代码:
using System.Reflection

// 获取 MyClass 类型的 Type 实例
Type type = typeof(MyClass);
// 获取 MyProperty 属性的 PropertyInfo 实例
// 这里要使用 GetProperty()方法获取属性信息 ,因为类属性本来要在该类实例化后通过实例对象访问
PropertyInfo property = type.GetProperty("MyProperty");
// 通过 PropertyInfo 实例的方法获取 MyAttribute 特性,并存储到对象数组
object[] attributes = property.GetCustomAttributes(typeof(MyAttribute), true);

其中,MyClass 是要获取属性的类,MyProperty 是属性名称,MyAttribute 是特性类型,true 表示要搜索派生类中的特性。这个代码将返回一个对象数组,其中包含了所有被 MyProperty 属性标记的 MyAttribute 特性。

从上面两段代码可以知道,通过反射查看特性其实就是使用 Type 类的 GetCustomAttributes 方法实现,因为其他的反射相关类都是直接或间接继承 Type 。 它的语法如下:

public virtual object[] GetCustomAttributes(Type attributeType, bool inherit);

其中:

  • Type attributeType 是一个Type类型参数,用于指定要匹配的自定义特性
  • inherit 是一个布尔类型参数,用于指定是否搜索继承链。如果该参数为 true,则 GetCustomAttributes 方法会将调用该方法的 Type 对象的基类添加进搜索范围。

介绍完了反射查看特性的方法后,我们将使用在特性部分3.3节——自定义特性中创建的 DeBugInfo 特性,并使用反射来读取 Rectangle 类中的元数据。下面是完整的代码:

using System;
using System.Reflection;
namespace BugFixApplication
{
	// 给 DeBugInfo 类添加自定义特性,用于记录调试信息
	[AttributeUsage(AttributeTargets.Class | // 可以应用于类
	                AttributeTargets.Constructor | // 可以应用于构造函数
	                AttributeTargets.Field |   // 可以应用于字段
	                AttributeTargets.Method |  // 可以应用于方法
	                AttributeTargets.Property, // 可以应用于属性
	    			AllowMultiple = true)]     // 允许多次应用此特性
	public class DeBugInfo : System.Attribute  // 继承自 System.Attribute
	{
	    // 构造函数,用于设置调试信息
	    public DeBugInfo(int bg, string dev, string d)
	    {
	        this.bugNo = bg; // 记录 bug 编号
	        this.developer = dev; // 记录开发者名字
	        this.lastReview = d; // 记录最后一次检查日期
	    }
	
	    // 私有字段,记录 bug 编号,开发者和最后一次检查日期
	    private int bugNo;
	    private string developer;
	    private string lastReview;
	
	    // 公共属性,用于访问 bug 编号,开发者和最后一次检查日期
	    public int BugNo
	    {
	        get { return bugNo; } // 获取 bug 编号
	    }
	    public string Developer
	    {
	        get { return developer; } // 获取开发者
	    }
	    public string LastReview
	    {
	        get { return lastReview; } // 获取最后一次检查日期
	    }
	
	    // 公共属性,用于设置和获取调试信息的消息
	    public string Message { get; set; }
	}
	
	// 对同一个类多次应用自定义的 DeBugInfo 特性
	[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")]
	[DeBugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")]
	// 定义 Rectangle 类
	class Rectangle
	{
	    // 成员变量
	    protected double length;
	    protected double width;
	
	    // 构造函数
	    public Rectangle(double l, double w)
	    {
	        length = l;
	        width = w;
	    }
	
	    // 指定 DeBugInfo 特性应用于 GetArea 方法
	    [DeBugInfo(55, "Zara Ali", "19/10/2012", Message = "Return type mismatch")]
	    // 计算矩形的面积
	    public double GetArea()
	    {
	        return length * width;
	    }
	
	    // 指定 DeBugInfo 特性应用于 Display 方法
	    [DeBugInfo(56, "Zara Ali", "19/10/2012")]
	    // 显示矩形的信息
	    public void Display()
	    {
	        Console.WriteLine("Length: {0}", length);
	        Console.WriteLine("Width: {0}", width);
	        Console.WriteLine("Area: {0}", GetArea());
	    }
	}
/*----------------------------------- 分割线,下面是新增代码 -----------------------------------*/ 
	// 定义 ExecuteRectangle 类
	class ExecuteRectangle
	{
	    // 定义程序的入口方法
	    static void Main(string[] args)
	    {
	        // 创建一个 Rectangle 类型的对象 r,传入长和宽
	        Rectangle r = new Rectangle(4.5, 7.5);
	        
	        // 调用 r 对象的 Display 方法显示矩形的长和宽
	        r.Display();
	        
	        // 获取 Rectangle 类型的 Type 对象
	        Type type = typeof(Rectangle);
	        
	        // 遍历 Rectangle 类型的特性,获取 DeBugInfo 类型的特性
	        foreach (Object attributes in type.GetCustomAttributes(false))
	        {
	            // 将特性对象强制转换为 DeBugInfo 类型,并进行输出
	            DeBugInfo dbi = (DeBugInfo)attributes;
	            if (null != dbi)
	            {
	                Console.WriteLine("Bug no: {0}", dbi.BugNo);
	                Console.WriteLine("Developer: {0}", dbi.Developer);
	                Console.WriteLine("Last Reviewed: {0}", dbi.LastReview);
	                Console.WriteLine("Remarks: {0}", dbi.Message);
	            }
	        }
	
	        // 遍历 Rectangle 类型的所有方法,获取方法上的特性
	        foreach (MethodInfo m in type.GetMethods())
	        {
	            foreach (Attribute a in m.GetCustomAttributes(true))
	            {
	                // 将特性对象强制转换为 DeBugInfo 类型,并进行输出
	                DeBugInfo dbi = (DeBugInfo)a;
	                if (null != dbi)
	                {
	                    Console.WriteLine("Bug no: {0}, for Method: {1}", dbi.BugNo, m.Name);
	                    Console.WriteLine("Developer: {0}", dbi.Developer);
	                    Console.WriteLine("Last Reviewed: {0}", dbi.LastReview);
	                    Console.WriteLine("Remarks: {0}", dbi.Message);
	                }
	            }
	        }
	
	        // 等待用户输入,防止控制台窗口关闭
	        Console.ReadLine();
	    }
	}
}

当上面的代码被编译和执行时,它会产生下列结果:

Length: 4.5
Width: 7.5
Area: 33.75


Bug No: 49
Developer: Nuha Ali
Last Reviewed: 10/10/2012
Remarks: Unused variable
Bug No: 45
Developer: Zara Ali
Last Reviewed: 12/8/2012
Remarks: Return type mismatch


Bug No: 55, for Method: GetArea
Developer: Zara Ali
Last Reviewed: 19/10/2012
Remarks: Return type mismatch
Bug No: 56, for Method: Display
Developer: Zara Ali
Last Reviewed: 19/10/2012
Remarks:

你可能感兴趣的:(unity的c#之旅,unity,c#,学习,开发语言)