【层面一】C#语言基础和核心语法-02(反射/委托/事件)

发布于:2025-09-15 ⋅ 阅读:(16) ⋅ 点赞:(0)

1. 反射(Reflection)

核心概念:

在 .NET 中,编译后的代码并不是直接变成机器码,而是生成一种叫做 IL(中间语言) 的代码,并打包在程序集(.dll 或 .exe 文件)中。同时,程序集中还包含了丰富的元数据,这些元
数据详细描述了代码中的类型、方法、属性、字段等。反射就是读取这些元数据并与之交互的 API。

1.1 核心比喻:X光机与黑盒子

想象一下,你面前有一个密封的、不透明的黑盒子(一个编译好的 .dll 或 .exe 程序集)。你看不到里面的结构,只知道它可以执行某些功能。

  • 正常使用:你通过盒子外部已知的几个按钮(公共类和方法)来与它交互。你知道“按下A按钮会执行A功能”,但你不知道A功能内部是如何实现的。

  • 反射:就像你获得了一台强大的X光机。你用这台X光机去扫描那个黑盒子,于是你可以:

    • 看清盒子内部所有的零件(类、结构、枚举)。

    • 看清每个零件的详细蓝图(方法、属性、字段、事件)。

    • 看清零件之间的组装关系(继承、接口实现)。

    • 甚至可以在盒子运行时,动态地去触发里面某个隐藏的开关(调用私有方法),或者临时替换掉某个零件(动态创建类型和对象)。

这个X光机,就是 .NET 的反射机制。那个黑盒子,就是你的程序集。而反射的本质,就是程序在运行时能够审视、发现和分析自身或其它的程序集结构,并能动态操作这些结构的能力。

1.2 原理与基石:元数据

反射之所以能实现,全靠 .NET 在编译时埋下的“伏笔”——元数据

  1. 编译时发生了什么?

当你用C#编译器编译代码时,它不只生成中间语言(IL)(即CPU无关的操作指令),还会生成大量的元数据

  • IL:相当于“怎么做”的指令集。例如,“调用一个方法”、“创建一个新对象”等。

  • 元数据:相当于“有什么”的详细清单。它是一个结构化的数据库,描述了程序集中的所有信息:

    • 定义了哪些类、结构、接口、枚举?

    • 每个类中有哪些字段、属性、方法、事件?

    • 每个方法有什么参数?什么返回值?什么访问权限?

    • 哪些类继承了哪些类?实现了哪些接口?

    • …等等一切细节。

这个元数据和IL代码一起被打包到程序集(.dll/.exe)中。因此,一个.NET程序集是自描述的。

  1. 运行时如何工作?
    当CLR加载一个程序集时,它会将元数据加载到内存中,并形成一个丰富的数据结构。反射API(System.Reflection 命名空间下的类)本质上就是查询这个内存中的元数据数据库的接口

所以,反射的原理就是:通过反射API,在运行时查询已加载程序集的元数据,从而动态地获取类型信息并与之交互。

1.3 核心功能与API

反射的主要功能都围绕一个核心类展开:System.Type。

  1. 获取 Type 对象
    一切反射操作始于获取一个Type对象,它代表了你要审查的类型。有几种基本方式:
// 1. 使用 typeof 运算符 (编译时已知类型)
Type stringType = typeof(string);

// 2. 使用 Object.GetType() 方法 (在对象实例上)
string name = "Alice";
Type nameType = name.GetType(); // 获取 string 的 Type 对象

// 3. 使用 Type.GetType() 静态方法 (通过类型名称字符串)
Type dtType = Type.GetType("System.DateTime");
Type localType = Type.GetType("MyNamespace.MyClass, MyAssembly");
  1. 探索类型信息
    获取Type对象后,你就可以像查阅字典一样探索类型的所有细节:
Type personType = typeof(Person);

// 获取类型基本信息
Console.WriteLine($"Full Name: {personType.FullName}");
Console.WriteLine($"Is it a class? {personType.IsClass}");

// 获取成员信息
PropertyInfo[] properties = personType.GetProperties(); // 所有公共属性
FieldInfo[] fields = personType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); // 所有私有实例字段
MethodInfo[] methods = personType.GetMethods(); // 所有公共方法
ConstructorInfo[] constructors = personType.GetConstructors(); // 所有构造函数

foreach (var prop in properties)
{
    Console.WriteLine($"Property: {prop.Name}, Type: {prop.PropertyType}");
}

