使用新的async / await模型生成在事件触发时完成的Task
非常简单;你只需要遵循这种模式:
public class MyClass
{
public event Action OnCompletion;
}
public static Task FromEvent(MyClass obj)
{
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
obj.OnCompletion += () =>
{
tcs.SetResult(null);
};
return tcs.Task;
}
然后允许:
await FromEvent(new MyClass());
问题是您需要为每个要FromEvent
的课程中的每个事件创建一个新的await
方法。这可能非常快,而且无论如何它主要只是样板代码。
理想情况下,我希望能够做到这样的事情:
await FromEvent(new MyClass().OnCompletion);
然后我可以对任何实例上的任何事件重复使用相同的FromEvent
方法。我花了一些时间来尝试创建这样的方法,并且存在许多障碍。对于上面的代码,它将生成以下错误:
事件'Namespace.MyClass.OnCompletion'只能出现在+ =或 - =
的左侧
据我所知,没有办法通过代码传递这样的事件。
所以,下一个最好的事情似乎是尝试将事件名称作为字符串传递:
await FromEvent(new MyClass(), "OnCompletion");
它并不理想;如果该类型的事件不存在,你就不会得到intellisense并会收到运行时错误,但它仍然比大量的FromEvent方法更有用。
因此,使用反射和GetEvent(eventName)
来获取EventInfo
对象非常容易。下一个问题是该事件的委托在运行时是未知的(并且需要能够变化)。这使得添加一个事件处理程序变得困难,因为我们需要在运行时动态创建一个方法,匹配一个访问我们已经拥有的TaskCompletionSource
的给定签名(但忽略所有参数)并设置其结果。
幸运的是,我发现了this link,其中包含有关如何通过Reflection.Emit
完全相同的说明。现在问题是我们需要发出IL,我不知道如何访问我的tcs
实例。
以下是我为完成此工作所取得的进展:
public static Task FromEvent<T>(this T obj, string eventName)
{
var tcs = new TaskCompletionSource<object>();
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
的结果?或者,是否存在另一种创建方法的方法,该方法为任意类型的任意事件返回任务?
答案 0 :(得分:23)
你走了:
internal class TaskCompletionSourceHolder
{
private readonly TaskCompletionSource<object[]> m_tcs;
internal object Target { get; set; }
internal EventInfo EventInfo { get; set; }
internal Delegate Delegate { get; set; }
internal TaskCompletionSourceHolder(TaskCompletionSource<object[]> 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<Type, DynamicMethod> s_emittedHandlers =
new Dictionary<Type, DynamicMethod>();
private static void GetDelegateParameterAndReturnTypes(Type delegateType,
out List<Type> 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<Type>(parameters.Length);
for (int i = 0; i < parameters.Length; i++)
parameterTypes.Add(parameters[i].ParameterType);
returnType = invoke.ReturnType;
}
public static Task<object[]> FromEvent<T>(this T obj, string eventName)
{
var tcs = new TaskCompletionSource<object[]>();
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<Type> 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
// i.e. 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 (i.e. *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 (i.e. *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<int, string>
。
答案 1 :(得分:5)
这将为您提供您所需要的而无需做任何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<object>();
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<T> style)
static Task<T> TaskFromEvent<T>(object target, string eventName) {
var addMethod = target.GetType().GetEvent(eventName).GetAddMethod();
var delegateType = addMethod.GetParameters()[0].ParameterType;
var tcs = new TaskCompletionSource<T>();
var resultSetter = (Action<T>)tcs.SetResult;
var d = Delegate.CreateDelegate(delegateType, resultSetter, "Invoke");
addMethod.Invoke(target, new object[] { d });
return tcs.Task;
}
// Two-value events (Action<T1, T2> or EventHandler style)
static Task<Tuple<T1, T2>> TaskFromEvent<T1, T2>(object target, string eventName) {
var addMethod = target.GetType().GetEvent(eventName).GetAddMethod();
var delegateType = addMethod.GetParameters()[0].ParameterType;
var tcs = new TaskCompletionSource<Tuple<T1, T2>>();
var resultSetter = (Action<T1, T2>)((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<int, string>(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;
}
Here's a helper function如果以上三种方法对您的首选项进行了太多的复制和粘贴,那么您将只允许在每行中编写一个TaskFromEvent函数。必须给予最大信用以简化我原来的。
答案 2 :(得分:2)
如果您愿意为每个委托类型设置一个方法,则可以执行以下操作:
Task FromEvent(Action<Action> add)
{
var tcs = new TaskCompletionSource<bool>();
add(() => tcs.SetResult(true));
return tcs.Task;
}
您可以使用它:
await FromEvent(x => new MyClass().OnCompletion += x);
请注意,通过这种方式,您永远不会取消订阅活动,这对您来说可能是也可能不是问题。
如果您正在使用泛型委托,每种泛型类型一个方法就足够了,每个具体类型都不需要一个方法:
Task<T> FromEvent<T>(Action<Action<T>> add)
{
var tcs = new TaskCompletionSource<T>();
add(x => tcs.SetResult(x));
return tcs.Task;
}
虽然类型推断不适用于此,但您必须明确指定类型参数(假设此处OnCompletion
的类型为Action<string>
):
string s = await FromEvent<string>(x => c.OnCompletion += x);
答案 3 :(得分:-1)
我面对这种问题,尝试为GetAwaiter
写System.Action
扩展方法,忘记System.Action
是不可变的,并通过将其作为参数传递来进行复制。但是,如果使用ref
关键字传递副本,则不会进行复制,因此:
public static class AwaitExtensions
{
public static Task FromEvent(ref Action action)
{
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
action += () => tcs.SetResult(null);
return tcs.Task;
}
}
用法:
await AwaitExtensions.FromEvent(ref OnActionFinished);
注意:TCS侦听器保持订阅状态