通用FromEvent方法

使用新的async / await模型,生成一个在事件触发时完成的Task非常简单; 你只需要遵循这种模式:

 public class MyClass { public event Action OnCompletion; } public static Task FromEvent(MyClass obj) { TaskCompletionSource tcs = new TaskCompletionSource(); obj.OnCompletion += () => { tcs.SetResult(null); }; return tcs.Task; } 

这允许:

 await FromEvent(new MyClass()); 

问题是您需要为每个要await类中的每个事件创建一个新的FromEvent方法。 这可能会变得非常快,而且它主要只是样板代码。

理想情况下,我希望能够做到这样的事情:

 await FromEvent(new MyClass().OnCompletion); 

然后我可以为任何实例上的任何事件重用相同的FromEvent方法。 我花了一些时间来尝试创建这样的方法,并且存在许多障碍。 对于上面的代码,它将生成以下错误:

事件’Namespace.MyClass.OnCompletion’只能出现在+ =或 – =的左侧

据我所知,没有办法通过代码传递这样的事件。

所以,下一个最好的事情似乎是尝试将事件名称作为字符串传递:

 await FromEvent(new MyClass(), "OnCompletion"); 

它并不理想; 如果该类型的事件不存在,则不会获得智能感知并且会出现运行时错误,但它仍然比大量的FromEvent方法更有用。

因此,使用reflection和GetEvent(eventName)来获取EventInfo对象非常容易。 下一个问题是该事件的委托在运行时是未知的(并且需要能够变化)。 这使得添加事件处理程序变得困难,因为我们需要在运行时动态创建方法,匹配访问我们已经拥有的TaskCompletionSource的给定签名(但忽略所有参数)并设置其结果。

幸运的是,我发现此链接包含有关如何通过Reflection.Emit完成[几乎]的说明。 现在的问题是我们需要发出IL,我不知道如何访问我拥有的tcs实例。

