接口回调本质是委托而非实现接口,C#中用Action/Func等泛型委托承载回调逻辑,事件只是对委托的封装;需区分通知(event)与策略注入(Action/Func)语义。

接口回调本质是委托,不是“实现接口”
很多人卡在“C# 接口回调”这个说法上,以为要让类实现某个叫 ICallback 的接口。其实 C# 里没这回事——真正承载回调逻辑的是 delegate,事件(event)只是对委托的封装加了一层访问控制。
典型错误现象:CS0246 找不到类型 ICallback,或者写完接口却不知道怎么“传给被调用方”。根本原因:混淆了 Java/Android 风格的接口回调和 C# 的委托模型。
- 委托才是回调的载体,比如
Action<string>、Func<int, bool>或自定义delegate void DataReceivedHandler(string data) - 事件本质是
public event EventHandler<EventArgs> MyEvent,背后仍是委托字段,只是编译器限制了外部直接赋值 - 如果你真需要“接口+回调”,通常是为了解耦,那应该用依赖注入 + 抽象服务接口(如
IDataProcessor),而不是硬套“回调接口”
用 Action 和 Func 快速传回调,别急着写 delegate
90% 的简单回调场景,Action 和 Func 足够,不用声明新 delegate 类型。它们是框架内置泛型委托,语义清晰、零额外类型开销。
使用场景:异步加载完成通知、按钮点击后处理、数据校验失败时的提示逻辑。
-
Action表示无返回值:比如LoadDataAsync(Action<string> onSuccess, Action<Exception> onError) -
Func<T>表示有返回值:比如RetryPolicy<bool>(Func<bool> operation) - 参数顺序很重要:习惯把回调放在最后,方便 C# 7.0+ 的尾随 lambda 写法:
DoWork(() => Console.WriteLine("done")) - 避免捕获大对象:lambda 里引用
this或大型局部变量会延长生命周期,可能引发内存泄漏
event 不是“更安全的 delegate”,而是单向订阅机制
很多人以为 event 是 delegate 的“升级版”,能防误调用。其实它只限制外部代码不能直接赋值(myObj.OnComplete = null 报错),但依然允许 += / -=,且触发逻辑完全由类内部控制。
常见错误现象:外部想“清空所有监听器”,结果发现只能 -= 自己加的;或误以为 event 能自动线程安全,实际不加锁照样并发出问题。
- 触发事件必须判空:
MyEvent?.Invoke(this, e),否则NullReferenceException - 多线程下 += / -= 和触发需同步:推荐用
lock或Interlocked.CompareExchange管理委托链(小项目用lock更直觉) - 事件不适合“请求-响应”:比如你发个事件问“要不要继续?”,没人能可靠返回值——这时该用
Func<bool>参数 - 别在事件里做耗时操作:UI 线程中触发
event后执行 500ms 逻辑,界面就卡死
跨线程触发事件或回调,InvokeRequired 已过时
.NET 6+ WinForms/WPF 中,Control.InvokeRequired 和 Dispatcher.CheckAccess() 还能用,但语义弱、易漏判。现代写法应统一走 SynchronizationContext 或 DispatcherQueue(WinUI 3)。
使用场景:后台线程下载完成,要更新 UI 控件文本。
- WinForms 推荐:
this.BeginInvoke(new Action(() => label.Text = "Done")),比手写if (InvokeRequired)更简洁 - WPF 推荐:
Application.Current.Dispatcher.BeginInvoke(...),注意不要用Invoke(会阻塞线程) - 通用方案(含 Console/ASP.NET):用
TaskScheduler.FromCurrentSynchronizationContext()捕获上下文,再Task.Factory.StartNew(..., scheduler) - 最容易被忽略的一点:async 方法里 await 之后的延续上下文,默认就是捕获的
SynchronizationContext,所以await Task.Delay(100); label.Text = "ok"在 WinForms 里天然线程安全
委托和事件的边界从来不在语法,而在语义:你要的是“通知”(event),还是“策略注入”(Action/Func)?选错就容易绕进线程、生命周期、空引用一堆坑里。


















