如何实现一个可以用 await 异步等待的 Awaiter

本文介绍了如何在.NET和C#中实现一个适用于UI的可等待类型DispatcherAsyncOperation,以解决UI的异步等待问题。通过定义Awaiter接口和实现自定义Awaitable,文章详细展示了如何避免在后台线程等待UI操作,减少资源浪费。文章提供完整代码示例,包括创建后台线程创建UI控件的功能,以及如何在不同场景下使用DispatcherAsyncOperation。
摘要由CSDN通过智能技术生成

.NET 和 C# 共同给我们带来的 async/await 异步编程模型(TAP)用起来真的很爽。为了实现异步等待,我们只需要在一切能够能够异步等待的方法前面加上 await 即可。能够异步等待的最常见的类型莫过于 Task,但也有一些其他类型。即便有些耗时操作没有返回可等待的类型,我们也可以用一句 Task.Run(action) 来包装(同步转异步 - 林德熙 中也有说明);不过副作用就是 Run 里面的方法在后台线程执行了(谁知道这是好处呢还是坏处呢 ^_^)。

问题就在于,有些“耗时”操作根本就无法放入后台线程,典型的莫过于“耗时”的 UI 操作。本文将通过实现一个适用于 UI 的可等待类型来解决这种 UI 的“耗时”等待问题。


本文代码较多,阅读建议:

  1. 标注为“本文推荐的完整代码”的代码块可直接放入自己的项目中使用,也贴出了 GitHub 上我以 MIT 开源的源代码(可能 GitHub 上会经常更新)。
  2. 标注“此处为试验代码”的代码块表明此处代码并不完善,仅用于本文分析使用,不建议放到自己的项目中使用。
  3. 没有注释标注的代码块是用于研究的代码片段,不需要使用。
  4. 可点击下面的导航跳转到你希望的地方。

我们的需求

这里说的 UI “耗时”,“耗时”打了引号,是因为严格来说并不是真的卡死了 UI,而是某个函数的执行需要更多的 UI 操作才能继续。这句话可能比较难懂,但举两个例子就好懂了。

  1. 某个函数的执行需要显示一个用户控件,用户填写控件中的信息并确定后,函数才继续执行。这种感觉很像模态窗口,但我们却是在同一个窗口内实现,不能通过模态窗口来实现我们的功能。(UWP 中的 ContentDialog 就是这么干的。)
  2. 我们需要在后台线程创建一个控件,创建完毕之后在原线程返回。这样我们就能得到一个在后台线程创建的控件了。

本文将以实现第 2 条为目标,一步步完善我们的代码,并做出一个非常通用的 UI 可等待类出来。最终你会发现,我们的代码也能轻松应对第 1 条的需求。

什么样的类是可等待的?

我们已经知道 Task 是可等待的,但是去看看 Task 类的实现,几乎找不到哪个基类、接口或者方法属性能够告诉我们与 await 相关。所以,await 的实现可能是隐式的。

幸运的是,Dixin’s Blog - Understanding C# async / await (2) The Awaitable-Awaiter Pattern 一文解决了我们的疑惑。async/await 是给编译器用的,只要我们的类包含一个 GetAwaiter 方法,并返回合适的对象,我们就能让这个类的实例被 await 使用了。

既然需要一个 GetAwaiter 方法,那我们先随便写个方法探索一下:

Test DoAsync()
{
    return new Test();
}
class Test
{
   
    void GetAwaiter()
    {
    }
}

尝试调用:

await DoAsync();

编译器告诉我们:

Test.GetAwaiter() 不可访问,因为它具有一定的保护级别。

原来 GetAwaiter 方法需要是可以被调用方访问到的才行。

于是我们将 GetAwaiter 前面的访问修饰符改成 public。现在提示变成了:

await 要求类型 Test 包含适当的 GetAwaiter 方法。

考虑到一定要获取到某个对象才可能有用,于是我们返回一个 Test2 对象:

public class Test
{
    public Test2 GetAwaiter()
    {
        return new Test2();
    }
}

public class Test2
{
}

这时编译器又告诉我们:

Test2 未包含 IsCompleted 的定义。

加上 public bool IsCompleted { get; },编译器又说:

Test2 不实现 INotifyCompletion。

于是我们实现之,编译器又告诉我们:

Test2 未包含 GetResult 的定义。

于是我们加上一个空的 GetResult 方法,现在编译器终于不报错了。

现在我们一开始的 DoAsync 和辅助类型变成了这样:

// 注:此处为试验代码。
private Test DoAsync()
{
    return new Test();
}

public class Test
{
    public Test2 GetAwaiter()
    {
        return new Test2();
    }
}

public class Test2 : INotifyCompletion
{
    public bool IsCompleted { get; }
    public void GetResult() { }
    public void OnCompleted(Action continuation) { }
}

总结起来,要想使一个方法可被 await 等待,必须具备以下条件:

  1. 这个方法返回一个类 A 的实例,这个类 A 必须满足后面的条件。
  2. 此类 A 有一个可被访问到的 GetAwaiter 方法(扩展方法也行,这算是黑科技吗?),方法返回类 B 的实例,这个类 B 必须满足后面的条件;
  3. 此类 B 实现 INotifyCompletion 接口,且拥有 bool IsCompleted { get; } 属性、GetResult() 方法、void OnCompleted(Action continuation) 方法。

定义抽象的 Awaiter/Awaitable

这里我们发现一个神奇的现象——明明那些属性和方法都是不可缺少的,却并没有接口来约束它们,而是靠着编译器来约束。

然而作为团队开发者的一员,我们不可能让每一位开发者都去探索一遍编译器究竟希望我们怎么来实现 await,于是我们自己来定义接口。方便我们自己后续再实现自己的可等待类型。

以下接口在 Dixin’s Blog - Understanding C# async / await (2) The Awaitable-Awaiter Pattern 一文中已有原型;但我增加了更通用却更严格的泛型约束,使得这些接口更加通用,且使用者实现的过程中更加

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值