一尘不染

如何等待C#中的事件?

c#

我正在创建一个包含一系列事件的类,其中一个是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);
}

阅读 734

收藏
2020-05-19

共1个答案

一尘不染

就我个人而言,我认为拥有async事件处理程序可能不是最佳的设计选择,其中至少一个原因就是您所遇到的问题。使用同步处理程序,知道它们何时完成很简单。

就是说,如果由于某种原因您必须或至少被迫坚持这种设计,则可以以await友好的方式进行。

您注册处理程序和await他们的想法是一个好主意。但是,我建议您坚持使用现有的事件范例,因为这将使事件在代码中保持可表达性。最主要的是,您必须偏离EventHandler基于标准的委托类型,并使用返回a的委托类型,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()在执行标准的“获取事件委托实例的本地副本”之后,该方法首先调用所有处理程序,然后等待所有返回的内容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-returning事件处理程序和新的async/
await功能的当前组合确实有点尴尬。上面的方法可以正常工作(恕我直言,效果很好),但是对场景有更好的CLR和/或语言支持(即能够等待多播委托并将C#编译器将其转换为WhenAll())会很好。。

2020-05-19