c# - 通用 FromEvent 方法

标签 c# async-await task-parallel-library cil reflection.emit

使用新的 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 FromEvent(new MyClass().OnCompletion);

然后我可以为任何实例上的任何事件重复使用相同的 FromEvent 方法。我花了一些时间尝试创建这样的方法,但遇到了很多问题。对于上面的代码,它将产生以下错误:

The event 'Namespace.MyClass.OnCompletion' can only appear on the left hand side of += or -=

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

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

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

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

因此很容易使用反射和 GetEvent(eventName) 来获取 EventInfo 对象。下一个问题是该事件的委托(delegate)在运行时是未知的(并且需要能够改变)。这使得添加事件处理程序变得困难,因为我们需要在运行时动态创建一个方法,匹配给定的签名(但忽略所有参数)访问我们已经拥有的 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 的结果?或者,是否有另一种方法来创建一个方法,该方法为任意类型的任意事件返回任务?

最佳答案

给你:

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;
}

简单地说,我的代码真正支持任何类型的委托(delegate)类型。您不应该(也不需要)像 TaskFromEvent<int, string> 那样明确指定它.

关于c# - 通用 FromEvent 方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12865848/

相关文章:

c# - await 关键字可以生成并行代码吗?

c# - 我如何随机订购 IEnumerable<>?

c# - 如何在 FakeItEasy 中为基于自定义委托(delegate)的事件引发事件?

node.js - 在 swagger Node 项目中使用异步中间件功能

c# - Dapper SqlException 和 Unobserved 异常

c# - 在任务中进行异步/等待数据库调用?

c# - https ://<myapp>. azurewebsites.net/.auth/login/aad/callback 是什么意思?

c# - 如何解析 C# 的命令行输出?

c# - 如何复制 HttpContent 异步和可取消?

javascript - 为什么这些 promise 没有解决?