foreach (var method in methods)
{
    Console.WriteLine($"Method: {method.Name}, Return Type: {method.ReturnType}");
    foreach (var param in method.GetParameters())
    {
        Console.WriteLine($"  Parameter: {param.Name}, Type: {param.ParameterType}");
    }
}
  1. 动态操作:反射的终极威力
    仅仅查看信息(自省)还不够,反射还能动态创建和调用。
  • 动态创建对象:
Type personType = typeof(Person);
// 获取特定的构造函数(例如,接收一个string参数的)
ConstructorInfo ctor = personType.GetConstructor(new[] { typeof(string) });
// 调用构造函数来创建对象
object personInstance = ctor.Invoke(new object[] { "Alice" });
  • 动态调用方法:
// 获取 MethodInfo 对象
MethodInfo methodInfo = personType.GetMethod("Introduce");
// 在某个对象实例上调用该方法
methodInfo.Invoke(personInstance, null); // 如果方法有参数,则传入参数数组
  • 动态获取/设置属性/字段(即使它们是私有的):
// 获取一个私有字段的信息
FieldInfo privateField = personType.GetField("_secretId", BindingFlags.NonPublic | BindingFlags.Instance);
// 读取该字段的值
object value = privateField.GetValue(personInstance);
Console.WriteLine($"Secret ID: {value}");

// 设置该字段的值
privateField.SetValue(personInstance, "NewSecret123");

1.4 优缺点与适用场景

优点(为什么需要它?)

  1. 极高的灵活性:允许编写非常通用和灵活的代码。许多框架的核心都基于反射。

  2. 实现解耦:代码可以不依赖具体的实现,而是通过字符串名称或接口来发现和调用类型,实现真正的插件化架构。

缺点(为什么要谨慎使用?)

  1. 性能开销:反射是运行时查询和操作,相比编译时绑定好的直接调用,速度慢一个数量级。频繁调用会是性能瓶颈。

  2. 安全性:可以绕过访问权限检查(访问私有成员),破坏封装性。

  3. 编译时安全性丧失:由于大量使用字符串(如方法名、类型名),拼写错误只能在运行时被发现,无法在编译时检查。

  4. 代码可读性:反射代码通常更复杂、更难以理解和维护。

典型应用场景

  1. 依赖注入/控制反转容器:如 ASP.NET Core 内置的容器,通过反射来发现构造函数参数,并自动创建所需的对象。

  2. 对象关系映射器:如 Entity Framework,通过反射读取类的属性信息,并将其映射到数据库表的列。

  3. 序列化/反序列化:如 System.Text.Json 或 Newtonsoft.Json,通过反射获取对象的属性及其类型,从而将对象转换为JSON字符串,或从JSON字符串重建对象。

  4. 测试框架:如 xUnit、NUnit,通过反射来发现标记了 [Fact] 或 [Test] 的测试方法并执行它们。

  5. 插件系统:应用程序从特定文件夹加载 .dll 文件(程序集),然后通过反射来查找实现了特定接口(如 IPlugin)的类,并动态创建实例来扩展功能。


总结:反射的本质
反射的本质是 .NET 运行时利用编译时嵌入的元数据,提供的一套允许程序在运行时进行自省(Introspection)和交互(Interoperation)的API。

  1. 它的根基是元数据:没有元数据,反射就是无米之炊。

  2. 它的核心是 Type 类:这是你与元数据交互的主要入口。

  3. 它的威力在于动态性:允许你编写不知道具体类型也能操作的代码,极大地提升了框架的灵活性和扩展性。

  4. 它的代价是性能:这种动态性是以牺牲编译时优化和安全性为代价的。

因此,反射是一个强大的高级工具,但它不是用于日常业务开发的常规武器。它的正确使用场景是构建框架、库和高级工具。在普通的应用代码中,应优先考虑接口、泛型、委托等更安全、更高效的设计,除非你确实需要反射才能解决的动态需求。


2. 委托(Delegate)与事件(Event):

2.1 委托(Delegate)

类型安全的“函数指针”或“方法契约”

  1. 核心比喻:遥控器与电器
    想象你有一个万能遥控器。这个遥控器上有一个特殊的按钮,这个按钮被编程为“启动任何具有‘开始工作’功能的东西”。

    • 你可以把这个遥控器对准空调,按下按钮,空调开始制冷。

    • 你也可以把它对准电视机,按下按钮,电视机开始播放。

    • 甚至可以把它对准咖啡机,按下按钮,咖啡机开始煮咖啡。

