You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现通用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;
    }
}

关键逻辑说明

  1. 表达式树构建委托:通过Expression类动态构建与TEventHandler同类型的委托,避免了动态编译的依赖,完全兼容.NET WASM的限制。
  2. 递归委托引用:用Expression.Variable声明委托变量,解决了"在委托内部引用自身以取消订阅"的问题。
  3. 异常安全处理:通过TryCatch块确保无论业务handler是否抛出异常,都会取消订阅并正确设置TCS的状态,避免内存泄漏或Task永远挂起。
  4. 异步延续优化:创建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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 02:27:46