35、.NET 中的特性与反射:全面解析与实践

.NET 中的特性与反射:全面解析与实践

1. 特性与反射概述

在 .NET 应用程序中,包含代码、数据和元数据。元数据是关于数据的信息,如类型、代码、程序集等,它与程序一同存储。特性和反射是处理元数据的重要机制。

特性是向程序添加元数据的一种方式,例如编译器指令以及关于数据、方法和类的其他信息。特性被插入到元数据中,可以通过 ILDASM 等元数据读取工具查看。反射则是程序读取自身或其他程序元数据的过程,程序可以从反射的程序集中提取元数据,并利用这些元数据向用户提供信息或修改程序行为。

2. 特性的类型与使用

特性有多种类型,部分由 CLR 或框架提供,同时也可以创建自定义特性。

2.1 特性的目标

特性可以应用于不同的目标,如程序集、类、接口、类成员等。可能的特性目标在 AttributeTargets 枚举中声明,具体如下表所示:
| 成员名称 | 用途 |
| ---- | ---- |
| All | 应用于以下任何元素:程序集、类、构造函数、委托、枚举、事件、字段、接口、方法、模块、参数、属性、返回值或结构体 |
| Assembly | 应用于程序集本身 |
| Class | 应用于类 |
| Constructor | 应用于给定的构造函数 |
| Delegate | 应用于委托 |
| Enum | 应用于枚举 |
| Event | 应用于事件 |
| Field | 应用于字段 |
| Interface | 应用于接口 |
| Method | 应用于方法 |
| Module | 应用于单个模块 |
| Parameter | 应用于方法的参数 |
| Property | 应用于属性(如果实现了 get 和 set 方法) |
| ReturnValue | 应用于返回值 |
| Struct | 应用于结构体 |

2.2 应用特性

将特性应用于目标时,需将其放在方括号中,并紧挨着目标项(程序集特性需放在文件顶部)。可以通过堆叠或用逗号分隔的方式组合特性,例如:

[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile(".\\keyFile.snk")]
// 或者
[assembly: AssemblyDelaySign(false), assembly: AssemblyKeyFile(".\\keyFile.snk")]

程序集特性必须放在所有 using 语句之后、任何代码之前。许多特性用于与 COM 进行互操作, System.Reflection 命名空间提供了多种特性,如程序集特性、配置特性和版本特性等。在日常 C# 编程中,如果不与 COM 交互,常用的特性之一是 [Serializable] ,只需将其添加到类中,即可确保该类可以序列化到磁盘或网络:

[Serializable]
class MySerializableClass
3. 自定义特性

自定义特性可以根据需求在运行时使用。例如,开发组织想要跟踪 bug 修复情况,可创建自定义特性来关联代码中的修复与数据库中的 bug 报告。

3.1 声明自定义特性

自定义特性通过从 System.Attribute 派生新类来创建,同时需要使用 [AttributeUsage] 特性指定该特性可以应用的目标元素以及是否允许一个元素有多个该特性实例:

[AttributeUsage(AttributeTargets.Class |
                AttributeTargets.Constructor |
                AttributeTargets.Field |
                AttributeTargets.Method |
                AttributeTargets.Property,
                AllowMultiple = true)]
public class BugFixAttribute : System.Attribute
{
    public BugFixAttribute(int bugID, string programmer, string date)
    {
        this.BugID = bugID;
        this.Programmer = programmer;
        this.Date = date;
    }

    public int BugID { get; private set; }
    public string Date { get; private set; }
    public string Programmer { get; private set; }
    public string Comment { get; set; }
}
3.2 命名自定义特性

按照约定,自定义特性类名通常以 Attribute 结尾,但编译器支持使用较短的名称调用特性。例如:

[BugFix(123, "Jesse Liberty", "01/01/08", Comment="Off by one")]
3.3 构造自定义特性

特性有两种类型的参数:位置参数和命名参数。位置参数通过构造函数传递,必须按照构造函数中声明的顺序传递;命名参数通过字段或属性实现:

public BugFixAttribute(int bugID, string programmer, string date)
{
    this.BugID = bugID;
    this.Programmer = programmer;
    this.Date = date;
}
public string Comment { get; set; }
3.4 使用自定义特性

定义好特性后,将其放在目标元素之前即可使用。以下是一个使用 BugFixAttribute 的示例:

using System;
namespace CustomAttributes
{
    [AttributeUsage(AttributeTargets.Class |
                    AttributeTargets.Constructor |
                    AttributeTargets.Field |
                    AttributeTargets.Method |
                    AttributeTargets.Property,
                    AllowMultiple = true)]
    public class BugFixAttribute : System.Attribute
    {
        public BugFixAttribute(int bugID, string programmer, string date)
        {
            this.BugID = bugID;
            this.Programmer = programmer;
            this.Date = date;
        }