这个万能遥控器,就是委托。

  • 它定义了一个契约:任何能被这个按钮触发的设备,都必须有一个“开始工作”的方法(即方法签名必须匹配)。

  • 它本身不实现任何功能,它只是间接地、安全地去调用别人已经实现好的方法。

  1. 委托的本质
    委托的本质是一个,它继承自 System.MulticastDelegate。它主要包含了三个重要部分:

    1. 目标对象:方法属于哪个对象(如果是实例方法)。

    2. 方法指针:要调用的方法是哪一个。

    3. 调用列表:支持多个方法的链式调用(多播)。


  1. 如何定义和使用委托
    第一步:声明委托类型(定义契约)
// 关键字 delegate
// 定义了一个委托类型,它要求“任何符合这个契约的方法,必须返回void,并接收一个string参数”
public delegate void ProcessStringDelegate(string input);

这行代码定义了一个新的类型,就像定义 class 一样。这个类型规定了什么样签名的方法可以被它表示。

第二步:创建委托实例(制作遥控器并配对)
你需要一个具体的方法来和委托配对。

// 1. 符合契约的具体方法(电器)
public static void ConvertToUpper(string text)
{
    Console.WriteLine(text.ToUpper());
}

public static void ConvertToLower(string text)
{
    Console.WriteLine(text.ToLower());
}
// 2. 创建委托实例,并关联具体方法(制作遥控器并配对)
ProcessStringDelegate processor; // 声明一个委托变量
processor = new ProcessStringDelegate(ConvertToUpper); // 方式一:构造函数
// 或者更简单的语法糖:
processor = ConvertToUpper; // 方式二:直接赋值

// 3. 调用委托(按下遥控器按钮)
processor("Hello World"); // 输出 "HELLO WORLD"

// 可以重新配对
processor = ConvertToLower;
processor("Hello World"); // 输出 "hello world"

第三步:多播委托(一个遥控器控制多个电器)

ProcessStringDelegate multiProcessor = null;
// 使用 += 添加方法到调用列表
multiProcessor += ConvertToUpper;
multiProcessor += ConvertToLower;
multiProcessor += Console.WriteLine; // 甚至可以添加框架自带的方法

// 调用时,所有方法会按添加顺序依次执行
multiProcessor("Abc");
// 输出:
// ABC
// abc
// Abc

// 使用 -= 从调用列表中移除方法
multiProcessor -= ConvertToUpper;
  1. 为什么需要委托?—— 实现回调与解耦
    委托最大的威力在于将方法作为参数传递,从而实现策略模式高阶函数
// 一个通用的数据处理方法
public static void DataProcessor(string data, ProcessStringDelegate processorMethod)
{
    // 我不关心具体怎么处理数据,我只负责提供数据。
    // 具体处理逻辑由调用者通过 processorMethod 告诉我。
    Console.WriteLine("Processing data...");
    processorMethod(data); // 回调(Callback)在这里发生
    Console.WriteLine("Processing complete.");
}

// 调用者决定如何处理
DataProcessor("Some Data", ConvertToUpper);
DataProcessor("Some Data", ConvertToLower);
// 甚至可以传入一个匿名方法
DataProcessor("Some Data", (s) => Console.WriteLine($"Result: {s}"));

这就实现了完美的解耦:DataProcessor 方法只依赖一个抽象的委托契约,而不依赖任何具体的实现。这使得它极其灵活和可复用。

2.2 事件(Event)

基于委托的“发布-订阅”模型

委托功能强大,但直接使用公共委托字段有一个问题:缺乏封装和控制。任何外部代码都可以直接调用委托(myDelegate())或清空整个调用列表(myDelegate = null),这非常危险。事件就是为了解决这个问题而生的。

  1. 核心比喻:杂志订阅
  • 出版社(发布者):它出版杂志。它提供一个“订阅”服务(事件)。

  • (订阅者):你对杂志感兴趣。你向出版社“订阅”了这个服务(订阅事件)。

  • 事件流程

    1. 出版社出版了新一期杂志(事件被触发)。

    2. 出版社的邮寄系统会自动将杂志发送给所有订阅者(调用所有事件处理程序)

    3. 你无法主动要求出版社“立刻给你寄一本”,你只能等待出版社发布(外部不能直接触发事件)。

    4. 你可以随时“取消订阅”(取消注册事件处理程序)。

