Is there a way to simplify this reflective delegat

2019-05-14 15:06发布

问题:

In this response, I came up with the following helper method that could be reused by different Task creators to convert events into task completion sources.

// Helper method
static Task<T> TaskFromEventHelper<T>(object target, string eventName, Func<TaskCompletionSource<T>, object> resultSetterFactory) {
    var tcs = new TaskCompletionSource<T>();
    var addMethod = target.GetType().GetEvent(eventName).GetAddMethod();
    var delegateType = addMethod.GetParameters()[0].ParameterType;
    var d = Delegate.CreateDelegate(delegateType, resultSetterFactory(tcs), "Invoke");
    addMethod.Invoke(target, new object[] {d});
    return tcs.Task;
}

// Empty events (Action style)
static Task TaskFromEvent(object target, string eventName) {
    return TaskFromEventHelper(target, eventName, (Func<TaskCompletionSource<object>, object>)(tcs => (Action)(() => tcs.SetResult(null))));
}

// One-value events (Action<T> style)
static Task<T> TaskFromEvent<T>(object target, string eventName) {
    return TaskFromEventHelper(target, eventName, (Func<TaskCompletionSource<T>, object>)(tcs => (Action<T>)(tcs.SetResult)));
}

// Two-value events (Action<T1, T2> or EventHandler style)
static Task<Tuple<T1, T2>> TaskFromEvent<T1, T2>(object target, string eventName) {
    return TaskFromEventHelper(target, eventName, (Func<TaskCompletionSource<Tuple<T1, T2>>, object>)(tcs => (Action<T1, T2>)((t1, t2) => tcs.SetResult(Tuple.Create(t1, t2)))));
}

In each of the three examples I gave that use the helper method, there's a tcs.SetResult component, which makes me think there's a way to move that to the helper method too, which might perhaps simplify the signatures, so that perhaps the helper method would just have to accept a Func<?, T>, where that Func would take the output of the event and convert it to whatever tcs.SetResult takes.

i.e., I'm thinking there must be a way to create a helper so I can write it as

// Empty events (Action style)
static Task TaskFromEvent(object target, string eventName) {
    return TaskFromEventHelper<object>(target, eventName, new Func<object>(() => null));
}

// One-value events (Action<T> style)
static Task<T> TaskFromEvent<T>(object target, string eventName) {
    return TaskFromEventHelper<T>(target, eventName, new Func<T, T>(t => t));
}

// Two-value events (Action<T1, T2> or EventHandler style)
static Task<Tuple<T1, T2>> TaskFromEvent<T1, T2>(object target, string eventName) {
    return TaskFromEventHelper<Tuple<T1, T2>>(target, eventName, new Func<T1, T2, Tuple<T1, T2>>(Tuple.Create));
}

, but that's why I don't know the ? in Func<?, T> above. This one for example needs ? to be two parameters. Could it be passed in as object somehow? I have a feeling it could be possible, but if so it needs some real reflection magic.

回答1:

You can use Expression:

static Task<T> TaskFromEventHelper<T>(object target, string eventName, Delegate resultSetter)
{
    var tcs             = new TaskCompletionSource<T>();
    var addMethod       = target.GetType().GetEvent(eventName).GetAddMethod();
    var delegateType    = addMethod.GetParameters()[0].ParameterType;
    var methodInfo      = delegateType.GetMethod("Invoke");
    var parameters      = methodInfo.GetParameters()
                                    .Select(a => Expression.Parameter(a.ParameterType))
                                    .ToArray();

    // building method, which argument count and
    // their types are not known at compile time
    var exp = // (%arguments%) => tcs.SetResult(resultSetter.Invoke(%arguments%))
        Expression.Lambda(
            delegateType,
            Expression.Call(
                Expression.Constant(tcs),
                tcs.GetType().GetMethod("SetResult"),
                Expression.Call(
                    Expression.Constant(resultSetter),
                    resultSetter.GetType().GetMethod("Invoke"),
                    parameters)),
            parameters);

    addMethod.Invoke(target, new object[] { exp.Compile() });
    return tcs.Task;
}