C# 实现你自己的异步方法

  • C# 实现你自己的异步方法已关闭评论
  • 126 次浏览
  • A+
所属分类:.NET技术
摘要

背景最近在重构自己曾经的代码, 具体需求是在Unity等待如一个模型动画, 一段ui动画 如下:

背景

最近在重构自己曾经的代码, 具体需求是在Unity等待如一个模型动画, 一段ui动画 如下:

C# 实现你自己的异步方法

Await的目标

await的目标是一个可等待对象, 而拥有GetAwaiter方法并且该方法拥有合适返回值的目标即可称为可等待对象(暂时你还不需要知道返回值需要符合什么规则,待会儿Studio会告诉你)

首先,我们通过一些简单的代码来模拟Unity的Slider, 并且实现一个方法来模拟ui动画, 然后在实例化它来看看效果

public class Slider {     public void ValueTo(int value)     {         Task.Run(async () =>          {             Console.Write("HP: ");             for (int i = 0; i < value; i++)             {                 await Task.Delay(350);                 Console.Write("");             }             Console.Write("n");         });     } }

C# 实现你自己的异步方法

效果不错, 现在你希望可以await slider.ValueTo 方法, 而不是再通过使用回调函数传来传去来监听如动画是否结束

首先我们知道await的目标是一个拥有GetAwaiter方法的对象

简单点, 我们令Slider本身是可等待的, 也即我们需要在Slider上实现一个GetAwaiter方法, 已知且其返回值有一定的限制, 我们假设其返回值为SliderAwaiter

public class SliderAwaiter {  } public class Slider {     public SliderAwaiter GetAwaiter()     {         SliderAwaiter awaiter = new SliderAwaiter();         return awaiter;     }     public Slider ValueToAsync(int value)     {         /*          * Task是可等待的, 这里直接返回Task即可在这里达成我们的目标          * 但本文的主要讨论的是实现 自己 的异步方法          * 并且UI动画你可以以此法逃课, 但播放动画就不好逃了          */         Task.Run(async () =>          {             Console.Write("HP: ");             for (int i = 0; i < value; i++)             {                 await Task.Delay(350);                 Console.Write("");             }             Console.Write("n");         });         return this;     } }

当我们尝试在Main中直接await ValueToAsync方法时, 编译器报错(“Program.SliderAwaiter”未包含“IsCompleted”的定义), 我们进行修补。又报错(“Program.SliderAwaiter”不实现“INotifyCompletion”), 继续修, 最后一个报错(“Program.SliderAwaiter”未包含“GetResult”的定义), 补上

经过修补, 我们得到这样的SliderAwaiter

public class SliderAwaiter : INotifyCompletion {     public bool IsCompleted => false;      public void OnCompleted(Action continuation)     {      }     public void GetResult() { } }

直接await ValueToAsync测试,发现Slider表现正常, 但await并没有触发

这里暂时先直接告诉你答案, 你需要手动触发OnCompleted传入的continuation来触发await后的代码

简单改造一下SliderAwaiter和Slider得到下面的代码

public class SliderAwaiter : INotifyCompletion {     public bool IsCompleted => false;     public Action onFinish;     public void OnCompleted(Action continuation)     {         if (IsCompleted)         {             continuation?.Invoke();             onFinish?.Invoke();         }         else         {             onFinish += continuation;         }     }     /// <summary>     /// 手动触发 continuation     /// </summary>     public void ReportResult()     {         onFinish?.Invoke();     }     public void GetResult() { } } public class Slider {     SliderAwaiter awaiter;     public SliderAwaiter GetAwaiter()     {         SliderAwaiter awaiter = new SliderAwaiter();         this.awaiter = awaiter;         return awaiter;     }     public Slider ValueToAsync(int value)     {         Task.Run(async () =>          {             Console.Write("HP: ");             for (int i = 0; i < value; i++)             {                 await Task.Delay(350);                 Console.Write("");             }             Console.Write("n");             awaiter?.ReportResult();         });         return this;     } }

在Main函数中测试一下, 神奇的事情发生了, await后的代码成功且正确地执行了

你可能觉得有些奇妙, 你只是手动触发了OnCompleted传入的后续操作(类似Task.ContinueWith)就成功触发了结束等待, 你可能也会尝试在ReportResult中多次执行continuation, 但await的后续代码依旧只执行一次

OnCompleted中的continuation是什么, 它做了什么

了解他之前, 你可能需要先了解await是如何运行的, 首先你需要知道await的实现是基于状态机

简单点, 我们把测试代码改的简单点

static async Task Main(string[] args) {     Console.WriteLine("before await");     await Task.Delay(500);     Console.WriteLine("after  await");     Console.Read(); }

然后通过dnSpy看看生成的中间语言是什么样的, 从下图和标注你应该可以清晰地看出程序的整体走向

C# 实现你自己的异步方法

通过简单测试代码的中间语言我们了解了await的大致走向, 那么回到最初的问题,continuation究竟做了什么

还原我们刚刚模拟Unity Slider的代码, 继续借助dnSpy, 二话不说找到ReportResult方法并且打上断点, 看看他做了什么

C# 实现你自己的异步方法

是的!一进来就发现一个惊喜, 第一步就跳转到一个叫做MoveNext的方法, 最终经过一番闪转腾挪我们又回到了的状态机里!

我们可以通过手动执行continuation来手动触发我们自己的awaiter!

至此, 你应该了解了如何实现自己的异步方法, 并且它不用依赖Task, TaskCompletionSource

 

最后附上完整的测试代码, 希望本片文章对你的异步之旅有所帮助

internal class Program {     public class SliderAwaiter : INotifyCompletion     {         public bool IsCompleted => false;         public Action onFinish;         public void OnCompleted(Action continuation)         {             if (IsCompleted)             {                 continuation?.Invoke();                 onFinish?.Invoke();             }             else             {                 onFinish += continuation;             }         }         /// <summary>         /// 手动触发 continuation         /// </summary>         public void ReportResult()         {             onFinish?.Invoke();         }         public void GetResult() { }     }     public class Slider     {         SliderAwaiter awaiter;         public SliderAwaiter GetAwaiter()         {             SliderAwaiter awaiter = new SliderAwaiter();             this.awaiter = awaiter;             return awaiter;         }         public Slider ValueToAsync(int value)         {             Task.Run(async () =>              {                 Console.Write("HP: ");                 for (int i = 0; i < value; i++)                 {                     await Task.Delay(350);                     Console.Write("");                 }                 Console.Write("n");                 awaiter?.ReportResult();             });             return this;         }     }      static async Task Main(string[] args)     {         Slider slider = new Slider();         Console.WriteLine("[ START ] PLAY UI_ANIMATION");         await slider.ValueToAsync(4);         Console.WriteLine("[  END  ] PLAY UI_ANIMATION");     }