【问题标题】:General purpose FromEvent method通用 FromEvent 方法
【发布时间】:2020-03-25 03:12:36
【问题描述】:

使用新的 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());

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

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

await FromEvent(new MyClass().OnCompletion);

然后我可以对任何实例上的任何事件重复使用相同的 FromEvent 方法。我花了一些时间尝试创建这样的方法,但有很多障碍。上面的代码会产生如下错误:

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

据我所知,永远不会有这样的方式通过代码传递事件。

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

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

这并不理想;如果该类型的事件不存在,您不会获得智能感知,并且会出现运行时错误,但它仍然可能比大量的 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 的结果?或者,是否有另一种方法来创建一个方法,该方法为任意类型的任意事件返回任务?

【问题讨论】:

  • 请注意,BCL 有TaskFactory.FromAsync 可以轻松地从 APM 转换为 TAP。没有一种简单的通用方法可以将 EAP 转换为 TAP,所以我认为这就是 MS 没有包含这样的解决方案的原因。无论如何,我发现 Rx(或 TPL 数据流)更接近于“事件”语义 - 并且 Rx 确实 有一个 FromEvent 类型的方法。
  • 我还想做一个通用的FromEvent&lt;&gt;this 很接近,因为我可以在不使用反射的情况下做到这一点。

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


【解决方案1】:

给你:

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&lt;int, string&gt; 那样明确指定它。

【讨论】:

  • 我刚刚浏览完您的更新并尝试了一下。我真的很喜欢它。事件处理程序已取消订阅,这是一个很好的接触。缓存各种事件处理程序,因此不会为相同类型重复生成 IL,并且与其他解决方案不同,无需指定事件处理程序的参数类型。
  • 我无法让代码在 windows phone 上运行,不知道是不是安全问题。但没用.. 异常:{“尝试访问该方法失败:System.Reflection.Emit.DynamicMethod..ctor(System.String, System.Type, System.Type[], System.Type)”}
  • @J.Lennon 不幸的是,我无法在 Windows Phone 上测试它。所以如果你能尝试使用这个updated version 并让我知道它是否有帮助,我将非常感激。提前致谢。
  • @J.Lennon 我认为 Servy 在他随后的评论中提到了这一点。也可能存在一些性能差异(不知道在哪些情况下不对其进行分析会更快),但事件订阅或触发器无论如何都不太可能成为瓶颈。
  • 您知道是否可以使用表达式树来简化此操作,还是需要低级 ilgen?
【解决方案2】:

这将为您提供所需的东西,而无需做任何 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 函数。必须归功于 max 以简化我最初的工作。

【讨论】:

  • 非常感谢!!!对于 windows phone,此行必须修改: var parameters = methodInfo.GetParameters().Select(a => System.Linq.Expressions.Expression.Parameter(a.ParameterType, a.Name)).ToArray();
【解决方案3】:

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

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&lt;string&gt;):

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

【讨论】:

  • 这里的主要问题是,许多 UI 框架为每个事件创建自己的委托类型(而不是使用 Action&lt;T&gt;/EventHandler&lt;T&gt;),而这就是这样的事情最多的地方有用,因此为每个委托类型创建一个FromEvent 方法会更好,但仍然不完美。也就是说,您可以在任何事件中使用您制作和使用的第一种方法:await FromEvent(x =&gt; new MyClass().OnCompletion += (a,b)=&gt; x());。这是一种半途而废的解决方案。
  • @Servy 是的,我也想过这样做,但我没有提到它,因为我认为它很难看(即样板太多)。
【解决方案4】:

我尝试为System.Action 编写GetAwaiter 扩展方法时遇到了这个问题,忘记了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 监听器保持订阅状态

【讨论】:

  • 此方法不能用于等待其他类的事件。它只能在当前类中使用。否则,您将收到编译时错误:事件“MyClass.OnActionFinished”只能出现在 += 或 -= 的左侧(在“MyClass”类型中使用时除外)
  • 不幸的是。在我的情况下,在await 之前添加一行代码otherClass.OnAction += () =&gt; OnActionFinished?.Invoke(); 很方便。
猜你喜欢
  • 1970-01-01
  • 2021-09-19
  • 2021-11-30
  • 1970-01-01
  • 1970-01-01
  • 2015-08-31
  • 2018-01-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多