        public int BugID { get; private set; }
        public string Date { get; private set; }
        public string Programmer { get; private set; }
        public string Comment { get; set; }
    }

    [BugFixAttribute(121, "Jesse Liberty", "01/03/08")]
    [BugFixAttribute(107, "Jesse Liberty", "01/04/08",
                     Comment = "Fixed off by one errors")]
    public class MyMath
    {
        public double DoFunc1(double param1)
        {
            return param1 + DoFunc2(param1);
        }

        public double DoFunc2(double param1)
        {
            return param1 / 3;
        }
    }

    public class Tester
    {
        static void Main(string[] args)
        {
            MyMath mm = new MyMath();
            Console.WriteLine("Calling DoFunc(7). Result: {0}", mm.DoFunc1(7));
        }
    }
}

运行该程序,特性对输出没有影响,但通过 ILDASM 可以看到特性已存在于元数据中。

4. 反射的用途与操作

反射主要用于以下四个任务:
- 查看元数据:供工具和实用程序显示元数据。
- 类型发现:检查程序集中的类型并与之交互或实例化这些类型,可用于创建自定义脚本。
- 方法和属性的后期绑定:根据类型发现动态实例化对象并调用其属性和方法,也称为动态调用。
- 运行时创建类型:在运行时创建新类型并使用这些类型执行任务,当运行时创建的自定义类比编译时创建的通用代码运行速度快时适用。

4.1 查看元数据

使用 C# 反射支持读取 MyMath 类的元数据,步骤如下:
1. 获取 MemberInfo 对象:

System.Reflection.MemberInfo inf = typeof(MyMath);
  1. 调用 GetCustomAttributes 方法获取指定类型的特性数组:
object[] attributes;
attributes = inf.GetCustomAttributes(typeof(BugFixAttribute), false);
  1. 遍历特性数组并打印特性属性:
foreach (Object attribute in attributes)
{
    BugFixAttribute bfa = (BugFixAttribute)attribute;
    Console.WriteLine("\nBugID: {0}", bfa.BugID);
    Console.WriteLine("Programmer: {0}", bfa.Programmer);
    Console.WriteLine("Date: {0}", bfa.Date);
    Console.WriteLine("Comment: {0}", bfa.Comment);
}

完整代码示例如下:

public static void Main(string[] args)
{
    MyMath mm = new MyMath();
    Console.WriteLine("Calling DoFunc(7). Result: {0}", mm.DoFunc1(7));
    System.Reflection.MemberInfo inf = typeof(MyMath);
    object[] attributes;
    attributes = inf.GetCustomAttributes(typeof(BugFixAttribute), false);
    foreach (Object attribute in attributes)
    {
        BugFixAttribute bfa = (BugFixAttribute)attribute;
        Console.WriteLine("\nBugID: {0}", bfa.BugID);
        Console.WriteLine("Programmer: {0}", bfa.Programmer);
        Console.WriteLine("Date: {0}", bfa.Date);
        Console.WriteLine("Comment: {0}", bfa.Comment);
    }
}
4.2 类型发现

可以使用反射探索程序集的内容,步骤如下:
1. 动态加载程序集:

Assembly a = Assembly.Load("Mscorlib");
  1. 获取程序集中的类型数组:
Type[] types = a.GetTypes();
  1. 遍历类型数组并打印类型信息:
foreach (Type t in types)
{
    Console.WriteLine("Type is {0}", t);
}
Console.WriteLine("{0} types found", types.Length);

完整代码示例如下:

using System;
using System.Reflection;
namespace ReflectingAnAssembly
{
    public class Tester
    {
        public static void Main()
        {
            Assembly a = Assembly.Load("Mscorlib");
            Type[] types = a.GetTypes();
            foreach (Type t in types)
            {
                Console.WriteLine("Type is {0}", t);
            }
            Console.WriteLine("{0} types found", types.Length);
        }
    }
}
5. 反射单个类型

可以对 Mscorlib 程序集中的单个类型进行反射。

5.1 查找所有类型成员

使用 GetType 方法获取类型,然后调用 GetMembers 方法获取该类型的所有成员:

using System;
namespace ReflectingOnAType
{
    public class Tester
    {
        public static void Main()
        {
            Type theType = Type.GetType("System.Reflection.Assembly");
            Console.WriteLine("\nSingle Type is {0}\n", theType);
            MemberInfo[] mbrInfoArray = theType.GetMembers();
            foreach (MemberInfo mbrInfo in mbrInfoArray)
            {
                Console.WriteLine("{0} is a {1}", mbrInfo, mbrInfo.MemberType);
            }
        }
    }
}
5.2 查找类型方法

