【层面一】C#语言基础和核心语法-02(反射/委托/事件)
文章目录
- 1. 反射(Reflection)
- 1.1 核心比喻:X光机与黑盒子
- 1.2 原理与基石:元数据
- 1.3 核心功能与API
- 1.4 优缺点与适用场景
- 2. 委托(Delegate)与事件(Event):
- 2.1 委托(Delegate)
- 2.2 事件(Event)
1. 反射(Reflection)
核心概念:
在 .NET 中,编译后的代码并不是直接变成机器码,而是生成一种叫做 IL(中间语言) 的代码,并打包在程序集(.dll 或 .exe 文件)中。同时,程序集中还包含了丰富的元数据,这些元
数据详细描述了代码中的类型、方法、属性、字段等。反射就是读取这些元数据并与之交互的 API。
1.1 核心比喻:X光机与黑盒子
想象一下,你面前有一个密封的、不透明的黑盒子(一个编译好的 .dll 或 .exe 程序集)。你看不到里面的结构,只知道它可以执行某些功能。
-
正常使用:你通过盒子外部已知的几个按钮(公共类和方法)来与它交互。你知道“按下A按钮会执行A功能”,但你不知道A功能内部是如何实现的。
-
反射:就像你获得了一台强大的X光机。你用这台X光机去扫描那个黑盒子,于是你可以:
-
看清盒子内部所有的零件(类、结构、枚举)。
-
看清每个零件的详细蓝图(方法、属性、字段、事件)。
-
看清零件之间的组装关系(继承、接口实现)。
-
甚至可以在盒子运行时,动态地去触发里面某个隐藏的开关(调用私有方法),或者临时替换掉某个零件(动态创建类型和对象)。
-
这个X光机,就是 .NET 的反射机制。那个黑盒子,就是你的程序集。而反射的本质,就是程序在运行时能够审视、发现和分析自身或其它的程序集结构,并能动态操作这些结构的能力。
1.2 原理与基石:元数据
反射之所以能实现,全靠 .NET 在编译时埋下的“伏笔”——元数据。
- 编译时发生了什么?
当你用C#编译器编译代码时,它不只生成中间语言(IL)(即CPU无关的操作指令),还会生成大量的元数据。
-
IL:相当于“怎么做”的指令集。例如,“调用一个方法”、“创建一个新对象”等。
-
元数据:相当于“有什么”的详细清单。它是一个结构化的数据库,描述了程序集中的所有信息:
-
定义了哪些类、结构、接口、枚举?
-
每个类中有哪些字段、属性、方法、事件?
-
每个方法有什么参数?什么返回值?什么访问权限?
-
哪些类继承了哪些类?实现了哪些接口?
-
…等等一切细节。
-
这个元数据和IL代码一起被打包到程序集(.dll/.exe)中。因此,一个.NET程序集是自描述的。
- 运行时如何工作?
当CLR加载一个程序集时,它会将元数据加载到内存中,并形成一个丰富的数据结构。反射API(System.Reflection 命名空间下的类)本质上就是查询这个内存中的元数据数据库的接口。
所以,反射的原理就是:通过反射API,在运行时查询已加载程序集的元数据,从而动态地获取类型信息并与之交互。
1.3 核心功能与API
反射的主要功能都围绕一个核心类展开:System.Type。
- 获取 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");
- 探索类型信息
获取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}");}
}
- 动态操作:反射的终极威力
仅仅查看信息(自省)还不够,反射还能动态创建和调用。
- 动态创建对象:
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 优缺点与适用场景
优点(为什么需要它?)
-
极高的灵活性:允许编写非常通用和灵活的代码。许多框架的核心都基于反射。
-
实现解耦:代码可以不依赖具体的实现,而是通过字符串名称或接口来发现和调用类型,实现真正的插件化架构。
缺点(为什么要谨慎使用?)
-
性能开销:反射是运行时查询和操作,相比编译时绑定好的直接调用,速度慢一个数量级。频繁调用会是性能瓶颈。
-
安全性:可以绕过访问权限检查(访问私有成员),破坏封装性。
-
编译时安全性丧失:由于大量使用字符串(如方法名、类型名),拼写错误只能在运行时被发现,无法在编译时检查。
-
代码可读性:反射代码通常更复杂、更难以理解和维护。
典型应用场景
-
依赖注入/控制反转容器:如 ASP.NET Core 内置的容器,通过反射来发现构造函数参数,并自动创建所需的对象。
-
对象关系映射器:如 Entity Framework,通过反射读取类的属性信息,并将其映射到数据库表的列。
-
序列化/反序列化:如 System.Text.Json 或 Newtonsoft.Json,通过反射获取对象的属性及其类型,从而将对象转换为JSON字符串,或从JSON字符串重建对象。
-
测试框架:如 xUnit、NUnit,通过反射来发现标记了 [Fact] 或 [Test] 的测试方法并执行它们。
-
插件系统:应用程序从特定文件夹加载 .dll 文件(程序集),然后通过反射来查找实现了特定接口(如 IPlugin)的类,并动态创建实例来扩展功能。
总结:反射的本质
反射的本质是 .NET 运行时利用编译时嵌入的元数据,提供的一套允许程序在运行时进行自省(Introspection)和交互(Interoperation)的API。
-
它的根基是元数据:没有元数据,反射就是无米之炊。
-
它的核心是 Type 类:这是你与元数据交互的主要入口。
-
它的威力在于动态性:允许你编写不知道具体类型也能操作的代码,极大地提升了框架的灵活性和扩展性。
-
它的代价是性能:这种动态性是以牺牲编译时优化和安全性为代价的。
因此,反射是一个强大的高级工具,但它不是用于日常业务开发的常规武器。它的正确使用场景是构建框架、库和高级工具。在普通的应用代码中,应优先考虑接口、泛型、委托等更安全、更高效的设计,除非你确实需要反射才能解决的动态需求。
2. 委托(Delegate)与事件(Event):
2.1 委托(Delegate)
类型安全的“函数指针”或“方法契约”
-
核心比喻:遥控器与电器
想象你有一个万能遥控器。这个遥控器上有一个特殊的按钮,这个按钮被编程为“启动任何具有‘开始工作’功能的东西”。-
你可以把这个遥控器对准空调,按下按钮,空调开始制冷。
-
你也可以把它对准电视机,按下按钮,电视机开始播放。
-
甚至可以把它对准咖啡机,按下按钮,咖啡机开始煮咖啡。
-
这个万能遥控器,就是委托。
-
它定义了一个契约:任何能被这个按钮触发的设备,都必须有一个“开始工作”的方法(即方法签名必须匹配)。
-
它本身不实现任何功能,它只是间接地、安全地去调用别人已经实现好的方法。
-
委托的本质
委托的本质是一个类,它继承自 System.MulticastDelegate。它主要包含了三个重要部分:-
目标对象:方法属于哪个对象(如果是实例方法)。
-
方法指针:要调用的方法是哪一个。
-
调用列表:支持多个方法的链式调用(多播)。
-
- 如何定义和使用委托
第一步:声明委托类型(定义契约)
// 关键字 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;
- 为什么需要委托?—— 实现回调与解耦
委托最大的威力在于将方法作为参数传递,从而实现策略模式和高阶函数。
// 一个通用的数据处理方法
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),这非常危险。事件就是为了解决这个问题而生的。
- 核心比喻:杂志订阅
-
出版社(发布者):它出版杂志。它提供一个“订阅”服务(事件)。
-
你(订阅者):你对杂志感兴趣。你向出版社“订阅”了这个服务(订阅事件)。
-
事件流程:
-
出版社出版了新一期杂志(事件被触发)。
-
出版社的邮寄系统会自动将杂志发送给所有订阅者(调用所有事件处理程序)。
-
你无法主动要求出版社“立刻给你寄一本”,你只能等待出版社发布(外部不能直接触发事件)。
-
你可以随时“取消订阅”(取消注册事件处理程序)。
-
在这个比喻中:
-
事件就是出版社提供的“订阅服务”。
-
委托是邮寄系统的协议(规定邮寄的东西必须是“杂志”)。
-
事件处理程序就是你提供的收货地址(一个符合协议的方法)。
-
事件的本质
事件本质上是一个加了访问限制的委托字段。它的内部仍然使用委托来维护调用列表,但外部代码只能通过 += 和 -= 操作来订阅和取消订阅,而不能直接调用或赋值。 -
如何定义和使用事件
标准模式(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"
- 事件的核心优势:封装与安全
对比一下直接使用公共委托字段:
// 危险的公共委托字段
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)手中。}
}
总结与关系
特性 | 委托 | 事件 |
---|---|---|
本质 | 一个类,用于持有和调用方法 | 一个加了封装的委托字段(语法糖) |
核心目的 | 实现回调机制和策略模式,将方法作为参数传递 | 实现发布-订阅模型,提供对象间的**松耦合通知 |
访问控制 | 公共委托字段可以被任意调用、赋值、清空 | 外部代码只能通过 += 和 -= 来订阅和取消订阅 |
比喻 | 万能遥控器 | 杂志订阅服务 |
关系 | 事件是基于委托实现的。事件是委托的包装器,为其提供了封装性和安全性。 |
一句话总结:
-
当你需要将一个方法传递给另一个方法时,使用委托。
-
当一个对象(发布者)需要通知其他对象(订阅者)某事已发生,但又不想与它们紧密耦合时,使用事件。