C#委托底层实现原理与技术细节

## C#委托底层实现原理与技术细节 **C#委托本质上是一个继承自System.MulticastDelegate的特殊类,通过单链表结构管理回调方法,实现类型安全、线程安全的多播机制**。委托在IL层面表现为一个类,其构造函数通过ldftn指令获取方法地址,调用时通过Invoke方法遍历链表执行所有绑定的方法。多播委托通过Combine和Remove方法实现不可变的链表重组,确保线程安全。与接口相比,委托提供更简洁的语法和原生的多播支持,但接口在定义复杂契约时更具优势。 ### 一、委托的本质与继承体系 C#委托不是简单的函数指针,而是完整继承自System.Delegate的类。当使用delegate关键字定义一个委托类型时,编译器会自动生成一个继承自System.MulticastDelegate的类。例如: ```csharp public delegate void MyDelegate(string message); ``` 上述代码在IL层面会被展开为: ```il .class public auto ansi sealed MyDelegate extends [System.Private.CoreLib]System.MulticastDelegate { .method public hidebysig specialname rtspecialname instance void .ctor (object 'object', native int 'method') runtime managed { } .method public hidebysig newslot virtual instance void Invoke (string message) runtime managed { } .method public hidebysig newslot virtual instance class [System.Private.CoreLib]System.IAsyncResult BeginInvoke (string message, class [System.Private.CoreLib]System.AsyncCallback callback, object 'object') runtime managed { } .method public hidebysig newslot virtual instance void EndInvoke (class [System.Private.CoreLib]System.IAsyncResult result) runtime managed { } } ``` System.Delegate类提供了委托的基础功能,包含四个私有字段: | 字段名称 | 类型 | 说明 | |---------|------|------| | `_target` | object | 调用方法的实例对象引用,静态方法时为null | | `_methodPtr` | native int | 方法的函数指针,用于直接调用 | | `_methodPtrAux` | native int | 辅助指针,可能用于泛型或异步方法 | | `_method` | RuntimeMethodInfo | 包含方法元数据的对象 | 这些字段在IL层面被封装在委托类内部,通过构造函数初始化。当创建委托实例时,编译器会根据方法类型选择合适的构造函数,通过ldnull(静态方法)或对象引用初始化`_target`,通过ldftn获取方法地址并赋值给`_methodPtr`。例如,创建一个指向静态方法的委托: ```il IL_0000: nop IL_0001: ldnull IL_0002: ldftn void Program::StaticMethod() IL_0008: newobj instance void MyDelegate::.ctor(object, native int) IL_000d: stloc.0 ``` ### 二、委托调用的执行机制 当调用委托的Invoke方法时,实际执行过程分为单播和多播两种情况: **单播委托**直接通过`_methodPtr`指向的方法指针调用目标方法,执行效率接近直接函数调用。例如,单播委托的IL调用代码: ```il IL_0012: ldloc.0 IL_0013: ldarg.0 IL_0014: callvirt instance void [System.Private.CoreLib]System.Delegate::Invoke(object) ``` **多播委托**则通过遍历内部维护的链表依次调用所有绑定的方法。MulticastDelegate类在Delegate基础上增加了`_prev`字段,形成单链表结构: ```il .class public auto ansi abstract sealed beforefieldinit System.MulticastDelegate extends [System.Private.CoreLib]System.Delegate { .field private valuetype [System.Private.CoreLib]System.RuntimeMethodHandle InternalMethod .field private object _target .field private native int _methodPtr .field private native int _methodPtrAux .field private valuetype [System.Private.CoreLib]System.RuntimeMethodHandle _method .field private class System.MulticastDelegate _prev } ``` 在调用多播委托的Invoke方法时,会遍历这个链表,依次执行每个节点的方法。为了确保线程安全,MulticastDelegate提供了GetInvocationList方法,它返回一个委托数组的副本,而非直接引用原始链表: ```csharp var delegates = multicaster.GetInvocationList(); foreach (var del in delegates) { del.DynamicInvoke(); } ``` 多播委托的执行顺序严格遵循注册顺序,且在调用过程中如果某个子委托抛出异常,默认只会抛出第一个异常,后续委托仍然会继续执行。这种异常处理机制在需要确保多个回调处理的情况下特别有用。 ### 三、运算符+/-的底层操作 C#委托的+/-运算符实际上是调用Delegate类的Combine和Remove方法的语法糖。这两个方法负责动态重组委托链表,实现多播委托的添加和移除功能: `+=`运算符在IL层面被转换为: ```il IL_0019: ldloc.0 IL_001a: ldloc.1 IL_001b: call class [System.Runtime]System.Delegate [System.Runtime]System.Delegate::Combine(class [System.Runtime]System.Delegate, class [System.Runtime]System.Delegate) IL_0020: castclass MyDelegate IL_0025: stloc.0 ``` `-=`运算符则转换为: ```il IL_002a: ldloc.0 IL_002b: ldloc.1 IL_002c: call class [System.Runtime]System.Delegate [System.Runtime]System.Delegate::Remove(class [System.Runtime]System.Delegate, class [System.Runtime]System.Delegate) IL_0031: castclass MyDelegate IL_0036: stloc.0 ``` Combine和Remove方法通过不可变性确保线程安全:每次操作都会创建一个新的委托实例,而不是修改原有的委托实例。这种设计使得多个线程可以同时操作同一个委托而不必担心数据竞争问题。 MulticastDelegate类的CombineImpl方法实现了具体的链表合并逻辑。当调用Combine方法时,会调用CombineImpl来创建一个包含两个委托链表的新链表,新链表的头节点指向当前委托实例,其`_prev`字段指向另一个委托实例,形成递归的链式结构。 ### 四、委托与接口回调的对比分析 在C#中,委托和接口都可以用于实现回调机制,但它们在实现方式和适用场景上存在显著差异: | 特性 | 委托 | 接口 | |------|------|------| | 实现复杂度 | 简单(直接绑定方法) | 复杂(需定义接口、实现类、注册实例) | | 多播支持 | 原生支持(通过+/-运算符) | 需手动实现(如用List<T>维护多个实例) | | 执行性能 | 接近直接函数调用(99.9分) | 虚函数调用(100分) | | 线程安全 | 自动保证(不可变性+内部锁) | 需自行管理(如加锁操作) | | 设计模式 | 适合单一回调或事件驱动 | 适合定义复杂契约或需要多继承行为 | 委托提供更简洁的语法和原生的多播支持,特别适合事件处理和异步编程等场景。例如,C#事件本质上就是特殊的多播委托,通过+=和-=操作符订阅和取消订阅事件处理方法。 接口则更适合需要定义多个方法契约的情况,例如在IoC容器中注册服务时,通常会通过接口来定义服务契约,而非单一的方法委托。接口还支持多重继承,允许一个类实现多个接口,从而实现更丰富的多态行为。 在性能方面,委托调用通常比接口调用快2-3倍,因为委托本质上是函数指针跳转,而接口调用需要通过虚函数表查找。但两者的差异在实际应用中通常可以忽略不计,除非在极端性能敏感的场景下。 ### 五、委托的闭包实现与内存管理 当委托捕获外部变量(形成闭包)时,C#编译器会自动生成一个包含捕获变量的匿名类: ```csharp Action action = () => { Console.WriteLine(x); Console.WriteLine(y); }; ``` 上述代码在IL层面会被转换为: ```il .class private auto ansi nested beforefieldinit '<Main>o__SiteContainer0' extends [System.Runtime]System.ValueType { .field public int32 '<>x__1' .field public int32 '<>y__2' .method private hidebysig specialname static void '<>p__0'(class [System.Private.CoreLib]System.Action CS$<>8__locals0, int32 CS$<>8__locals1, int32 CS$<>8__locals2) cil managed { } .method private hidebysig specialname static void '<>p__1'(class [System.Private.CoreLib]System.Action CS$<>8__locals0, int32 CS$<>8__locals1, int32 CS$<>8__locals2) cil managed { } } ``` **闭包委托的内存分配成本远高于无闭包委托**,具体取决于捕获的变量类型: - 无闭包:仅分配一次内存 - 捕获静态字段:产生1次内存分配 - 捕获实例字段:产生100次内存分配 - 捕获局部变量:产生200次内存分配 这种内存分配差异在高性能场景下需要特别注意。优化闭包内存分配的方法是:通过增加参数数量,使用方法参数传递变量,避免捕获外部变量。例如: ```csharp // 优化前 for (int i = 0; i < 100; i++) { act += () => Console.WriteLine(i); } // 优化后 for (int i = 0; i < 100; i++) { act += (x) => Console.WriteLine(x); act += i; } ``` 在优化后代码中,闭包委托仅捕获一个参数,而不是循环中的局部变量,从而显著降低了内存分配次数。 ### 六、委托在事件处理中的应用 事件是C#中特殊的多播委托,通过event关键字声明,提供了一种安全的回调注册和触发机制。事件在底层使用委托实现,但通过访问器(add/remove)提供了更严格的控制: ```csharp public class Publisher { public event EventHandler MyEvent; protected virtual void OnMyEvent(EventArgs e) { MyEvent?.Invoke(this, e); } } ``` 上述代码在IL层面会被转换为: ```il .class public auto ansi sealed Publisher { .event [System.Runtime]System.EventHandler MyEvent { .add on add_MyEvent class [System.Runtime]System.EventHandler .remove on remove_MyEvent class [System.Runtime]System.EventHandler .raise } .method protected hidebysig virtual instance void OnMyEvent ( class [System.Runtime]System.EventArgs e ) cil managed { // 触发事件的代码 } } ``` **事件处理的核心优势在于提供了类型安全的回调注册和触发机制**,同时通过访问器方法可以实现额外的安全检查,例如确保事件只能在特定条件下触发,或者在触发前进行参数验证。 事件触发时使用?.Invoke语法(C#6.0+),可以避免处理过程中因事件处理程序为null而引发的异常,这种写法在多线程环境下特别有用,因为它保证了线程安全。 ### 七、委托与接口在设计模式中的选择 在实际开发中,选择委托还是接口实现回调机制取决于具体需求: **委托更适合单一方法回调场景**,例如事件处理、异步通知等。委托提供了简洁的语法和原生的多播支持,可以轻松实现一对多的通知机制。例如: ```csharp public delegate void ProgressChangedDelegate(int value); public class ProgressManager { public ProgressChangedDelegate ProgressChanged; public void UpdateProgress(int value) { ProgressChanged?.Invoke(value); } } ``` **接口则更适合定义复杂的行为契约**,例如需要多个方法配合的场景。接口提供了更严格的类型检查和更丰富的多态支持,可以实现多继承行为。例如: ```csharp public interface IProgressHandler { void OnProgress(int value); void OnCompleted(); } public class ProgressManager { private List<IProgressHandler> handlers = new List<>(); public void AddHandler(IProgressHandler handler) { handlers.Add(handler); } public void RemoveHandler(IProgressHandler handler) { handlers.Remove(handler); } public void UpdateProgress(int value) { foreach (var handler in handlers) { handler.OnProgress(value); } } public void NotifyCompleted() { foreach (var handler in handlers) { handler.OnCompleted(); } } } ``` 在需要实现观察者模式时,事件(基于多播委托)通常是首选,因为它提供了类型安全的注册和触发机制,同时支持多播。而在需要定义更复杂的交互协议时,接口则提供了更好的结构化支持。 ### 八、总结与性能建议 **C#委托是.NET框架中实现回调和事件的基础机制,其底层通过继承自MulticastDelegate的特殊类实现,采用单链表结构管理多播调用列表**。委托提供了类型安全、线程安全的回调注册和触发机制,特别适合事件驱动编程和异步通知场景。 在性能敏感的应用中,应尽量避免使用闭包委托,因为它们会导致额外的内存分配。对于需要重复赋值的Action变量,应优先考虑无闭包的委托实现方式。 在选择回调机制时,如果只需单一方法回调,委托是更简洁高效的选择;如果需要定义复杂的行为契约或支持多继承,接口则更为适合。在实际开发中,事件(基于多播委托)是处理GUI交互和系统通知的理想选择,而接口则更适合在IoC容器中定义服务契约。

