提问人:Emperor Eto 提问时间:2/17/2023 更新时间:2/28/2023 访问量:211
用于处理一次事件然后取消订阅的通用 C# 方法
Generalized C# Method for Handling an Event Once and Then Unsubscribing
问:
我正在尝试设计一种通用的方法来实现我认为非常常见的模式,即需要处理一次类事件才能设置结果,并立即取消订阅。该模式如下所示: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);
问题在于动态创建可以提供给 和 的委托。对于这两个委托必须是同一类型,因此我实际上必须动态创建一个调用 和 的类型委托。addHandler
removeHandler
Delegate.Combine
Tdel
removeHandler
tcs.SetResult
我想也许可以对动态编译做一些事情,但这最终将与 .NET WASM 一起使用,因此考虑到运行时的脾气暴躁,我对走这条路持谨慎态度。
所以我甚至不确定我最初的想法是否是做到这一点的最佳方法,但想不出任何其他方法来解决这个问题。有什么想法吗?
请注意,这需要与使用 s 的现有代码一起使用,因此该模式不是一个选项。event
IObservable
答:
我假设事件被声明为
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);
}
}
请注意,它被声明为捕获和 的局部函数。因此,它不需要将它们作为额外的参数传递,因此具有与原始事件处理程序相同的签名。Proxy
removeHandler
action
它将像这样消耗:
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");
}
);
}
评论
EventHandler<T>
EventHandler<MyArgumentsWithFiveParameters>
EventHandler<T>
+=
TaskCompletionSource
这就是我想出的,它使用类而不是静态方法。如果我们想在现有代码中支持任何通用的 、 或其他代码,委托类型在这里被证明是一个重大挑战。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
就其本身而言,使用起来会非常笨拙,但是如果我为我想要支持的每个/组合声明一个子类,事情就会变得更容易管理,例如:object
event
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
评论