如何实现通用C#方法:事件仅处理一次后立即取消订阅
实现通用的单次事件订阅异步方案
针对需要实现事件触发一次后自动取消订阅并返回Task的通用需求,这里提供一个基于表达式树的可行方案,完全适配.NET WASM环境(无需动态编译),且能兼容任意类型的事件委托:
核心实现代码
using System.Linq.Expressions; public static class EventExtensions { public static Task SubscribeOnceAsync<TEventHandler>( TEventHandler handler, Action<TEventHandler> addHandler, Action<TEventHandler> removeHandler) where TEventHandler : Delegate { var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); var handlerType = typeof(TEventHandler); // 获取事件委托的Invoke方法参数 var invokeMethod = handlerType.GetMethod("Invoke") ?? throw new InvalidOperationException($"委托类型 {handlerType.Name} 没有Invoke方法"); var parameters = invokeMethod.GetParameters() .Select(p => Expression.Parameter(p.ParameterType, p.Name)) .ToArray(); // 声明变量用于引用即将创建的委托(递归引用) var delegateVar = Expression.Variable(handlerType, "self"); // 1. 调用传入的业务handler var callHandler = Expression.Invoke(Expression.Constant(handler), parameters); // 2. 调用removeHandler移除当前委托 var callRemoveHandler = Expression.Invoke(Expression.Constant(removeHandler), delegateVar); // 3. 设置TaskCompletionSource的结果 var setResult = Expression.Call(Expression.Constant(tcs), nameof(TaskCompletionSource<object>.SetResult), null, Expression.Constant(null)); // 处理异常:如果handler抛出异常,设置TCS的异常并移除订阅 var tryBlock = Expression.TryCatch( Expression.Block(callHandler, callRemoveHandler, setResult), Expression.Catch( Expression.Parameter(typeof(Exception), "ex"), Expression.Block( Expression.Invoke(Expression.Constant(removeHandler), delegateVar), Expression.Call(Expression.Constant(tcs), nameof(TaskCompletionSource<object>.SetException), null, Expression.Constant(new[] { (Exception)Expression.Variable(typeof(Exception), "ex") })) ) ) ); // 构建完整的委托逻辑块 var block = Expression.Block( new[] { delegateVar }, Expression.Assign(delegateVar, Expression.Lambda(handlerType, tryBlock, parameters)), Expression.Invoke(Expression.Constant(addHandler), delegateVar), delegateVar ); // 编译并执行,完成订阅 var compiledDelegate = (TEventHandler)block.Compile().DynamicInvoke(); return tcs.Task; } }
关键逻辑说明
- 表达式树构建委托:通过
Expression类动态构建与TEventHandler同类型的委托,避免了动态编译的依赖,完全兼容.NET WASM的限制。 - 递归委托引用:用
Expression.Variable声明委托变量,解决了"在委托内部引用自身以取消订阅"的问题。 - 异常安全处理:通过
TryCatch块确保无论业务handler是否抛出异常,都会取消订阅并正确设置TCS的状态,避免内存泄漏或Task永远挂起。 - 异步延续优化:创建
TaskCompletionSource时使用TaskCreationOptions.RunContinuationsAsynchronously,避免同步延续可能导致的死锁问题。
使用示例
1. INotifyPropertyChanged场景
INotifyPropertyChanged inpc = GetInpcInstance(); await EventExtensions.SubscribeOnceAsync<PropertyChangedEventHandler>( (sender, e) => { if (e.PropertyName == "TargetProperty") { // 执行单次业务逻辑 Console.WriteLine("目标属性已变更,仅执行一次"); } }, handler => inpc.PropertyChanged += handler, handler => inpc.PropertyChanged -= handler );
2. 自定义多参数事件场景
假设你有一个自定义事件委托:
public delegate void CustomEventHandler(string arg1, int arg2, bool arg3); public class CustomEventSource { public event CustomEventHandler CustomEvent; public void RaiseEvent() => CustomEvent?.Invoke("test", 123, true); }
使用方式:
var eventSource = new CustomEventSource(); await EventExtensions.SubscribeOnceAsync<CustomEventHandler>( (arg1, arg2, arg3) => { Console.WriteLine($"自定义事件触发:{arg1}, {arg2}, {arg3}"); }, handler => eventSource.CustomEvent += handler, handler => eventSource.CustomEvent -= handler ); // 触发事件,只会执行一次 eventSource.RaiseEvent(); eventSource.RaiseEvent(); // 第二次触发不会执行业务逻辑
注意事项
- 确保
addHandler和removeHandler正确对应事件的订阅/取消订阅逻辑,否则会导致订阅无法移除。 - 对于线程安全的事件,该实现本身是线程安全的,因为
TaskCompletionSource的状态设置是原子性的,且异常处理块保证了订阅会被移除。
内容的提问来源于stack exchange,提问作者Emperor Eto
相关产品推荐
相关产品推荐

