自动取消生命周期绑定
Unity 异步代码中最常见的 bug 之一,是从 MonoBehaviour 启动一个任务,然后在对象被销毁时忘记取消它。任务继续运行,尝试访问已销毁的 Unity 对象,并抛出 MissingReferenceException——或者更糟,静默地破坏状态。
ValkarnTasks 通过 Roslyn 源码生成器消除了这类 bug,该生成器会自动将异步方法与对象的销毁生命周期绑定。
问题所在
在没有任何基础设施的情况下,MonoBehaviour 中的每个异步方法都需要开发者手动传递 CancellationToken:
// 手动方式——容易遗忘,维护繁琐
public class EnemyAI : MonoBehaviour
{
CancellationTokenSource _cts;
void OnEnable()
{
_cts = new CancellationTokenSource();
RunPatrolLoop(_cts.Token).Forget();
}
void OnDestroy()
{
_cts?.Cancel();
_cts?.Dispose();
}
async ValkarnTask RunPatrolLoop(CancellationToken ct)
{
while (true)
{
await ValkarnTask.Delay(2000, ct);
MoveToNextWaypoint();
}
}
}
随着异步方法数量增加,样板代码也随之增多。跳过 OnDestroy——或以错误的顺序释放资源——会导致上述的内存泄漏。
生成方式
将类声明为 partial,ValkarnTasks 负责其余的工作:
// 之后——声明 partial,生成器完成连接
public partial class EnemyAI : MonoBehaviour
{
void OnEnable()
{
RunPatrolLoop().Forget();
}
async ValkarnTask RunPatrolLoop()
{
while (true)
{
await ValkarnTask.Delay(2000, __ValkarnTaskLifetimeToken);
MoveToNextWaypoint();
}
}
}
无需 CancellationTokenSource,无需 OnDestroy,无需释放资源。生成的 token 会在 Unity 销毁对象时自动取消。
源码生成器的工作原理
生成器(LifecycleBindingGenerator)是一个在编译时运行的增量式 Roslyn 生成器。其流程包含三个阶段。
阶段 1——语法过滤器
生成器检查你项目中的每个类声明。满足以下条件的类被视为候选:
- 使用
partial关键字声明。 - 有基类列表(即继承自某个类)。
此过滤器纯粹基于语法,速度很快。此阶段不运行语义分析。
阶段 2——语义变换
对于每个候选类,生成器使用 Roslyn 语义模型来:
- 确认该类派生自
UnityEngine.MonoBehaviour(遍历完整的继承链)。 - 枚举所有成员。对于每个成员,检查它是否:
- 是
async方法。 - 返回
UnaPartidaMas.Valkarn.Tasks.ValkarnTask(或ValkarnTask<T>)。 - 不带有
[NoAutoCancel]。
- 是
- 如果未找到符合条件的方法,则静默跳过该类——不生成任何代码。
- 仅处理第一个 partial 声明。如果一个类分散在多个文件中,生成器只为第一个声明生成代码,以避免重复成员。
阶段 3——代码生成
对于通过阶段 1 和 2 的每个类,生成器写入一个新的 .g.cs 文件。对于命名空间为 Game.Enemies 中名为 EnemyAI 的类,生成的代码如下:
// <auto-generated/>
#nullable disable
using System.Threading;
namespace Game.Enemies
{
partial class EnemyAI
{
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
CancellationTokenSource __valkarnTaskLifetimeCts;
/// <summary>
/// 当此 MonoBehaviour 被销毁时触发的取消 token。
/// 由 ValkarnTask 源码生成器自动生成。
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
protected CancellationToken __ValkarnTaskLifetimeToken
{
get
{
if (__valkarnTaskLifetimeCts == null)
__valkarnTaskLifetimeCts =
CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
return __valkarnTaskLifetimeCts.Token;
}
}
}
}
关键细节:
CancellationTokenSource是惰性的——仅在首次访问__ValkarnTaskLifetimeToken时分配。- 它链接到 Unity 内置的
destroyCancellationToken(MonoBehaviour.destroyCancellationToken,自 Unity 2022 起可用)。当 Unity 销毁对象时,destroyCancellationToken触发,级联到__valkarnTaskLifetimeCts,进而取消__ValkarnTaskLifetimeToken。 - 字段和属性都被标记为
EditorBrowsable(Never),使它们不出现在类的使用者的 IntelliSense 中。 - 属性为
protected,因此子类也可以使用同一个 token。
[NoAutoCancel] 特性
当你有意希望某个异步 ValkarnTask 方法在对象生命周期结束后继续运行时,对其应用 [NoAutoCancel]。常见场景:
- 将数据保存到磁盘且必须完成的方法,即使触发它的对象已被销毁。
- 管理属于不同系统的共享资源的方法。
- 有意在启动它的对象生命周期之外继续的过渡效果。
public partial class SaveManager : MonoBehaviour
{
// 此方法在销毁时会被自动取消
async ValkarnTask AutoSaveLoop()
{
while (true)
{
await ValkarnTask.Delay(30_000, __ValkarnTaskLifetimeToken);
await WriteSaveToDisk();
}
}
// 此方法不会被自动取消——它必须完成写入
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk(CancellationToken ct = default)
{
await FileSystem.WriteAsync(_saveData, ct);
}
}
[NoAutoCancel] 是方法级特性。生成器只是将该方法从其符合条件的方法计数中排除。如果一个类中的所有方法都带有 [NoAutoCancel],生成器不会为该类生成任何代码。
分析器:TT014——没有 CancellationToken 参数的 [NoAutoCancel]
配套分析器(NoAutoCancelAnalyzer)在你将 [NoAutoCancel] 应用于没有 CancellationToken 参数的方法时报告诊断 TT014。如果没有 token 参数,该方法没有任何方式来观察取消——这意味着 [NoAutoCancel] 存在但没有实际效果。这通常意味着你忘记了添加 token:
// TT014:[NoAutoCancel] 已应用,但方法没有 CancellationToken 参数
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk() // <-- 缺少 ct 参数
{
await FileSystem.WriteAsync(_saveData);
}
修复方法是添加 CancellationToken 参数:
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk(CancellationToken ct = default)
{
await FileSystem.WriteAsync(_saveData, ct);
}
[FireAndForget] 特性
[FireAndForget] 是一个独立的、互补的特性,用于将异步方法标记为有意不被等待。它有两个用途:
- 抑制警告 VTASKS-TASK002 和 VTASKS-TASK013,这两个警告在调用者不
awaitValkarnTask返回值时触发。 - 表明意图——代码的未来读者知道这种丢弃是有意为之的。
源码生成器封装 [FireAndForget] 方法,确保任何未观察到的异常通过 ValkarnTasks 的未观察异常处理器发布,而不是被静默丢失。
public partial class SpawnManager : MonoBehaviour
{
void OnPlayerDied()
{
// 无警告,意图明确
ShowDeathScreenAsync();
}
[FireAndForget]
async ValkarnTask ShowDeathScreenAsync()
{
await ValkarnTask.Delay(500, __ValkarnTaskLifetimeToken);
_deathScreen.SetActive(true);
}
}
[FireAndForget] 和 [NoAutoCancel] 是独立的,可以组合使用:
[FireAndForget]
[NoAutoCancel]
async ValkarnTask PlayGlobalMusicAsync(CancellationToken ct = default) { ... }
分析器:TT010——自动取消已激活
AutoCancelInfoAnalyzer 对 MonoBehaviour 中每个将被自动取消的异步 ValkarnTask 方法(即没有 [NoAutoCancel] 的方法)报告信息性诊断 TT010。这不是错误或警告——它是有意提供的透明度,让开发者能一眼看出哪些方法绑定了生命周期。
你可以用 [NoAutoCancel] 在每个方法上抑制 TT010,或者如果你不想看到它,可以通过 .editorconfig 在项目范围内禁用它。
限制
类必须声明为 partial。 源码生成器无法向非 partial 类添加成员。如果你的 MonoBehaviour 不是 partial 的,生成器会静默跳过它,不创建任何绑定。[NoAutoCancel] 和 [FireAndForget] 特性仍然作为文档和分析器标注有效,但 __ValkarnTaskLifetimeToken 将不可用。
嵌套类。 如果一个 MonoBehaviour 被声明为另一个类中的嵌套类,外部类和内部类的声明都必须是 partial 的。Roslyn 要求所有封闭类型都是 partial 的,生成的成员才能正确编译。
基类。 生成的 __ValkarnTaskLifetimeToken 属性是 protected 的。子类自动继承对它的访问权限。生成器对继承层次中的每个类独立运行;如果基类和派生类都是带有异步方法的 partial MonoBehaviour,每个都会获得自己的生成 partial,但它们共享同一个底层 token,因为 destroyCancellationToken 是从 MonoBehaviour 基类继承的。
多重继承。 C# 不支持类的多重继承。MonoBehaviour 只能有一个类基,因此不存在关于链接哪个 destroyCancellationToken 的歧义。
ScriptableObjects。 生成器当前仅针对 MonoBehaviour。ScriptableObject 在 Unity API 中没有等效的 destroyCancellationToken,因此自动取消生成不适用于它们。