7

Task新しい async/await モデルを使用すると、イベントが発生したときに完了するを生成するのはかなり簡単です。次のパターンに従うだけです。

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");

それは理想的ではありません。インテリセンスが得られず、その型のイベントが存在しない場合は実行時エラーが発生しますが、大量の FromEvent メソッドよりも便利な場合があります。

したがって、リフレクションを使用してオブジェクトGetEvent(eventName)を取得するのは簡単です。EventInfo次の問題は、そのイベントのデリゲートが実行時に不明である (変更できる必要がある) ことです。これにより、イベント ハンドラーの追加が難しくなります。実行時にメソッドを動的に作成し、特定のシグネチャに一致する (ただし、すべてのパラメーターを無視する) 必要があるTaskCompletionSourceためです。

幸いなことに、[ほぼ]正確にそれを行う方法に関する指示を含むこのリンク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ますか?または、代わりに、任意の型からの任意のイベントの Task を返すメソッドを作成する別の方法はありますか?

4

4 に答える 4

24

どうぞ:

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>

于 2012-10-12T22:21:01.723 に答える
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;
}

上記の 3 つのメソッドをコピー アンド ペーストするのが面倒な場合は、TaskFromEvent 関数をそれぞれ 1 行で記述できるようにするヘルパー関数を次に示します。私がもともと持っていたものを単純化するために、最大にクレジットを与える必要があります。

于 2012-10-12T21:08:06.873 に答える
2

デリゲートの種類ごとに 1 つのメソッドを使用する場合は、次のようにすることができます。

Task FromEvent(Action<Action> add)
{
    var tcs = new TaskCompletionSource<bool>();

    add(() => tcs.SetResult(true));

    return tcs.Task;
}

次のように使用します。

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

この方法では、イベントの購読を解除することは決してないことに注意してください。これは、問題になる場合とそうでない場合があります。

ジェネリック デリゲートを使用している場合は、ジェネリック型ごとに 1 つのメソッドで十分です。具象型ごとに 1 つ必要ありません。

Task<T> FromEvent<T>(Action<Action<T>> add)
{
    var tcs = new TaskCompletionSource<T>();

    add(x => tcs.SetResult(x));

    return tcs.Task;
}

型推論はそれでは機能しませんが、型パラメーターを明示的に指定する必要があります (型OnCompletionAction<string>ここにあると仮定します)。

string s = await FromEvent<string>(x => c.OnCompletion += x);
于 2012-10-12T20:27:23.583 に答える