C#面试题及详细答案120道(31-40)-- 委托与事件
《前后端面试题》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,SQL,Linux… 。

文章目录
- 一、本文面试题目录
- 31. 什么是委托(Delegate)?委托的作用是什么?
- 32. 委托和接口的区别
- 33. 什么是匿名方法?如何使用?
- 34. Lambda表达式与匿名方法的关系
- 35. 什么是事件(Event)?事件与委托的关系
- 36. 事件的访问修饰符(add/remove)有什么作用?
- 37. 多播委托(Multicast Delegate)的执行顺序是什么?
- 38. 简述委托的异步调用(BeginInvoke/EndInvoke)
- 39. 如何避免事件订阅中的内存泄漏?
- 40. 什么是Func<T>和Action<T>委托?
 
 
- 二、120道C#面试题目录列表
一、本文面试题目录
31. 什么是委托(Delegate)?委托的作用是什么?
委托(Delegate) 是一种引用类型,用于封装方法,使得方法可以像变量一样被传递、存储和调用。它相当于方法的"指针"或"回调函数",是C#实现回调机制和事件驱动的基础。
委托的特点:
- 类型安全:委托在声明时必须指定方法签名(参数和返回值)
- 可封装静态方法或实例方法
- 支持多播(Multicast):一个委托可以包含多个方法
声明与使用示例:
// 1. 声明委托类型(指定方法签名)
public delegate int CalculateDelegate(int a, int b);public class Calculator
{// 符合委托签名的方法public static int Add(int x, int y) => x + y;public int Subtract(int x, int y) => x - y;
}public class DelegateExample
{public static void Main(){// 2. 创建委托实例,关联静态方法CalculateDelegate addDelegate = Calculator.Add;// 3. 调用委托(等价于调用关联的方法)int sum = addDelegate(3, 5);Console.WriteLine($"3 + 5 = {sum}");// 关联实例方法var calculator = new Calculator();CalculateDelegate subtractDelegate = calculator.Subtract;int difference = subtractDelegate(10, 4);Console.WriteLine($"10 - 4 = {difference}");}
}
委托的主要作用:
- 回调机制:允许将方法作为参数传递给其他方法(如排序时的比较器)
- 事件处理:作为事件的基础,实现发布-订阅模式
- 解耦代码:使调用者和被调用者无需知道彼此的存在
- 实现多态:通过不同的方法实现相同的委托签名,实现不同行为
示例:回调机制
// 排序方法接受比较委托
public static void Sort(int[] array, Func<int, int, bool> compare)
{for (int i = 0; i < array.Length; i++){for (int j = i + 1; j < array.Length; j++){if (compare(array[i], array[j])){// 交换元素int temp = array[i];array[i] = array[j];array[j] = temp;}}}
}// 使用
int[] numbers = { 3, 1, 4, 1, 5 };
// 传递比较方法作为回调
Sort(numbers, (a, b) => a > b); // 降序排序
Console.WriteLine(string.Join(", ", numbers)); // 输出 5, 4, 3, 1, 1
32. 委托和接口的区别
委托和接口都可以实现代码解耦和多态行为,但它们的实现机制和适用场景不同:
| 特性 | 委托 | 接口 | 
|---|---|---|
| 本质 | 封装方法的引用类型 | 定义方法签名的契约 | 
| 实现方式 | 关联具体方法实现 | 类必须实现接口的所有成员 | 
| 多方法支持 | 支持多播(一个委托包含多个方法) | 一个类只能实现接口一次 | 
| 灵活性 | 可以在运行时动态更改关联的方法 | 实现一旦确定,运行时无法更改 | 
| 适用场景 | 回调函数、事件处理、临时方法组合 | 定义类的行为契约、多继承场景 | 
| 签名要求 | 严格匹配方法签名 | 实现类必须实现所有接口成员 | 
委托示例:
// 委托实现灵活的行为注入
public delegate void LoggerDelegate(string message);public class Logger
{public void LogToConsole(string message) => Console.WriteLine($"Console: {message}");public void LogToFile(string message) => System.IO.File.AppendAllText("log.txt", message);
}public class Processor
{// 接受委托作为参数public void Process(LoggerDelegate logger){logger("开始处理");// 处理逻辑...logger("处理完成");}
}// 使用
var logger = new Logger();
var processor = new Processor();
// 动态组合方法
processor.Process(logger.LogToConsole + logger.LogToFile);
接口示例:
// 接口定义行为契约
public interface ILogger
{void Log(string message);
}public class ConsoleLogger : ILogger
{public void Log(string message) => Console.WriteLine($"Console: {message}");
}public class FileLogger : ILogger
{public void Log(string message) => System.IO.File.AppendAllText("log.txt", message);
}public class Processor
{// 接受接口作为参数public void Process(ILogger logger){logger.Log("开始处理");// 处理逻辑...logger.Log("处理完成");}
}// 使用
var processor = new Processor();
processor.Process(new ConsoleLogger()); // 固定实现
选择建议:
- 当需要动态更换方法或组合多个方法时,使用委托
- 当需要定义类的长期行为契约或多个类共享相同行为时,使用接口
- 事件处理场景优先使用委托,类的功能扩展优先使用接口
33. 什么是匿名方法?如何使用?
匿名方法是没有名称的方法,可以直接赋值给委托,简化了委托的使用。它允许在声明委托时内联定义方法体,无需单独声明方法。
语法:
委托类型 委托变量 = delegate(参数列表)
{// 方法体
};
使用示例:
// 声明委托
public delegate int OperationDelegate(int a, int b);public class AnonymousMethodExample
{public static void Main(){// 使用匿名方法赋值给委托OperationDelegate add = delegate(int x, int y){return x + y;};OperationDelegate multiply = delegate(int x, int y){return x * y;};// 调用委托Console.WriteLine($"3 + 5 = {add(3, 5)}");Console.WriteLine($"4 * 6 = {multiply(4, 6)}");// 作为参数传递ProcessNumbers(10, 20, delegate(int a, int b){return a - b;});}public static void ProcessNumbers(int a, int b, OperationDelegate operation){int result = operation(a, b);Console.WriteLine($"结果: {result}");}
}
匿名方法的特点:
- 没有方法名,直接关联到委托
- 可以访问外部变量(闭包)
- 参数列表可以省略(如果委托有参数且方法体不使用)
- 不能有修饰符(如public、static等)
- 不能是泛型方法
访问外部变量示例:
public delegate void MessageDelegate();public static void ShowClosureExample()
{string message = "初始消息";MessageDelegate showMessage = delegate{// 访问外部变量Console.WriteLine(message);};message = "修改后的消息";showMessage(); // 输出"修改后的消息"(捕获的是变量引用)
}
适用场景:
- 简单的委托实现,无需复用的方法
- 作为临时回调函数传递
- 简化事件处理程序的编写
注意:C# 3.0引入的Lambda表达式功能更强大,逐渐替代了匿名方法的使用,但匿名方法在需要忽略参数时仍有优势(如delegate { ... }无需指定参数)。
34. Lambda表达式与匿名方法的关系
Lambda表达式是匿名方法的简化语法,提供了更简洁的方式来创建委托实例,两者都可以用于定义内联的方法实现。
关系与区别:
| 特性 | Lambda表达式 | 匿名方法 | 
|---|---|---|
| 语法 | 更简洁,使用 =>运算符 | 使用 delegate关键字 | 
| 类型推断 | 编译器可推断参数类型 | 必须显式声明参数类型(除非省略参数) | 
| 表达式体 | 支持表达式体(单语句)和语句块 | 仅支持语句块 | 
| 返回值 | 表达式体可隐式返回,无需return | 必须显式使用return语句 | 
| 参数省略 | 不允许省略参数(除非无参数) | 允许省略参数列表( delegate { ... }) | 
| 适用场景 | 大多数场景,更常用 | 需忽略参数时,或兼容旧代码 | 
语法对比:
// 委托定义
public delegate int MathOperation(int a, int b);
public delegate void ActionDelegate();public class LambdaVsAnonymous
{public static void Compare(){// 匿名方法MathOperation addAnon = delegate(int x, int y){return x + y;};// 对应的Lambda表达式(语句块形式)MathOperation addLambda1 = (x, y) =>{return x + y;};// Lambda表达式(表达式体形式,更简洁)MathOperation addLambda2 = (x, y) => x + y;// 无参数情况ActionDelegate actionAnon = delegate{Console.WriteLine("匿名方法");};ActionDelegate actionLambda = () => Console.WriteLine("Lambda表达式");}
}
类型推断示例:
// Lambda表达式的类型推断
public delegate double NumericOperation(double a, double b);public static void TypeInferenceExample()
{// 无需指定参数类型,编译器会根据委托推断NumericOperation divide = (x, y) => x / y;// 匿名方法必须显式指定类型NumericOperation multiply = delegate(double x, double y){return x * y;};
}
使用场景:
- 优先使用Lambda表达式,因为语法更简洁,支持更多功能
- 当需要忽略委托的参数时,使用匿名方法(如delegate { ... })
- LINQ查询中必须使用Lambda表达式
总结:Lambda表达式是匿名方法的进化版本,提供了更简洁、灵活的语法,几乎可以替代匿名方法的所有使用场景,同时增加了类型推断和表达式体等特性。
35. 什么是事件(Event)?事件与委托的关系
事件(Event) 是基于委托的一种机制,用于实现对象间的通信(发布-订阅模式),允许一个对象通知其他对象发生了特定事情。
事件与委托的关系:
- 事件是委托的封装器,提供了更安全的委托使用方式
- 事件内部使用委托存储和调用事件处理程序
- 事件限制了委托的访问权限(只能添加、移除和调用处理程序)
声明与使用示例:
// 1. 定义事件所需的委托
public delegate void PriceChangedEventHandler(decimal oldPrice, decimal newPrice);public class Product
{private decimal _price;// 2. 声明事件(基于上面的委托)public event PriceChangedEventHandler PriceChanged;public decimal Price{get => _price;set{if (_price == value) return;decimal oldPrice = _price;_price = value;// 3. 触发事件(通知订阅者)OnPriceChanged(oldPrice, value);}}// 受保护的虚方法,用于触发事件protected virtual void OnPriceChanged(decimal oldPrice, decimal newPrice){// 检查是否有订阅者,有则调用PriceChanged?.Invoke(oldPrice, newPrice);}
}// 4. 订阅事件
public class PriceWatcher
{public void Subscribe(Product product){// 订阅事件(添加处理程序)product.PriceChanged += Product_PriceChanged;}// 事件处理程序(必须匹配委托签名)private void Product_PriceChanged(decimal oldPrice, decimal newPrice){Console.WriteLine($"价格变化: 从{oldPrice}到{newPrice}");}public void Unsubscribe(Product product){// 取消订阅product.PriceChanged -= Product_PriceChanged;}
}// 使用
public static void Main()
{var product = new Product();var watcher = new PriceWatcher();watcher.Subscribe(product);product.Price = 100; // 触发事件product.Price = 150; // 触发事件watcher.Unsubscribe(product);product.Price = 200; // 不再触发事件
}
事件的特点:
- 只能在声明事件的类内部触发(调用)
- 外部只能添加(+=)或移除(-=)事件处理程序
- 通常使用OnEventName模式的受保护虚方法触发事件,便于派生类重写
- 遵循命名约定:事件名以"Changed"结尾,委托名以"EventHandler"结尾
作用:实现松耦合的通信机制,发布者无需知道订阅者的存在,只需在事件发生时通知所有订阅者。
36. 事件的访问修饰符(add/remove)有什么作用?
事件的add和remove访问器用于自定义事件处理程序的添加和移除逻辑,类似于属性的get和set访问器,提供了对事件订阅过程的控制。
默认实现:
 当声明事件时,编译器会自动生成默认的add和remove访问器,内部维护一个私有的委托字段:
public event EventHandler MyEvent;
// 编译器自动生成类似以下的代码:
private EventHandler _myEvent;
public event EventHandler MyEvent
{add { _myEvent += value; }remove { _myEvent -= value; }
}
自定义访问器:
 可以显式定义add和remove访问器,实现自定义逻辑(如线程安全、权限检查、日志记录等):
public class SecureEventExample
{// 私有委托字段private EventHandler _secureEvent;// 用于线程同步的对象private readonly object _lock = new object();// 声明事件并自定义访问器public event EventHandler SecureEvent{add{// 添加自定义逻辑if (!IsAuthorized())throw new UnauthorizedAccessException("无订阅权限");// 线程安全的添加操作lock (_lock){_secureEvent += value;}Console.WriteLine("事件处理程序已添加");}remove{// 线程安全的移除操作lock (_lock){_secureEvent -= value;}Console.WriteLine("事件处理程序已移除");}}private bool IsAuthorized(){// 实际应用中检查用户权限return true;}// 触发事件的方法public void RaiseEvent(){_secureEvent?.Invoke(this, EventArgs.Empty);}
}
访问修饰符的作用:
- 线程安全:通过lock确保多线程环境下添加/移除处理程序的安全性
- 权限控制:验证订阅者是否有权限订阅事件
- 日志记录:记录事件订阅的相关信息
- 自定义存储:使用自定义数据结构存储事件处理程序(而非默认委托)
- 限制订阅:例如限制只能有一个处理程序,或最多N个处理程序
单播事件示例(限制只能有一个处理程序):
public event EventHandler SingleHandlerEvent
{add{// 只允许一个处理程序,替换现有处理程序_singleHandler = value;}remove{if (_singleHandler == value)_singleHandler = null;}
}
private EventHandler _singleHandler;
注意:自定义访问器时,必须自己维护存储事件处理程序的委托字段,编译器不会自动生成。
37. 多播委托(Multicast Delegate)的执行顺序是什么?
多播委托是包含多个方法引用的委托,允许通过一个委托调用多个方法,这些方法会按特定顺序依次执行。
执行顺序:
 多播委托中的方法按添加顺序(注册顺序) 依次执行,先添加的方法先执行。
示例代码:
// 声明委托
public delegate void MessageDelegate(string message);public class MulticastOrderExample
{public static void Main(){MessageDelegate multicast = null;// 按顺序添加方法multicast += Method1;multicast += Method2;multicast += Method3;Console.WriteLine("调用多播委托:");multicast?.Invoke("测试消息");}public static void Method1(string msg) => Console.WriteLine($"Method1: {msg}");public static void Method2(string msg) => Console.WriteLine($"Method2: {msg}");public static void Method3(string msg) => Console.WriteLine($"Method3: {msg}");
}
输出结果:
调用多播委托:
Method1: 测试消息
Method2: 测试消息
Method3: 测试消息
特殊情况处理:
-  方法移除:移除中间方法会影响执行顺序 multicast += Method1; multicast += Method2; multicast += Method3; multicast -= Method2; // 移除Method2 // 执行顺序:Method1 → Method3
-  返回值处理:多播委托调用时,只能获取最后一个方法的返回值 public delegate int IntDelegate();public static int Get1() => 1; public static int Get2() => 2;// 使用 IntDelegate del = Get1; del += Get2; int result = del(); // result为2(仅最后一个方法的返回值)
-  异常处理:如果一个方法抛出未处理的异常,后续方法不会执行 public static void MethodA() => Console.WriteLine("MethodA"); public static void MethodB() => throw new Exception("出错了"); public static void MethodC() => Console.WriteLine("MethodC");// 使用 Action action = MethodA; action += MethodB; action += MethodC; action(); // 输出MethodA后抛出异常,MethodC不会执行
执行机制:
 多播委托内部维护一个委托链表(Invocation List),+=操作向链表添加委托,-=操作从链表移除委托,调用时按链表顺序依次执行。
注意:在多线程环境中,委托链表可能在遍历过程中被修改,需注意线程安全。
38. 简述委托的异步调用(BeginInvoke/EndInvoke)
委托的异步调用允许在后台线程执行方法,避免阻塞主线程,是C#早期实现异步操作的方式(.NET 4.5前),基于IAsyncResult模式。
核心方法:
- BeginInvoke:启动异步调用,立即返回
- EndInvoke:获取异步调用结果,会阻塞直到调用完成
使用步骤:
- 声明委托
- 调用BeginInvoke启动异步操作,可指定回调函数
- 通过EndInvoke获取结果或处理异常
示例代码:
// 声明委托
public delegate int LongRunningOperationDelegate(int milliseconds);public class AsyncDelegateExample
{// 耗时操作public static int LongRunningOperation(int milliseconds){Console.WriteLine($"开始耗时操作,线程ID: {System.Threading.Thread.CurrentThread.ManagedThreadId}");System.Threading.Thread.Sleep(milliseconds); // 模拟耗时return milliseconds;}// 回调函数(异步操作完成后调用)public static void AsyncCallback(IAsyncResult ar){Console.WriteLine($"回调执行,线程ID: {System.Threading.Thread.CurrentThread.ManagedThreadId}");// 从IAsyncResult获取委托实例var result = (LongRunningOperationDelegate)((System.Runtime.Remoting.Messaging.AsyncResult)ar).AsyncDelegate;try{// 获取结果int duration = result.EndInvoke(ar);Console.WriteLine($"异步操作完成,耗时: {duration}ms");}catch (Exception ex){Console.WriteLine($"异步操作出错: {ex.Message}");}}public static void Main(){Console.WriteLine($"主线程ID: {System.Threading.Thread.CurrentThread.ManagedThreadId}");// 创建委托LongRunningOperationDelegate operation = LongRunningOperation;// 启动异步调用,指定回调函数IAsyncResult asyncResult = operation.BeginInvoke(2000, AsyncCallback, null);// 主线程可以继续执行其他操作Console.WriteLine("主线程继续执行...");// 可选:等待异步完成(不推荐,会阻塞)// asyncResult.AsyncWaitHandle.WaitOne();// 保持主线程运行Console.ReadLine();}
}
输出结果:
主线程ID: 1
主线程继续执行...
开始耗时操作,线程ID: 3
回调执行,线程ID: 3
异步操作完成,耗时: 2000ms
特点与局限:
- 优点:简单易用,无需手动管理线程
- 局限: - 仅适用于委托,灵活性低
- 不支持取消操作
- 错误处理复杂
- .NET 4.5后已被async/await模式取代
 
注意:现代C#开发中,推荐使用Task和async/await进行异步编程,而非委托的BeginInvoke/EndInvoke。
39. 如何避免事件订阅中的内存泄漏?
事件订阅中,如果订阅者(包含事件处理程序的对象)的生命周期长于发布者,可能导致发布者被意外保留,从而引发内存泄漏。
泄漏原因:
 事件内部的委托会持有订阅者对象的引用,即使发布者不再被使用,只要订阅者还在引用委托,发布者就不会被GC回收。
示例:内存泄漏场景
public class Publisher
{public event EventHandler MyEvent;// ...
}public class Subscriber
{public Subscriber(Publisher publisher){// 订阅事件:publisher的委托持有Subscriber的引用publisher.MyEvent += OnEvent;}private void OnEvent(object sender, EventArgs e) { }
}// 使用
public static void CreateAndForget()
{Publisher publisher = new Publisher();// 创建订阅者后,即使publisher没有其他引用new Subscriber(publisher);// 由于Subscriber订阅了事件,publisher会被Subscriber间接引用,无法回收
}
避免内存泄漏的方法:
-  及时取消订阅 
 在订阅者不再需要时,显式移除事件处理程序:public class Subscriber : IDisposable {private readonly Publisher _publisher;public Subscriber(Publisher publisher){_publisher = publisher;_publisher.MyEvent += OnEvent;}public void Dispose(){// 取消订阅_publisher.MyEvent -= OnEvent;}private void OnEvent(object sender, EventArgs e) { } }// 使用 using (var subscriber = new Subscriber(publisher)) {// 使用订阅者 } // 自动调用Dispose()取消订阅
-  使用弱引用(WeakReference) 
 通过弱引用包装事件处理程序,不影响GC回收:public class WeakSubscriber {public WeakSubscriber(Publisher publisher){// 使用弱引用包装处理程序EventHandler handler = OnEvent;publisher.MyEvent += (s, e) => {if (handler.Target != null) // 检查对象是否已回收handler(s, e);elsepublisher.MyEvent -= handler; // 自动取消订阅};}private void OnEvent(object sender, EventArgs e) { } }
-  使用弱事件模式(Weak Event Pattern) 
 适用于WPF等UI框架,通过弱引用管理器管理事件订阅:// WPF中的弱事件示例 WeakEventManager<Publisher, EventArgs>.AddHandler(publisher, "MyEvent", OnEvent);
-  限制发布者生命周期 
 确保发布者的生命周期短于或等于订阅者,或在发布者生命周期结束时主动取消所有订阅。
最佳实践:
- 对于短期存在的对象,确保在不再需要时取消订阅
- 对于长期存在的发布者和临时订阅者,使用弱引用或弱事件模式
- 实现IDisposable接口统一管理订阅的取消
40. 什么是Func和Action委托?
Func<T>和Action<T>是.NET框架预定义的泛型委托,用于简化常见委托场景的代码,避免手动声明委托类型。
Action委托:
- 表示无返回值的方法
- 有多个重载,支持0-16个参数
- 定义:public delegate void Action();、public delegate void Action<T>(T obj);等
Func委托:
- 表示有返回值的方法
- 最后一个类型参数是返回值类型
- 有多个重载,支持0-16个参数(加返回值)
- 定义:public delegate TResult Func<TResult>();、public delegate TResult Func<T, TResult>(T arg);等
使用示例:
public class FuncActionExample
{public static void Main(){// Action<T>示例(无返回值)Action<string> printAction = Console.WriteLine;printAction("使用Action输出");Action<int, int> addAndPrint = (a, b) => Console.WriteLine($"和为: {a + b}");addAndPrint(3, 5);// Func<T>示例(有返回值)Func<int, int, int> addFunc = (a, b) => a + b;int sum = addFunc(2, 4);Console.WriteLine($"sum = {sum}");Func<string, int> stringLength = s => s.Length;int length = stringLength("Hello");Console.WriteLine($"长度 = {length}");// 无参数的FuncFunc<DateTime> getCurrentTime = () => DateTime.Now;Console.WriteLine($"当前时间: {getCurrentTime()}");}
}
输出结果:
使用Action输出
和为: 8
sum = 6
长度 = 5
当前时间: 2023/10/10 15:30:00
常用场景:
-  LINQ查询:大量使用 Func<T>作为查询条件var numbers = new List<int> { 1, 2, 3, 4, 5 }; var evenNumbers = numbers.Where(n => n % 2 == 0); // Where接受Func<int, bool>
-  委托参数:作为方法参数传递操作 public static void ProcessData(int a, int b, Func<int, int, int> processor) {int result = processor(a, b);Console.WriteLine($"处理结果: {result}"); }// 使用 ProcessData(10, 5, (x, y) => x * y); // 传递乘法操作 ProcessData(10, 5, (x, y) => x / y); // 传递除法操作
-  异步编程:与 Task结合使用// 使用Func创建任务 var task = Task.Factory.StartNew(() => {// 耗时操作return 42; });
优势:
- 避免重复声明相似的委托类型,减少代码量
- 提高代码一致性和可读性
- 便于使用Lambda表达式简化代码
Func<T>和Action<T>是C#中非常实用的预定义委托,广泛应用于LINQ、异步编程和各种需要传递方法的场景。
二、120道C#面试题目录列表
| 文章序号 | C#面试题120道 | 
|---|---|
| 1 | C#面试题及详细答案120道(01-10) | 
| 2 | C#面试题及详细答案120道(11-20) | 
| 3 | C#面试题及详细答案120道(21-30) | 
| 4 | C#面试题及详细答案120道(31-40) | 
| 5 | C#面试题及详细答案120道(41-50) | 
| 6 | C#面试题及详细答案120道(51-60) | 
| 7 | C#面试题及详细答案120道(61-75) | 
| 8 | C#面试题及详细答案120道(76-85) | 
| 9 | C#面试题及详细答案120道(86-95) | 
| 10 | C#面试题及详细答案120道(96-105) | 
| 11 | C#面试题及详细答案120道(106-115) | 
| 12 | C#面试题及详细答案120道(116-120) | 