以下是我为完成此任务所取得的进展:

 public static Task FromEvent(this T obj, string eventName) { var tcs = new TaskCompletionSource(); var eventInfo = obj.GetType().GetEvent(eventName); Type eventDelegate = eventInfo.EventHandlerType; Type[] parameterTypes = GetDelegateParameterTypes(eventDelegate); DynamicMethod handler = new DynamicMethod("unnamed", null, parameterTypes); ILGenerator ilgen = handler.GetILGenerator(); //TODO ilgen.Emit calls go here Delegate dEmitted = handler.CreateDelegate(eventDelegate); eventInfo.AddEventHandler(obj, dEmitted); return tcs.Task; } 

我可能发出什么IL才能让我设置TaskCompletionSource的结果? 或者,是否存在另一种创建方法的方法,该方法从任意类型返回任意事件的任务?

干得好:

 internal class TaskCompletionSourceHolder { private readonly TaskCompletionSource m_tcs; internal object Target { get; set; } internal EventInfo EventInfo { get; set; } internal Delegate Delegate { get; set; } internal TaskCompletionSourceHolder(TaskCompletionSource tsc) { m_tcs = tsc; } private void SetResult(params object[] args) { // this method will be called from emitted IL // so we can set result here, unsubscribe from the event // or do whatever we want. // object[] args will contain arguments // passed to the event handler m_tcs.SetResult(args); EventInfo.RemoveEventHandler(Target, Delegate); } } public static class ExtensionMethods { private static Dictionary s_emittedHandlers = new Dictionary(); private static void GetDelegateParameterAndReturnTypes(Type delegateType, out List parameterTypes, out Type returnType) { if (delegateType.BaseType != typeof(MulticastDelegate)) throw new ArgumentException("delegateType is not a delegate"); MethodInfo invoke = delegateType.GetMethod("Invoke"); if (invoke == null) throw new ArgumentException("delegateType is not a delegate."); ParameterInfo[] parameters = invoke.GetParameters(); parameterTypes = new List(parameters.Length); for (int i = 0; i < parameters.Length; i++) parameterTypes.Add(parameters[i].ParameterType); returnType = invoke.ReturnType; } public static Task FromEvent(this T obj, string eventName) { var tcs = new TaskCompletionSource(); var tcsh = new TaskCompletionSourceHolder(tcs); EventInfo eventInfo = obj.GetType().GetEvent(eventName); Type eventDelegateType = eventInfo.EventHandlerType; DynamicMethod handler; if (!s_emittedHandlers.TryGetValue(eventDelegateType, out handler)) { Type returnType; List parameterTypes; GetDelegateParameterAndReturnTypes(eventDelegateType, out parameterTypes, out returnType); if (returnType != typeof(void)) throw new NotSupportedException(); Type tcshType = tcsh.GetType(); MethodInfo setResultMethodInfo = tcshType.GetMethod( "SetResult", BindingFlags.NonPublic | BindingFlags.Instance); // I'm going to create an instance-like method // so, first argument must an instance itself // ie TaskCompletionSourceHolder *this* parameterTypes.Insert(0, tcshType); Type[] parameterTypesAr = parameterTypes.ToArray(); handler = new DynamicMethod("unnamed", returnType, parameterTypesAr, tcshType); ILGenerator ilgen = handler.GetILGenerator(); // declare local variable of type object[] LocalBuilder arr = ilgen.DeclareLocal(typeof(object[])); // push array's size onto the stack ilgen.Emit(OpCodes.Ldc_I4, parameterTypesAr.Length - 1); // create an object array of the given size ilgen.Emit(OpCodes.Newarr, typeof(object)); // and store it in the local variable ilgen.Emit(OpCodes.Stloc, arr); // iterate thru all arguments except the zero one (ie *this*) // and store them to the array for (int i = 1; i < parameterTypesAr.Length; i++) { // push the array onto the stack ilgen.Emit(OpCodes.Ldloc, arr); // push the argument's index onto the stack ilgen.Emit(OpCodes.Ldc_I4, i - 1); // push the argument onto the stack ilgen.Emit(OpCodes.Ldarg, i); // check if it is of a value type // and perform boxing if necessary if (parameterTypesAr[i].IsValueType) ilgen.Emit(OpCodes.Box, parameterTypesAr[i]); // store the value to the argument's array ilgen.Emit(OpCodes.Stelem, typeof(object)); } // load zero-argument (ie *this*) onto the stack ilgen.Emit(OpCodes.Ldarg_0); // load the array onto the stack ilgen.Emit(OpCodes.Ldloc, arr); // call this.SetResult(arr); ilgen.Emit(OpCodes.Call, setResultMethodInfo); // and return ilgen.Emit(OpCodes.Ret); s_emittedHandlers.Add(eventDelegateType, handler); } Delegate dEmitted = handler.CreateDelegate(eventDelegateType, tcsh); tcsh.Target = obj; tcsh.EventInfo = eventInfo; tcsh.Delegate = dEmitted; eventInfo.AddEventHandler(obj, dEmitted); return tcs.Task; } } 

此代码几乎适用于所有返回void的事件(无论参数列表如何)。

如有必要,可以对其进行改进以支持任何返回值。

您可以在下面看到Dax和我的方法之间的区别:

 static async void Run() { object[] result = await new MyClass().FromEvent("Fired"); Console.WriteLine(string.Join(", ", result.Select(arg => arg.ToString()).ToArray())); // 123, abcd } public class MyClass { public delegate void TwoThings(int x, string y); public MyClass() { new Thread(() => { Thread.Sleep(1000); Fired(123, "abcd"); }).Start(); } public event TwoThings Fired; } 

简而言之,我的代码支持任何类型的委托类型。 您不应该(并且不需要)显式地指定它,如TaskFromEvent

这将为您提供您所需要的而无需做任何ilgen,并且更简单。 它适用于任何类型的事件代表; 您只需为事件委托中的每个参数数创建不同的处理程序。 以下是0..2所需的处理程序,这应该是绝大多数用例。 扩展到3及以上是从2参数方法的简单复制和粘贴。

这也比ilgen方法更强大,因为您可以在异步模式中使用事件创建的任何值。

 // Empty events (Action style) static Task TaskFromEvent(object target, string eventName) { var addMethod = target.GetType().GetEvent(eventName).GetAddMethod(); var delegateType = addMethod.GetParameters()[0].ParameterType; var tcs = new TaskCompletionSource(); var resultSetter = (Action)(() => tcs.SetResult(null)); var d = Delegate.CreateDelegate(delegateType, resultSetter, "Invoke"); addMethod.Invoke(target, new object[] { d }); return tcs.Task; } // One-value events (Action style) static Task TaskFromEvent(object target, string eventName) { var addMethod = target.GetType().GetEvent(eventName).GetAddMethod(); var delegateType = addMethod.GetParameters()[0].ParameterType; var tcs = new TaskCompletionSource(); var resultSetter = (Action)tcs.SetResult; var d = Delegate.CreateDelegate(delegateType, resultSetter, "Invoke"); addMethod.Invoke(target, new object[] { d }); return tcs.Task; } // Two-value events (Action or EventHandler style) static Task> TaskFromEvent(object target, string eventName) { var addMethod = target.GetType().GetEvent(eventName).GetAddMethod(); var delegateType = addMethod.GetParameters()[0].ParameterType; var tcs = new TaskCompletionSource>(); var resultSetter = (Action)((t1, t2) => tcs.SetResult(Tuple.Create(t1, t2))); var d = Delegate.CreateDelegate(delegateType, resultSetter, "Invoke"); addMethod.Invoke(target, new object[] { d }); return tcs.Task; } 

使用就像这样。 如您所见,即使事件是在自定义委托中定义的,它仍然有效。 并且您可以将公平值捕获为元组。

 static async void Run() { var result = await TaskFromEvent(new MyClass(), "Fired"); Console.WriteLine(result); // (123, "abcd") } public class MyClass { public delegate void TwoThings(int x, string y); public MyClass() { new Thread(() => { Thread.Sleep(1000); Fired(123, "abcd"); }).Start(); } public event TwoThings Fired; } 

这里有一个帮助函数 ,如果上述三种方法对于您的首选项进行了太多的复制和粘贴,则允许您在每行中只编写一行TaskFromEvent函数。 必须给予最大信用以简化我最初的工作。

如果您愿意为每个委托类型设置一个方法,则可以执行以下操作:

 Task FromEvent(Action add) { var tcs = new TaskCompletionSource(); add(() => tcs.SetResult(true)); return tcs.Task; } 

您会像以下一样使用它:

 await FromEvent(x => new MyClass().OnCompletion += x); 

请注意,这种方式您永远不会取消订阅该活动,这可能会也可能不是您的问题。

如果您使用generics委托,每种generics类型一个方法就足够了,每个具体类型不需要一个方法:

 Task FromEvent(Action> add) { var tcs = new TaskCompletionSource(); add(x => tcs.SetResult(x)); return tcs.Task; } 

虽然类型推断不适用于此,但您必须显式指定类型参数(假设OnCompletion的类型为Action ):

 string s = await FromEvent(x => c.OnCompletion += x);