public async UniTask<int> FooAsync()
{
await UniTask.Yield();
throw new OperationCanceledException();
}
如果您处理异常但想忽略(传播到全局cancellation处理的地方),请使用异常过滤器。
public async UniTask<int> BarAsync()
{
try
{
var x = await FooAsync();
return x * 2;
}
catch (Exception ex) when (!(ex is OperationCanceledException)) // when (ex is not OperationCanceledException) at C# 9.0
{
return -1;
}
}
throws/catchOperationCanceledException有点重,所以如果性能是一个问题,请使用UniTask.SuppressCancellationThrow以避免 OperationCanceledException 抛出。它将返回(bool IsCanceled, T Result)而不是抛出。
var (isCanceled, _) = await UniTask.DelayFrame(10, cancellationToken: cts.Token).SuppressCancellationThrow();
if (isCanceled)
{
// ...
}
F:Cysharp.Threading.Tasks.PlayerLoopTiming.Initialization; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastInitialization; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.EarlyUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastEarlyUpdate; Isn't injected this PlayerLoop in this project.d
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastFixedUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PreUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastPreUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PreLateUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastPreLateUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PostLateUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.TimeUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastTimeUpdate; Isn't injected this PlayerLoop in this project.
public class SyncContextInjecter
{
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
public static void Inject()
{
SynchronizationContext.SetSynchronizationContext(new UniTaskSynchronizationContext());
}
}
UniTask
为Unity提供一个高性能,0GC的async/await异步方案。
UniTask<T>和自定义的 AsyncMethodBuilder 来实现0GCUniTask.Yield,UniTask.Delay,UniTask.DelayFrame, etc..) 可以替换所有协程操作有关技术细节,请参阅博客文章:UniTask v2 — Unity 的0GC async/await 以及 异步LINQ 的使用 有关高级技巧,请参阅博客文章:通过异步装饰器模式扩展 UnityWebRequest — UniTask 的高级技术
Table of Contents
入门
通过UniTask/releases页面中提供的UPM 包或资产包 (
UniTask.*.*.*.unitypackage)安装。UniTask 和 AsyncOperation 基础知识
UniTask 功能依赖于 C# 7.0( task-like custom async method builder feature ) 所以需要的 Unity 最低版本是
Unity 2018.3,官方支持的最低版本是Unity 2018.4.13f1.为什么需要 UniTask(自定义task对象)?因为原生 Task 太重,与 Unity 线程(单线程)相性不好。UniTask 不使用线程和 SynchronizationContext/ExecutionContext,因为 Unity 的异步对象由 Unity 的引擎层自动调度。它实现了更快和更低的分配,并且与Unity完全兼容。
你可以在使用
using Cysharp.Threading.Tasks;时对AsyncOperation,ResourceRequest,AssetBundleRequest,AssetBundleCreateRequest,UnityWebRequestAsyncOperation,AsyncGPUReadbackRequest,IEnumerator以及其他的异步操作进行 awaitUniTask 提供了三种模式的扩展方法。
WithCancellation是ToUniTask的简化版本,两者都返回UniTask。有关cancellation的详细信息,请参阅:取消和异常处理部分。UniTask可以使用UniTask.WhenAll和UniTask.WhenAny等实用函数。它们就像Task.WhenAll/Task.WhenAny。但它们会返回内容,这很有用。它们会返回值元组,因此您可以传递多种类型并解构每个结果。如果你想转换一个回调逻辑块,让它变成UniTask的话,可以使用
UniTaskCompletionSource<T>(TaskCompletionSource<T>的轻量级魔改版)您可以进行如下转换
Task->UniTask: 使用AsUniTaskUniTask->UniTask<AsyncUnit>: 使用AsAsyncUnitUniTaskUniTask<T>->UniTask: 使用AsUniTask,这两者的转换是无消耗的如果你想将异步转换为协程,你可以使用
.ToCoroutine(),如果你只想允许使用协程系统,这很有用。UniTask 不能await两次。这是与.NET Standard 2.1 中引入的ValueTask/IValueTaskSource相同的约束。
如果实在需要多次await一个异步操作,可以使用
UniTask.Lazy来支持多次调用。.Preserve()同样允许多次调用(由UniTask内部缓存的结果)。这种方法在函数范围内有多个调用时很有用。同样的
UniTaskCompletionSource可以在同一个地方被await多次,或者在很多不同的地方被await。Cancellation and Exception handling
一些 UniTask 工厂方法有一个
CancellationToken cancellationToken = default参数。Unity 的一些异步操作也有WithCancellation(CancellationToken)和ToUniTask(..., CancellationToken cancellation = default)拓展方法。可以传递原生
CancellationTokenSource给参数CancellationTokenCancellationToken 可以由
CancellationTokenSource或 MonoBehaviour 的GetCancellationTokenOnDestroy扩展方法创建。对于链式取消,所有异步方法都建议最后一个参数接受
CancellationToken cancellationToken,并将CancellationToken从头传递到尾。CancellationToken表示异步的生命周期。您可以使用自定义的生命周期,而不是默认的 CancellationTokenOnDestroy。当检测到取消时,所有方法都会向上游抛出并传播
OperationCanceledException。当异常(不限于OperationCanceledException)没有在异步方法中处理时,它将最终传播到UniTaskScheduler.UnobservedTaskException。接收到的未处理异常的默认行为是将日志写入异常。可以使用UniTaskScheduler.UnobservedExceptionWriteLogType更改日志级别。如果要使用自定义行为,请为UniTaskScheduler.UnobservedTaskException.设置一个委托而
OperationCanceledException是一个特殊的异常,会被UnobservedTaskException.无视如果要取消异步 UniTask 方法中的行为,请手动抛出
OperationCanceledException。如果您处理异常但想忽略(传播到全局cancellation处理的地方),请使用异常过滤器。
throws/catch
OperationCanceledException有点重,所以如果性能是一个问题,请使用UniTask.SuppressCancellationThrow以避免 OperationCanceledException 抛出。它将返回(bool IsCanceled, T Result)而不是抛出。注意:仅当您在原方法直接调用SuppressCancellationThrow时才会抑制异常抛出。否则,返回值将被转换,且整个管道不会抑制 throws。
超时处理
超时是取消的一种变体。您可以通过
CancellationTokenSouce.CancelAfterSlim(TimeSpan)设置超时并将 CancellationToken 传递给异步方法。为优化减少每个调用异步方法超时的 CancellationTokenSource 分配,您可以使用 UniTask 的
TimeoutController.如果您想将超时与其他取消源一起使用,请使用
new TimeoutController(CancellationToken).注意:UniTask 有
.Timeout,.TimeoutWithoutException方法,但是,如果可能,不要使用这些,请通过CancellationToken. 由于.Timeout作用在task外部,无法停止超时任务。.Timeout表示超时时忽略结果。如果您将一个CancellationToken传递给该方法,它将从任务内部执行,因此可以停止正在运行的任务。进度
一些Unity的异步操作具有
ToUniTask(IProgress<float> progress = null, ...)扩展方法。您不应该使用原生的
new System.Progress<T>,因为它每次都会导致GC分配。改为使用Cysharp.Threading.Tasks.Progress。这个 progress factory 有两个方法,Create和CreateOnlyValueChanged.CreateOnlyValueChanged仅在进度值更新时调用。为调用者实现 IProgress 接口会更好,因为这样可以没有 lambda 分配。
PlayerLoop
UniTask 在自定义PlayerLoop上运行。UniTask 的基于 playerloop 的方法(例如
Delay、DelayFrame、asyncOperation.ToUniTask等)接受这个PlayerLoopTiming。它表示何时运行,您可以检查PlayerLoopList.md到 Unity 的默认 playerloop 并注入 UniTask 的自定义循环。
PlayerLoopTiming.Update与协程中的yield return null类似,但在 Update(Update 和 uGUI 事件(button.onClick, etc…) 前被调用(在ScriptRunBehaviourUpdate时被调用),yield return null 在ScriptRunDelayedDynamicFrameRate时被调用。PlayerLoopTiming.FixedUpdate类似于WaitForFixedUpdate。yield return null和UniTask.Yield相似但不同。yield return null总是返回下一帧但UniTask.Yield返回下一个调用。也就是说,UniTask.Yield(PlayerLoopTiming.Update)在PreUpdate上调用,它返回相同的帧。UniTask.NextFrame()保证返回下一帧,您可以认为它的行为与yield return null一致.AsyncOperation在原生生命周期返回。例如,awaitSceneManager.LoadSceneAsync在EarlyUpdate.UpdatePreloading时返回,在此之后,加载的场景的Start方法调用自EarlyUpdate.ScriptRunDelayedStartupFrame。同样的,await UnityWebRequest在EarlyUpdate.ExecuteMainThreadJobs时返回.在 UniTask 中,await 直接使用原生生命周期,
WithCancellation和ToUniTask可以指定使用的原生生命周期。这通常不会有问题,但是LoadSceneAsync在等待之后,它会导致开始和继续的不同顺序。所以建议不要使用LoadSceneAsync.ToUniTask。在堆栈跟踪中,您可以检查它在 playerloop 中的运行位置。
默认情况下,UniTask 的 PlayerLoop 初始化在
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)].在 BeforeSceneLoad 中调用方法的顺序是不确定的,所以如果你想在其他 BeforeSceneLoad 方法中使用 UniTask,你应该尝试在此之前初始化它。
如果您导入 Unity 的
Entities包,则会将自定义playerloop重置为默认值BeforeSceneLoad并注入 ECS 的循环。当 Unity 在 UniTask 的 initialize 方法之后调用 ECS 的 inject 方法时,UniTask 将不再工作。为了解决这个问题,您可以在 ECS 初始化后重新初始化 UniTask PlayerLoop。
您可以通过调用
PlayerLoopHelper.IsInjectedUniTaskPlayerLoop()来诊断 UniTask 的PlayerLoop是否准备就绪。并且PlayerLoopHelper.DumpCurrentPlayerLoop还会将所有当前PlayerLoop记录到控制台。您可以通过删除未使用的 PlayerLoopTiming 注入来稍微优化循环成本。您可以在初始化时调用
PlayerLoopHelper.Initialize(InjectPlayerLoopTimings)。InjectPlayerLoopTimings有三个预设,All,Standard(除 LastPostLateUpdate 外),Minimum(Update | FixedUpdate | LastPostLateUpdate)。默认为全部,您可以组合自定义注入时间,例如InjectPlayerLoopTimings.Update | InjectPlayerLoopTimings.FixedUpdate | InjectPlayerLoopTimings.PreLateUpdate.使用未注入
PlayerLoopTiming的Microsoft.CodeAnalysis.BannedApiAnalyzers可能会出错。例如,您可以为InjectPlayerLoopTimings.Minimum设置BannedSymbols.txt您可以将
RS0030严重性配置为错误。async void 与 async UniTaskVoid 对比
async void是一个原生的 C# 任务系统,因此它不能在 UniTask 系统上运行。也最好不要使用它。async UniTaskVoid是async UniTask的轻量级版本,因为它没有等待完成并立即向UniTaskScheduler.UnobservedTaskException报告错误. 如果您不需要等待(即发即弃),那么使用UniTaskVoid会更好。不幸的是,要解除警告,您需要在尾部添加Forget().UniTask 也有
Forget方法,类似UniTaskVoid且效果相同。但是如果你完全不需要使用await,UniTaskVoid会更高效。要使用注册到事件的异步 lambda,请不要使用
async void. 相反,您可以使用UniTask.Action或UniTask.UnityAction,两者都通过async UniTaskVoidlambda 创建委托。UniTaskVoid也可以用在 MonoBehaviour 的Start方法中。UniTaskTracker
对于检查(泄露的)UniTasks 很有用。您可以在
Window -> UniTask Tracker中打开跟踪器窗口。UniTaskTracker 仅用于调试用途,因为启用跟踪和捕获堆栈跟踪很有用,但会对性能产生重大影响。推荐的用法是启用跟踪和堆栈跟踪以查找任务泄漏并在完成时禁用它们。
外部拓展
默认情况下,UniTask 支持 TextMeshPro(
BindTo(TMP_Text)和TMP_InputField,并且TMP_InputField有同原生uGUIInputField类似的事件扩展)、DOTween(Tween作为等待)和Addressables(AsyncOperationHandle``AsyncOperationHandle<T>作为等待)。在单独的 asmdef 中定义,如
UniTask.TextMeshPro,UniTask.DOTween,UniTask.Addressables.从包管理器导入包时,会自动启用 TextMeshPro 和 Addressables 支持。但是对于 DOTween 支持,需要
com.demigiant.dotween从OpenUPM导入或定义UNITASK_DOTWEEN_SUPPORT以启用它。DOTween 支持的默认行为(
await,WithCancellation,ToUniTask) await tween 被终止。它适用于 Complete(true/false) 和 Kill(true/false)。但是如果你想重用tweens (SetAutoKill(false)),它就不能按预期工作。如果您想等待另一个时间点,Tween 中存在以下扩展方法,AwaitForComplete,AwaitForPause,AwaitForPlay,AwaitForRewind,AwaitForStepComplete。AsyncEnumerable 和 Async LINQ
Unity 2020.2 支持 C# 8.0,因此您可以使用
await foreach. 这是异步时代的新更新符号。在 C# 7.3 环境中,您可以使用该
ForEachAsync方法以几乎相同的方式工作。UniTaskAsyncEnumerable 实现异步 LINQ,类似于 LINQ 的
IEnumerable<T>或 Rx 的IObservable<T>。所有标准 LINQ 查询运算符都可以应用于异步流。例如,以下代码表示如何将 Where 过滤器应用于每两次单击运行一次的按钮单击异步流。Fire and Forget 风格(例如,事件处理),你也可以使用
Subscribe.Async LINQ 在 时启用
using Cysharp.Threading.Tasks.Linq;,并且UniTaskAsyncEnumerable在UniTask.Linqasmdef 中定义。它更接近 UniRx(Reactive Extensions),但 UniTaskAsyncEnumerable 是pull-base的异步流,而 Rx 是基于push-base异步流。请注意,尽管相似,但特征不同,并且细节的行为也随之不同。
UniTaskAsyncEnumerable是类似的入口点Enumerable。除了标准查询运算符之外,还有其他 Unity 生成器,例如EveryUpdate、Timer、TimerFrame、Interval、IntervalFrame和EveryValueChanged。并且还添加了额外的 UniTask 原始查询运算符,如Append,Prepend,DistinctUntilChanged,ToHashSet,Buffer,CombineLatest,Do,Never,ForEachAsync,Pairwise,Publish,Queue,Return,SkipUntil,TakeUntil,SkipUntilCanceled,TakeUntilCanceled,TakeLast,Subscribe。以 Func 作为参数的方法具有三个额外的重载,
***Await,***AwaitWithCancellation。如果在 func 方法内部使用
async,请使用***Awaitor***AwaitWithCancellation。如何创建异步迭代器:C# 8.0 支持异步迭代器(
async yield return),但它只允许IAsyncEnumerable<T>并且当然需要 C# 8.0。UniTask 支持UniTaskAsyncEnumerable.Create创建自定义异步迭代器的方法。可等待事件
所有 uGUI 组件都实现
***AsAsyncEnumerable了异步事件流的转换。所有 MonoBehaviour 消息事件都可以转换异步流
AsyncTriggers,可以通过using Cysharp.Threading.Tasks.Triggers;进行启用,.AsyncTrigger 可以使用 UniTaskAsyncEnumerable 来创建,通过GetAsync***Trigger触发。AsyncReactiveProperty,AsyncReadOnlyReactiveProperty是 UniTask 的 ReactiveProperty 版本。将异步流值绑定到 Unity 组件(Text/Selectable/TMP/Text)BindTo的IUniTaskAsyncEnumerable<T>扩展方法。在序列中的异步处理完成之前,pull-based异步流不会获取下一个值。这可能会从按钮等推送类型的事件中溢出数据。
它很有用(防止双击),但有时没用。
使用该
Queue()方法还将在异步处理期间对事件进行排队。或使用
Subscribe, fire and forget 风格。Channel
Channel与System.Threading.Tasks.Channels相同,类似于 GoLang Channel。目前只支持多生产者、单消费者无界渠道。它可以由
Channel.CreateSingleConsumerUnbounded<T>().对于 producer(
.Writer),用TryWrite推送值和TryComplete完成通道。对于 consumer(.Reader),使用TryRead、WaitToReadAsync、ReadAsync和Completion,ReadAllAsync来读取队列的消息。ReadAllAsync返回IUniTaskAsyncEnumerable<T>查询 LINQ 运算符。Reader 只允许单消费者,但使用.Publish()查询运算符来启用多播消息。例如,制作 pub/sub 实用程序。单元测试
Unity 的
[UnityTest]属性可以测试协程(IEnumerator)但不能测试异步。UniTask.ToCoroutine将 async/await 桥接到协程,以便您可以测试异步方法。UniTask 自己的单元测试是使用 Unity Test Runner 和Cysharp/RuntimeUnitTestToolkit编写的,以与 CI 集成并检查 IL2CPP 是否正常工作。
线程池限制
大多数 UniTask 方法在单个线程 (PlayerLoop) 上运行,只有
UniTask.Run(Task.Run等效)和UniTask.SwitchToThreadPool在线程池上运行。如果您使用线程池,它将无法与 WebGL 等平台兼容。UniTask.Run现在已弃用。你可以改用UniTask.RunOnThreadPool。并且还要考虑是否可以使用UniTask.Create或UniTask.Void。IEnumerator.ToUniTask 限制
您可以将协程(IEnumerator)转换为 UniTask(或直接等待),但它有一些限制。
WaitForEndOfFrame,WaitForFixedUpdate,CoroutineStartCoroutine不一样,它使用指定PlayerLoopTiming的并且默认情况下,PlayerLoopTiming.Update在 MonoBehaviourUpdate和StartCoroutine的循环之前运行。如果您想要从协程到异步的完全兼容转换,请使用
IEnumerator.ToUniTask(MonoBehaviour coroutineRunner)重载。它在参数 MonoBehaviour 的实例上执行 StartCoroutine 并等待它在 UniTask 中完成。关于UnityEditor
UniTask 可以像编辑器协程一样在 Unity 编辑器上运行。但是,有一些限制。
DelayType.Realtime等待正确的时间。EditorApplication.update生命周期上运行。-quit的-batchmode带不起作用,因为 UnityEditorApplication.update在单帧后不会运行并退出。相反,不要使用-quit并手动退出EditorApplication.Exit(0).与原生Task API对比
UniTask 有许多原生的 Task-like API。此表显示了一一对应的 API 是什么。
使用原生类型。
IProgress<T>CancellationTokenCancellationTokenSource使用 UniTask 类型.
Task/ValueTaskUniTaskTask<T>/ValueTask<T>UniTask<T>async voidasync UniTaskVoid+= async () => { }UniTask.Void,UniTask.Action,UniTask.UnityActionUniTaskCompletionSourceTaskCompletionSource<T>UniTaskCompletionSource<T>/AutoResetUniTaskCompletionSource<T>ManualResetValueTaskSourceCore<T>UniTaskCompletionSourceCore<T>IValueTaskSourceIUniTaskSourceIValueTaskSource<T>IUniTaskSource<T>ValueTask.IsCompletedUniTask.Status.IsCompleted()ValueTask<T>.IsCompletedUniTask<T>.Status.IsCompleted()new Progress<T>Progress.Create<T>CancellationToken.Register(UnsafeRegister)CancellationToken.RegisterWithoutCaptureExecutionContextCancellationTokenSource.CancelAfterCancellationTokenSource.CancelAfterSlimChannel.CreateUnbounded<T>(false){ SingleReader = true }Channel.CreateSingleConsumerUnbounded<T>IAsyncEnumerable<T>IUniTaskAsyncEnumerable<T>IAsyncEnumerator<T>IUniTaskAsyncEnumerator<T>IAsyncDisposableIUniTaskAsyncDisposableTask.DelayUniTask.DelayTask.YieldUniTask.YieldTask.RunUniTask.RunOnThreadPoolTask.WhenAllUniTask.WhenAllTask.WhenAnyUniTask.WhenAnyTask.CompletedTaskUniTask.CompletedTaskTask.FromExceptionUniTask.FromExceptionTask.FromResultUniTask.FromResultTask.FromCanceledUniTask.FromCanceledTask.ContinueWithUniTask.ContinueWithTaskScheduler.UnobservedTaskExceptionUniTaskScheduler.UnobservedTaskException池化配置
UniTask 积极缓存异步promise对象以实现零分配(有关技术细节,请参阅博客文章UniTask v2 — Unity 的零分配异步/等待,使用异步 LINQ)。默认情况下,它缓存所有promise ,但您可以配置
TaskPool.SetMaxPoolSize为您的值,该值表示每种类型的缓存大小。TaskPool.GetCacheSizeInfo返回池中当前缓存的对象。Profiler下的分配
在 UnityEditor 中,分析器显示编译器生成的 AsyncStateMachine 的分配,但它只发生在调试(开发)构建中。C# 编译器将 AsyncStateMachine 生成为 Debug 构建的类和 Release 构建的结构。
Unity 从 2020.1 开始支持代码优化选项(右,页脚)。
您可以将 C# 编译器优化更改为 release 以删除开发版本中的 AsyncStateMachine 分配。此优化选项也可以通过设置
Compilation.CompilationPipeline-codeOptimization和Compilation.CodeOptimization。UniTaskSynchronizationContext
Unity 的默认 SynchronizationContext(
UnitySynchronizationContext) 在性能方面表现不佳。UniTask 绕过SynchronizationContext(和ExecutionContext) 因此它不使用它,但如果存在async Task,则仍然使用它。UniTaskSynchronizationContext是UnitySynchronizationContext性能更好的替代品。这是一个可选的选择,并不总是推荐;
UniTaskSynchronizationContext性能不如async UniTask,并且不是完整的 UniTask 替代品。它也不保证与UnitySynchronizationContext完全兼容API References
UniTask 的 API 参考由DocFX和Cysharp/DocfXTemplate托管在cysharp.github.io/UniTask上。
例如,UniTask 的工厂方法可以在UniTask#methods中看到。UniTaskAsyncEnumerable 的工厂/扩展方法可以在UniTaskAsyncEnumerable#methods中看到。
UPM Package
通过 git URL 安装
需要支持 git 包路径查询参数的 unity 版本(Unity >= 2019.3.4f1,Unity >= 2020.1a21)。您可以添加
https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask到包管理器或添加
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask"到Packages/manifest.json.如果要设置目标版本,UniTask 使用
*.*.*发布标签,因此您可以指定一个版本,如#2.1.0. 例如https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#2.1.0.通过 OpenUPM 安装
该软件包在openupm 注册表中可用。建议通过openupm-cli安装。
.NET Core
对于 .NET Core,请使用 NuGet。
.NET Core 版本的 UniTask 是 Unity UniTask 的子集,移除了 PlayerLoop 依赖的方法。
它以比标准 Task/ValueTask 更高的性能运行,但在使用时应注意忽略 ExecutionContext/SynchronizationContext。
AsyncLocal也不起作用,因为它忽略了 ExecutionContext。如果您在内部使用 UniTask,但将 ValueTask 作为外部 API 提供,您可以编写如下(受PooledAwait启发)代码。
.NET Core 版本允许用户在与Unity共享代码时(例如CysharpOnion),像使用接口一样使用UniTask。.NET Core 版本的 UniTask 可以提供丝滑的代码共享体验。
WhenAll 等实用方法作为 UniTask 的补充,由Cysharp/ValueTaskSupplement提供。
License
此仓库基于MIT协议