面试题答案
一键面试1. C# 委托底层实现原理
- 内存分配:
- 委托本质上是一个类,继承自
System.MulticastDelegate
(间接继承,System.Delegate
继承自System.MulticastDelegate
)。当定义一个委托类型时,CLR 会为其创建一个派生自System.MulticastDelegate
的新类。 - 委托实例在内存中包含目标对象(如果是实例方法)、方法指针(指向目标方法)以及指向下一个委托实例的引用(用于多播委托)。对于静态方法,目标对象为
null
。 - 例如,定义委托
public delegate void MyDelegate(int num);
,当创建委托实例MyDelegate del = new MyDelegate(SomeClass.SomeMethod);
时,会在堆上为del
分配内存,存储相关信息。
- 委托本质上是一个类,继承自
- 调用机制:
- 单播委托调用直接通过存储的方法指针调用目标方法。例如,
del(5);
会直接调用SomeClass.SomeMethod(5)
。 - 多播委托调用时,CLR 会遍历委托链表,依次调用每个委托实例所指向的方法。
MulticastDelegate
类维护了一个内部链表结构,通过Combine
和Remove
方法来管理委托链表。例如,MyDelegate combinedDel = del1 + del2;
会创建一个新的多播委托,将del1
和del2
链接起来。当调用combinedDel(5);
时,会依次调用del1
和del2
所指向的方法。
- 单播委托调用直接通过存储的方法指针调用目标方法。例如,
2. C# 事件底层实现原理
- 内存分配:
- 事件基于委托实现。当在类中声明一个事件时,编译器会自动生成一个私有的委托字段、一个添加方法(
add_
前缀)和一个移除方法(remove_
前缀)。 - 例如,
public event EventHandler MyEvent;
,编译器会生成一个类似于private EventHandler _myEvent;
的字段,以及public void add_MyEvent(EventHandler value)
和public void remove_MyEvent(EventHandler value)
方法。
- 事件基于委托实现。当在类中声明一个事件时,编译器会自动生成一个私有的委托字段、一个添加方法(
- 调用机制:
- 事件的订阅通过
+=
操作符,实际调用的是编译器生成的添加方法,将新的委托实例添加到事件的委托链表中。例如,obj.MyEvent += new EventHandler(MyEventHandler);
。 - 事件的取消订阅通过
-=
操作符,调用移除方法,从委托链表中移除相应的委托实例。 - 事件的引发通常在类的内部通过判断委托字段是否为
null
,如果不为null
,则调用委托。例如,if (MyEvent != null) { MyEvent(this, EventArgs.Empty); }
- 事件的订阅通过
3. 优化委托与事件使用的策略
- 减少不必要的委托创建:
- 策略:在频繁调用的场景下,避免每次都创建新的委托实例。例如,对于一些固定的处理逻辑,可以将委托实例定义为静态字段,复用该实例。
- 性能影响:减少内存分配和垃圾回收压力,提高性能。因为创建新的委托实例需要在堆上分配内存,频繁创建会增加垃圾回收的频率。
- 可维护性影响:代码结构更清晰,可维护性提高。因为委托实例的创建集中在一处,便于管理和修改。
- 合理使用弱引用委托:
- 策略:在存在对象生命周期较长且事件订阅可能导致内存泄漏的场景下,使用弱引用委托。可以通过自定义弱引用委托类或者使用第三方库(如
WeakEventManager
在 WPF 中)来实现。 - 性能影响:会增加一定的内存开销和处理时间,因为需要额外管理弱引用。但可以有效避免内存泄漏,从长远来看提升了应用程序的稳定性和性能。
- 可维护性影响:增加了代码的复杂性,需要额外处理弱引用相关逻辑。但能避免潜在的内存泄漏问题,提高代码的健壮性,从整体项目维护角度可能是有益的。
- 策略:在存在对象生命周期较长且事件订阅可能导致内存泄漏的场景下,使用弱引用委托。可以通过自定义弱引用委托类或者使用第三方库(如
- 批量事件处理优化:
- 策略:在某些场景下,将多个事件的处理合并为一个批量处理。例如,在处理多个 UI 元素的点击事件时,如果它们的处理逻辑类似,可以统一注册到一个事件处理方法中,通过判断事件源来执行不同的具体逻辑。
- 性能影响:减少委托调用的开销,因为减少了方法调用的次数。同时,由于事件处理逻辑集中,缓存局部性更好,进一步提升性能。
- 可维护性影响:代码逻辑更集中,便于理解和修改。但如果处理逻辑差异较大,可能会使单个处理方法变得复杂,需要合理设计代码结构来保持可维护性。