用于处理一次事件然后取消订阅的通用 C# 方法

Generalized C# Method for Handling an Event Once and Then Unsubscribing

提问人:Emperor Eto 提问时间:2/17/2023 更新时间:2/28/2023 访问量:211

问:

我正在尝试设计一种通用的方法来实现我认为非常常见的模式,即需要处理一次类事件才能设置结果,并立即取消订阅。该模式如下所示:TaskCompletionSource

Task DoSomethingAfterAnEventHasBeenTriggeredOnceAsync() 
{
     var tcs = new TaskCompletionSource<object>();
     SomeEventHandlerDelegate handler = null;
     handler = new SomeEventHandlerDelegate((p1,p2,p3) => 
     {
        // do my thing
        // ...
        someObj.SomeEvent -= handler;
        tcs.SetResult(null);
     });
     someObj.SomeEvent += handler;
     return tcs.Task;
}

我最初的想法是按照这些思路制作一个通用方法:

    public static Task SubscribeOnceAsync<Tsender, Tdel>(
        Tdel handler,
        Action<Tdel> addHandler, 
        Action<Tdel> removeHandler)
        where Tdel: System.Delegate
    {
        TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();

        // ???
        // somehow create a new delegate to send to 
        // "addHandler" that calls "handler",
        // then calls "removeHandler", and then
        // sets the tcs result
        // ???
    }

这将像这样消耗:

     INotifyPropertyChanged inpc; 
     // ...
     await SubscribeOnceAsync<INotifyPropertyChanged, PropertyChangedEventHandler>(
         (s, e) =>
         {                
             // do my one time thing
         },
         (s, h) => inpc.PropertyChanged += h, 
         (s, h) => inpc.PropertyChanged -= h);

问题在于动态创建可以提供给 和 的委托。对于这两个委托必须是同一类型,因此我实际上必须动态创建一个调用 和 的类型委托。addHandlerremoveHandlerDelegate.CombineTdelremoveHandlertcs.SetResult

我想也许可以对动态编译做一些事情,但这最终将与 .NET WASM 一起使用,因此考虑到运行时的脾气暴躁,我对走这条路持谨慎态度。

所以我甚至不确定我最初的想法是否是做到这一点的最佳方法,但想不出任何其他方法来解决这个问题。有什么想法吗?

请注意,这需要与使用 s 的现有代码一起使用,因此该模式不是一个选项。eventIObservable

C# 异步 泛型 事件 委托

评论


答:

2赞 Olivier Jacot-Descombes 2/18/2023 #1

我假设事件被声明为

public EventHandler<SomeEventArgs> SomeEvent;

我的想法是提供一个代理委托,该委托取消订阅事件,然后执行原始委托(我称之为)。action

public static Task SubscribeOnceAsync<TArgs>(
    Action<EventHandler<TArgs>> addHandler,
    Action<EventHandler<TArgs>> removeHandler,
    Action<object, TArgs> action) where TArgs : EventArgs
{
    TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
    addHandler(Proxy); 
    return tcs.Task;
           
    void Proxy(object sender, TArgs args)
    {
        removeHandler(Proxy);
        action(sender, args);
        tcs.SetResult(null);
    }
}

请注意,它被声明为捕获和 的局部函数。因此,它不需要将它们作为额外的参数传递,因此具有与原始事件处理程序相同的签名。ProxyremoveHandleraction

它将像这样消耗:

static async Task TestSubscribeOnceAsync()
{
    var someObj = new EventClass();

    await SubscribeOnceAsync<SomeEventArgs>(
        eh => someObj.SomeEvent += eh,
        eh => someObj.SomeEvent -= eh,
        (sender, args) => {
            Console.WriteLine("do my thing");
        }
    );
}

评论

0赞 Emperor Eto 2/18/2023
哦,现在这就是为什么我很高兴我发布了这个问题。我从没想过要做一个本地功能。这很耐人寻味。让我稍微玩一下,看看我是否能做到这一点。我希望它理想地比仅仅泛泛一点,这样可能就没有办法对此进行多次重载,每个 # 个参数支持一个,但这仍然是一次易。谢谢,我会及时通知你。EventHandler<T>
0赞 Olivier Jacot-Descombes 2/18/2023
这个想法是让一个参数类具有与您需要的参数一样多的属性:.因此,您只需要一个重载。您甚至可以通过这些参数从事件处理程序返回结果。EventHandler<MyArgumentsWithFiveParameters>
0赞 Emperor Eto 2/18/2023
是的,但我希望它适用于我无法控制的对象/事件,例如 INotifyPropertyChanged。
0赞 Emperor Eto 2/19/2023
是的,所以我尝试了很多排列 - 它绝对适用于任何遵循该模式的东西,但不幸的是,如果我们想将它与不遵循该模式的代码一起使用,委托类型问题似乎仍然是不可避免的。我似乎无法让编译器接受内联函数作为泛型委托类型的可行源,以至于它可以 -able。我使用一个类提出了一个潜在的解决方案,我将将其作为答案发布,希望您或其他人可以想到改进。EventHandler<T>+=
0赞 Emperor Eto 2/19/2023
但不要误会我的意思,这是一个很好的选择,谢谢。如果你不介意,我会建议一些编辑来适应我所想到的模式。TaskCompletionSource
0赞 Emperor Eto 2/19/2023 #2

