1. ホーム
  2. c#

[解決済み] C#でイベントを待ち受けるには?

2023-02-11 20:33:23

質問

一連のイベントを持つクラスを作成していますが、そのうちの1つは GameShuttingDown . このイベントが発生したとき、イベントハンドラを呼び出す必要があります。このイベントのポイントは、ゲームがシャットダウンされ、データを保存する必要があることをユーザーに通知することです。保存は待機させることができますが、イベントはそうではありません。したがって、ハンドラーが呼び出されると、待機中のハンドラーが完了する前にゲームがシャットダウンされます。

public event EventHandler<EventArgs> GameShuttingDown;

public virtual async Task ShutdownGame()
{
    await this.NotifyGameShuttingDown();

    await this.SaveWorlds();

    this.NotifyGameShutDown();
}

private async Task SaveWorlds()
{
    foreach (DefaultWorld world in this.Worlds)
    {
        await this.worldService.SaveWorld(world);
    }
}

protected virtual void NotifyGameShuttingDown()
{
    var handler = this.GameShuttingDown;
    if (handler == null)
    {
        return;
    }

    handler(this, new EventArgs());
}

イベント登録

// The game gets shut down before this completes because of the nature of how events work
DefaultGame.GameShuttingDown += async (sender, args) => await this.repo.Save(blah);

イベントに対するシグネチャは void EventName であるため、非同期化することは基本的に火をつけて忘れることです。私のエンジンは、サードパーティの開発者 (および複数の内部コンポーネント) にエンジン内でイベントが起こっていることを通知し、それらに反応させるために、イベント処理を多用しています。

私が使用できる非同期ベースの何かでイベントを置き換えるために進むべき良いルートはありますか? 私が使用できる非同期ベースの何かにイベントを置き換えるために進むべき良いルートはありますか? BeginShutdownGameEndShutdownGame というのは、コールバックを渡すことができるのは呼び出し元だけで、エンジンにプラグインするサードパーティのものは渡せないからです(これはイベントで得られるものです)。もしサーバーが game.ShutdownGame() を呼び出した場合、エンジン プラグインやエンジン内の他のコンポーネントがコールバックを渡す方法はありません。

これで行くために望ましい/推奨されるルートに関する任意のアドバイスは、非常に高く評価されます! 私は周りを見回し、私が見た大部分は Begin/End アプローチを使用しており、私がやりたいことを満たすとは思えません。

編集

私が考えている別のオプションは、待機状態のコールバックを受け取る登録メソッドを使用することです。私はすべてのコールバックを繰り返し、それらのTaskを取得します。 WhenAll .

private List<Func<Task>> ShutdownCallbacks = new List<Func<Task>>();

public void RegisterShutdownCallback(Func<Task> callback)
{
    this.ShutdownCallbacks.Add(callback);
}

public async Task Shutdown()
{
    var callbackTasks = new List<Task>();
    foreach(var callback in this.ShutdownCallbacks)
    {
        callbackTasks.Add(callback());
    }

    await Task.WhenAll(callbackTasks);
}

どのように解決するのですか?

個人的には async イベント ハンドラを持つことは、最良のデザイン選択ではないかもしれません。同期ハンドラでは、いつ完了するかを知るのは簡単です。

とはいえ、何らかの理由でこのデザインにこだわらなければならない、あるいは少なくとも強くこだわらざるを得ないのであれば、それを await -のようなフレンドリーな方法で行うことができます。

ハンドラを登録するあなたのアイデアと await を登録するのは良いアイデアです。しかし、私は既存のイベントパラダイムにこだわることをお勧めします。そうすれば、コードの中でイベントの表現力を保つことができるからです。主なものは、標準的な EventHandler -をベースとしたデリゲートタイプを使用し、そのデリゲートタイプが返す Task が使えるように await というハンドラを作成します。

以下は、私が言いたいことを説明する簡単な例です。

class A
{
    public event Func<object, EventArgs, Task> Shutdown;

    public async Task OnShutdown()
    {
        Func<object, EventArgs, Task> handler = Shutdown;

        if (handler == null)
        {
            return;
        }

        Delegate[] invocationList = handler.GetInvocationList();
        Task[] handlerTasks = new Task[invocationList.Length];

        for (int i = 0; i < invocationList.Length; i++)
        {
            handlerTasks[i] = ((Func<object, EventArgs, Task>)invocationList[i])(this, EventArgs.Empty);
        }

        await Task.WhenAll(handlerTasks);
    }
}

OnShutdown() メソッドは、標準的な "get local copy of the event delegate instance" を行った後、最初にすべてのハンドラを起動し、次に返されたすべての Tasks (ハンドラが呼び出されたときにローカル配列に保存されます)。

以下は、使い方を説明する短いコンソールプログラムです。

class Program
{
    static void Main(string[] args)
    {
        A a = new A();

        a.Shutdown += Handler1;
        a.Shutdown += Handler2;
        a.Shutdown += Handler3;

        a.OnShutdown().Wait();
    }

    static async Task Handler1(object sender, EventArgs e)
    {
        Console.WriteLine("Starting shutdown handler #1");
        await Task.Delay(1000);
        Console.WriteLine("Done with shutdown handler #1");
    }

    static async Task Handler2(object sender, EventArgs e)
    {
        Console.WriteLine("Starting shutdown handler #2");
        await Task.Delay(5000);
        Console.WriteLine("Done with shutdown handler #2");
    }

    static async Task Handler3(object sender, EventArgs e)
    {
        Console.WriteLine("Starting shutdown handler #3");
        await Task.Delay(2000);
        Console.WriteLine("Done with shutdown handler #3");
    }
}

この例を見て、私は今、C#がこれを少し抽象化する方法はなかったのだろうかと思うようになりました。もしかしたら、それはあまりにも複雑な変更かもしれませんが、現在の旧式の void -を返すイベントハンドラと、新しい async / await の機能は少し厄介に見えます。上記は動作しますが(そしてうまく動作しています、IMHO)、このシナリオに対してより良いCLRや言語サポートがあればよかったと思います(つまり、マルチキャストデリゲートを待ち、C#コンパイラがその呼び出しを WhenAll() ).