在这个比喻中:

  • 事件就是出版社提供的“订阅服务”。

  • 委托是邮寄系统的协议(规定邮寄的东西必须是“杂志”)。

  • 事件处理程序就是你提供的收货地址(一个符合协议的方法)。

  1. 事件的本质
    事件本质上是一个加了访问限制的委托字段。它的内部仍然使用委托来维护调用列表,但外部代码只能通过 += 和 -= 操作来订阅和取消订阅,而不能直接调用或赋值。

  2. 如何定义和使用事件

标准模式(EventHandler模式)
.NET 定义了一个通用的 EventHandler<T> 委托,并推荐使用以下模式:

// 1. 定义事件参数(如果需要传递额外信息)
public class TemperatureChangedEventArgs : EventArgs
{
    public double OldTemperature { get; }
    public double NewTemperature { get; }

    public TemperatureChangedEventArgs(double oldTemp, double newTemp)
    {
        OldTemperature = oldTemp;
        NewTemperature = newTemp;
    }
}

// 2. 发布者类
public class Thermostat
{
    // 3. 声明事件(使用 event 关键字)
    public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;

    private double _currentTemperature;
    public double CurrentTemperature
    {
        get => _currentTemperature;
        set
        {
            if (_currentTemperature != value)
            {
                double oldTemp = _currentTemperature;
                _currentTemperature = value;
                // 5. 触发事件(通知所有订阅者)
                OnTemperatureChanged(oldTemp, value);
            }
        }
    }

    // 4. 封装触发事件的逻辑(通常是一个受保护的虚方法)
    protected virtual void OnTemperatureChanged(double oldTemp, double newTemp)
    {
        // 在触发前,将委托赋值给一个临时变量,是线程安全的做法
        TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(oldTemp, newTemp));
    }
}

// 6. 订阅者类
public class Display
{
    public void Subscribe(Thermostat thermostat)
    {
        // 使用 += 订阅事件(提供事件处理程序方法)
        thermostat.TemperatureChanged += HandleTemperatureChange;
    }

    public void Unsubscribe(Thermostat thermostat)
    {
        // 使用 -= 取消订阅
        thermostat.TemperatureChanged -= HandleTemperatureChange;
    }

    // 7. 事件处理程序:符合 EventHandler<T> 签名的方法
    private void HandleTemperatureChange(object sender, TemperatureChangedEventArgs e)
    {
        Console.WriteLine($"Temperature changed from {e.OldTemperature}°C to {e.NewTemperature}°C");
    }
}

// 使用
var thermostat = new Thermostat();
var display = new Display();

display.Subscribe(thermostat);

thermostat.CurrentTemperature = 25; // 输出 "Temperature changed from 0°C to 25°C"
thermostat.CurrentTemperature = 26; // 输出 "Temperature changed from 25°C to 26°C"
  1. 事件的核心优势:封装与安全

对比一下直接使用公共委托字段:

// 危险的公共委托字段
public class ThermostatDangerous
{
    public EventHandler<TemperatureChangedEventArgs> TemperatureChanged; // 没有 event 关键字

    public void SetTemperature(double value)
    {
        // 任何外部代码都可以做到:
        // 1. 清空所有订阅:this.TemperatureChanged = null;
        // 2. 直接触发事件:this.TemperatureChanged?.Invoke(...);
        // 这破坏了程序的设计,极其危险!
    }
}

// 安全的事件
public class ThermostatSafe
{
    public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged; // 有 event 关键字

    public void SetTemperature(double value)
    {
        // 外部代码只能:
        // thermostat.TemperatureChanged += handler; (订阅)
        // thermostat.TemperatureChanged -= handler; (取消订阅)
        // 无法直接调用或赋值,控制权完全在发布者(ThermostatSafe)手中。
    }
}

总结与关系

特性 委托 事件
本质 一个,用于持有和调用方法 一个加了封装的委托字段(语法糖)
核心目的 实现回调机制策略模式,将方法作为参数传递 实现发布-订阅模型,提供对象间的**松耦合通知
访问控制 公共委托字段可以被任意调用、赋值、清空 外部代码只能通过 += 和 -= 来订阅取消订阅
比喻 万能遥控器 杂志订阅服务
关系 事件是基于委托实现的。事件是委托的包装器,为其提供了封装性和安全性

一句话总结:

  • 当你需要将一个方法传递给另一个方法时,使用委托

  • 当一个对象(发布者)需要通知其他对象(订阅者)某事已发生,但又不想与它们紧密耦合时,使用事件