若只关注方法,可调用 GetMethods 方法:

MemberInfo[] mbrInfoArray = theType.GetMethods();
5.3 查找特定类型成员

使用 FindMembers 方法查找特定成员,例如查找名称以 “Get” 开头的方法:

using System;
using System.Reflection;
namespace FindingParticularMembers
{
    public class Tester
    {
        public static void Main()
        {
            Type theType = Type.GetType("System.Reflection.Assembly");
            MemberInfo[] mbrInfoArray = theType.FindMembers(
                                MemberTypes.Method,
                                BindingFlags.Public |
                                BindingFlags.Static |
                                BindingFlags.NonPublic |
                                BindingFlags.Instance |
                                BindingFlags.DeclaredOnly,
                                Type.FilterName, "Get*");
            foreach (MemberInfo mbrInfo in mbrInfoArray)
            {
                // 处理成员信息
            }
        }
    }
}

总结

特性和反射是 .NET 中强大的工具,特性可以方便地向程序添加元数据,而反射则提供了在运行时访问和操作元数据的能力。通过自定义特性和反射的结合,可以实现许多高级功能,如代码维护跟踪、动态类型创建等。在实际开发中,根据具体需求合理使用特性和反射,可以提高代码的灵活性和可维护性。

流程图

graph LR
    A[开始] --> B[特性与反射概述]
    B --> C[特性的类型与使用]
    C --> D[自定义特性]
    D --> E[反射的用途与操作]
    E --> F[反射单个类型]
    F --> G[结束]

表格总结

主题 主要内容
特性与反射概述 介绍 .NET 应用中的元数据、特性和反射的概念及作用
特性的类型与使用 说明特性的类型、目标、应用方式以及常用特性
自定义特性 阐述自定义特性的声明、命名、构造和使用方法
反射的用途与操作 列举反射的四个主要用途,并详细说明查看元数据和类型发现的操作步骤
反射单个类型 介绍查找所有类型成员、类型方法和特定类型成员的方法

.NET 中的特性与反射:全面解析与实践

6. 后期绑定与动态调用

反射的一个重要应用是后期绑定,即根据类型发现动态实例化对象并调用其属性和方法。以下是一个简单的示例,展示如何使用反射进行后期绑定:

using System;
using System.Reflection;

namespace LateBindingExample
{
    public class MyClass
    {
        public void MyMethod()
        {
            Console.WriteLine("MyMethod is called.");
        }
    }

    public class Tester
    {
        public static void Main()
        {
            // 获取类型
            Type type = typeof(MyClass);

            // 创建实例
            object instance = Activator.CreateInstance(type);

            // 获取方法信息
            MethodInfo method = type.GetMethod("MyMethod");

            // 调用方法
            method.Invoke(instance, null);
        }
    }
}

上述代码的操作步骤如下:
1. 获取目标类型:使用 typeof 操作符获取目标类的 Type 对象。
2. 创建实例:使用 Activator.CreateInstance 方法根据 Type 对象创建类的实例。
3. 获取方法信息:使用 GetMethod 方法获取要调用的方法的 MethodInfo 对象。
4. 调用方法:使用 Invoke 方法调用目标方法,第一个参数是实例对象,第二个参数是方法的参数数组(如果方法无参数则传入 null )。

7. 运行时创建类型(反射发射)

反射的终极应用是在运行时创建新类型,这在某些场景下可以显著提高性能。以下是一个简单的示例,展示如何使用反射发射创建一个简单的类型:

using System;
using System.Reflection.Emit;

namespace ReflectionEmitExample
{
    public class Tester
    {
        public static void Main()
        {
            // 创建一个动态程序集
            AssemblyName assemblyName = new AssemblyName("DynamicAssembly");
            AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);

            // 创建一个动态模块
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");

            // 创建一个动态类型
            TypeBuilder typeBuilder = moduleBuilder.DefineType("DynamicType", TypeAttributes.Public);

            // 定义一个方法
            MethodBuilder methodBuilder = typeBuilder.DefineMethod("DynamicMethod", MethodAttributes.Public | MethodAttributes.Static, typeof(void), Type.EmptyTypes);

            // 获取 IL 生成器
            ILGenerator il = methodBuilder.GetILGenerator();

            // 生成方法体
            il.Emit(OpCodes.Ldstr, "Dynamic method is called.");
            il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
            il.Emit(OpCodes.Ret);

            // 创建类型
            Type dynamicType = typeBuilder.CreateType();

            // 获取方法信息
            MethodInfo dynamicMethod = dynamicType.GetMethod("DynamicMethod");