游戏研发 校招面经解读(一)

如何定义纯虚函数: C++:在一个类里面定义纯虚函数: 在函数定义后面加上= 0; 并且这个类就变成了虚基类,无法实例化,就是不能创建对象,只能被继承,并且继承之后,子类必须实现纯虚函数。 抽象类是实现多态的一种途径。可以提高代码的**可扩展性**和**灵活性**。 可扩展性:对**同一个接口**想实现**有区别的功能**的时候,只需要继承抽象类实现一个新的接口就行了。 灵活性:灵活地编程。**面向接口编程**,只需要改接口的部分,不需要改其他地方,而且**可以用抽象类的指针去指向其子类的对象**,这样就实现了以统一的方式处理不同的子类对象 C#中没有像C++那样的纯虚函数的概念,但是**依旧有抽象类和抽象方法** (abstract),不能实例化,方法不用实现public abstract void area(); C++ 和 C#的抽象类中都可以定义虚函数(非抽象方法),不用 =0 和 abstract,可以被继承,也可以被重写override C#的抽象方法只能定义在抽象类里面吗? 是的,因为不同的类不允许有没有实现的方法。 宏定义有哪些作用? 定义常量 定义函数 调试信息 #ifdef DEBUG #define LOG(x) std::cout<<x<<std::endl; #else #define LOG(x) //空展开 #endif 条件编译:#ifdef #ifndef #elif #if #elif #endif 用宏define出来的常量和函数,在使用到它的时候会进行宏展开,其实就是文本 替换,但是,就是因为宏是文本替换,所以他是类型不安全的。比如,用一个宏 实现比较大小的功能,如果是int,double这样的数据类型,他就可以正常工 作,但是如果传进去了两个字符串(sring),那就不能正常工作了 不会被编译器检查出来的错误有哪些? 要解决这种问题,可以用函数重载来实现,或者用模板来实现,对于 string,可以用模板特化来实现,自定义的数据类型的话,可以使用操作符 重载 条件编译和visual studio的debug模式, release模式有点像,比如说,我们可以**预定义好一种LOG宏函数**, #ifdef DEBUG #define LOG(X) std::cout<<X<<std::endl; #endif 并且它只有DEBUG这个宏被定义的时候才会被编译。 并且在代码段里面,也是用条件编译,只有DEBUG这个宏被定义的时候,相关代码才会被执行 #ifdef DEBUG Log(x); #endif C++是**类型安全**的编程语言:**绝大部分类型相关的错误能够在编译期间被检查出来** C++编译器找不出来的错误: **数组越界**:不管是[]还是vector,都无法在编译的时候找出越界问题 **访问空指针**:解引用空指针(nullptr)的时候 一个模板类有什么作用: 减少重复代码。模板类是编译时多态的一种体现,他把数据类型作为参数,在编译时对不同的数据类型生成特定的代码实现,能够减少重复代码的书写。像C++的STL容器就都是模板类 提高代码的灵活性和可扩展性:对某些特殊类型的实例化模板,我们可以进行模板特化,为它实现和模板类不同的功能。 增加类型安全。使用模板类,会在编译期间生成代码进行编译,同时会检查类型的正确性 提高性能,虽然会增加编译时间,但是比运行时再进行类型转换或者运行时多态快 C#的委托实际上是怎样的类型。 是一个特殊的类,是方法的容器,可以将任意个同类型的方法当作参数传递,存储和调用 C#的编译流程是怎样的 和C++很不一样。 先编译生成**中间语言(IL)**,存储在.exe或者dll中, 运行的时候,需要使用.NET框架,再经过一次JIT编译,.NET框架会根据不同的平台生成对应的机器码,这期间还会进行链接,优化等操作,并将程序载入内存 静态库和动态库有什么不一样呢 静态库会被拷贝至应用程序中,成为应用程序的一部分,因此调用它会更快,不过,当很多个程序都用到这个库的时候,内存中会出现很多个这个库的代码,浪费空间 动态库只有在**用到它的时候才会被加载进内存**,且内存中只会存在一个,允许多个程序共用这个库 C++ 动态库的代码是怎么被主程序执行的: 比如说,我现在要printf了,那么我就要用到**CRT(C RunTime)这个动态链接库**,它会被载入内存,但是我要怎么知道printf在哪里,我又要怎么读到CPU里面去并执行它呢? 当你的程序调用 CRT 中的 printf 函数时,**操作系统**会负责将 CRT 动态链接库加载到内存中。 在编译和链接过程中,链接器会将你的程序中对 printf 函数的调用**解析**为对 CRT 中 printf 函数在**内存中地址的引用**(这里是逻辑地址)。这样,在运行时,当你的程序调用 printf 函数时,它实际上是通过引用 CRT 中 printf 函数在内存中的地址来调用它的。 当你的程序运行时,操作系统会为它分配一个**虚拟地址空间**。虚拟地址空间中的**每个虚拟地址**都会通过内存管理单元(MMU)映射到物理内存中的一个物理地址。这个映射过程是由操作系统管理的,并且对你的程序是透明的。 因此,当你的程序调用 printf 函数时,它实际上是通过引用一个虚拟地址来调用它的。这个虚拟地址会被 MMU 映射到物理内存中“**CRT 动态链接库**”所在位置的一个物理地址。 这样,你的程序就能够通过引用这个虚拟地址来调用 CRT 中 printf 函数在内存中的实际位置。 操作系统会为每个进程创建页表,当进程被载入内存的时候,就会更新页表的内容 在用到了动态链接库的指令的时候,CPU会根据操作系统的MMU,访问到动态链接库所在的物理内存,取出相关的指令,便可以执行动态链接库的代码了(例如printf) 更多内容: 编程导航:https://www.codefather.cn/user/1646369469980790786/post 知乎:https://www.zhihu.com/people/xiao-xun-mo-yu/posts bilibili:https://space.bilibili.com/689579105

Learning-SICP

《计算机程序的构造和解释》系列公开课,视频是两位作者(Harold Abelson、Gerald Jay Sussman)在1986年7月给Hewlett-Packard公司员工培训时的录像。 还收集了一些有用的学习资料,包括SICP原书、Scheme新手教程、教学环境搭建、相关拓展习题等资料。 地址:https://learningsicp.github.io/

下载 APP