这就是我想出的,它使用类而不是静态方法。如果我们想在现有代码中支持任何通用的 、 或其他代码,委托类型在这里被证明是一个重大挑战。event

这不是看起来最优雅的代码,它确实有固有的局限性,但它在实践中非常有用。

一、基类:

public class OneTimeObserver<Tsender, Tdel, Tdelarg1, Tdelarg2, Tdelarg3, Tdelarg4, Tdelarg5>
    where Tdel : Delegate
{
    Tdel _theirDel;
    Tdel _ourDel;
    Action<Tsender, Tdel> _unsub;
    TaskCompletionSource<object> _tcs;
    Tsender _sender;

    protected OneTimeObserver(
        Tsender sender,
        Tdel del,
        Action<Tsender, Tdel> sub,
        Action<Tsender, Tdel> unsub,
        int argCount)
    {
        _sender = sender;
        _unsub = unsub;
        _theirDel = del;
        _tcs = new TaskCompletionSource<object>();

        string methodName = $"Observe{argCount}";

        _ourDel = (Tdel)Delegate.CreateDelegate(typeof(Tdel), this, methodName);
        sub(_sender, _ourDel);
    }

    public Task Task => _tcs.Task;

    protected void Observe0()
    {
        _unsub(_sender, _ourDel);
        _theirDel?.DynamicInvoke();
        _tcs.SetResult(null);
    }

    protected void Observe1(Tdelarg1 arg1)
    {
        _unsub(_sender, _ourDel);
        _theirDel?.DynamicInvoke(arg1);
        _tcs.SetResult(null);
    }

    protected void Observe2(Tdelarg1 arg1, Tdelarg2 arg2)
    {
        _unsub(_sender, _ourDel);
        _theirDel?.DynamicInvoke(arg1, arg2);
        _tcs.SetResult(null);
    }

    protected void Observe3(Tdelarg1 arg1, Tdelarg2 arg2, Tdelarg3 arg3)
    {
        _unsub(_sender, _ourDel);
        _theirDel?.DynamicInvoke(arg1, arg2, arg3);
        _tcs.SetResult(null);
    }

    protected void Observe4(Tdelarg1 arg1, Tdelarg2 arg2, Tdelarg3 arg3, Tdelarg4 arg4)
    {
        _unsub(_sender, _ourDel);
        _theirDel?.DynamicInvoke(arg1, arg2, arg3, arg4);
        _tcs.SetResult(null);
    }

    protected void Observe5(Tdelarg1 arg1, Tdelarg2 arg2, Tdelarg3 arg3, Tdelarg4 arg4, Tdelarg5 arg5)
    {
        _unsub(_sender, _ourDel);
        _theirDel?.DynamicInvoke(arg1, arg2, arg3, arg4, arg5);
        _tcs.SetResult(null);
    }
}

当然,限制是支持的参数数量,而不优雅来自于这样一个事实,即我们需要为每个可能的参数数量提供一个方法,根据委托类型中的参数数量,最终只调用其中一个参数(因此超出此范围的类型参数将被忽略,并且可以全部设置为)。Observe{N}object

就其本身而言,使用起来会非常笨拙,但是如果我为我想要支持的每个/组合声明一个子类,事情就会变得更容易管理,例如:objectevent

public class OneTimePropertyChangeObserver : OneTimeObserver<
    INotifyPropertyChanged, 
    PropertyChangedEventHandler, 
    object, 
    PropertyChangedEventArgs, 
    object,
    object,
    object>
{
    public OneTimePropertyChangeObserver(INotifyPropertyChanged sender, PropertyChangedEventHandler handler) :
        base(
            sender,
            handler,
            (inpc, d) => inpc.PropertyChanged += d,
            (inpc, d) => inpc.PropertyChanged -= d,
            2)
    {
    }
}

可以食用:

await new OneTimePropertyChangeObserver(obj, (sender, e) =>
{
    // do my thing
}).Task;

如果有人能想出一种方法来解决参数/委托问题,并且仍然支持任意委托模式,我绝对希望看到它!event