            // 调用方法
            dynamicMethod.Invoke(null, null);
        }
    }
}

上述代码的操作步骤如下:
1. 创建动态程序集:使用 AssemblyBuilder.DefineDynamicAssembly 方法创建一个动态程序集。
2. 创建动态模块:使用 AssemblyBuilder DefineDynamicModule 方法创建一个动态模块。
3. 创建动态类型:使用 ModuleBuilder DefineType 方法创建一个动态类型。
4. 定义方法:使用 TypeBuilder DefineMethod 方法定义一个方法。
5. 获取 IL 生成器:使用 MethodBuilder GetILGenerator 方法获取 IL 生成器,用于生成方法的中间语言代码。
6. 生成方法体:使用 ILGenerator Emit 方法生成方法的中间语言代码。
7. 创建类型:使用 TypeBuilder CreateType 方法创建最终的类型。
8. 调用方法:使用反射获取方法信息并调用方法。

8. 反射的性能考虑

虽然反射提供了强大的功能,但它也有一定的性能开销。反射操作通常比直接调用代码慢,因为它涉及到运行时的类型查找、方法调用等操作。因此,在使用反射时,需要考虑以下几点:
- 缓存反射信息 :如果需要多次使用相同的反射信息,建议将其缓存起来,避免重复的查找操作。
- 避免不必要的反射 :在性能敏感的场景中,尽量避免使用反射,优先使用直接调用代码。
- 使用泛型 :泛型可以在编译时进行类型检查,避免运行时的类型查找,从而提高性能。

9. 特性与反射的综合应用

特性和反射可以结合使用,实现许多高级功能。例如,在一个 Web 应用中,可以使用特性标记控制器的方法,然后使用反射在运行时根据特性信息进行路由和权限验证。以下是一个简单的示例:

using System;
using System.Reflection;

// 定义一个自定义特性
[AttributeUsage(AttributeTargets.Method)]
public class PermissionAttribute : Attribute
{
    public string RequiredPermission { get; set; }

    public PermissionAttribute(string permission)
    {
        RequiredPermission = permission;
    }
}

// 定义一个控制器类
public class MyController
{
    [PermissionAttribute("Admin")]
    public void AdminMethod()
    {
        Console.WriteLine("Admin method is called.");
    }

    [PermissionAttribute("User")]
    public void UserMethod()
    {
        Console.WriteLine("User method is called.");
    }
}

// 模拟权限验证
public class PermissionValidator
{
    public static bool ValidatePermission(MethodInfo method, string userPermission)
    {
        PermissionAttribute attribute = method.GetCustomAttribute<PermissionAttribute>();
        if (attribute != null)
        {
            return attribute.RequiredPermission == userPermission;
        }
        return true;
    }
}

// 测试类
public class Tester
{
    public static void Main()
    {
        MyController controller = new MyController();
        Type type = typeof(MyController);

        // 获取所有方法
        MethodInfo[] methods = type.GetMethods();

        foreach (MethodInfo method in methods)
        {
            if (PermissionValidator.ValidatePermission(method, "Admin"))
            {
                method.Invoke(controller, null);
            }
        }
    }
}

上述代码的操作步骤如下:
1. 定义自定义特性:定义一个 PermissionAttribute 特性,用于标记方法所需的权限。
2. 定义控制器类:在控制器类的方法上使用 PermissionAttribute 特性标记所需的权限。
3. 实现权限验证逻辑:定义一个 PermissionValidator 类,用于验证方法的权限。
4. 使用反射调用方法:在测试类中,使用反射获取控制器类的所有方法,并根据权限验证结果调用方法。

流程图

graph LR
    A[开始] --> B[后期绑定与动态调用]
    B --> C[运行时创建类型]
    C --> D[反射的性能考虑]
    D --> E[特性与反射的综合应用]
    E --> F[结束]

表格总结

主题 主要内容
后期绑定与动态调用 介绍使用反射进行后期绑定和动态调用的方法及示例代码
运行时创建类型 展示如何使用反射发射在运行时创建新类型的示例代码及操作步骤
反射的性能考虑 提出使用反射时的性能优化建议,如缓存反射信息、避免不必要的反射等
特性与反射的综合应用 给出特性和反射结合使用的示例,如在 Web 应用中进行路由和权限验证

总结

特性和反射是 .NET 中非常强大的工具,它们为开发者提供了在运行时操作元数据和动态创建类型的能力。通过合理使用特性和反射,可以实现许多高级功能,如代码维护跟踪、动态类型创建、权限验证等。然而,反射也有一定的性能开销,在使用时需要谨慎考虑。在实际开发中,开发者应根据具体需求,灵活运用特性和反射,以提高代码的灵活性和可维护性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值