Vinculação de Ciclo de Vida com Auto-Cancel
Um dos bugs mais comuns no código async Unity é lançar uma task de um MonoBehaviour e depois esquecer de cancelá-la quando o objeto é destruído. A task continua executando, tenta acessar objetos Unity destruídos e lança MissingReferenceException — ou pior, corrompe silenciosamente o estado.
O ValkarnTasks elimina essa classe de bug por meio de um gerador de código-fonte Roslyn que vincula automaticamente métodos async ao tempo de vida de destruição do objeto.
O Problema
Sem nenhuma infraestrutura, todo método async em um MonoBehaviour requer que o desenvolvedor passe um CancellationToken manualmente:
// Abordagem manual — fácil de esquecer, tedioso de manter
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();
}
}
}
À medida que o número de métodos async cresce, o boilerplate também cresce. Omitir OnDestroy — ou descartar na ordem errada — causa os vazamentos descritos acima.
A Abordagem Gerada
Declare sua classe como partial e o ValkarnTasks cuida do resto:
// Depois — declare partial e o gerador faz a conexão
public partial class EnemyAI : MonoBehaviour
{
void OnEnable()
{
RunPatrolLoop().Forget();
}
async ValkarnTask RunPatrolLoop()
{
while (true)
{
await ValkarnTask.Delay(2000, __ValkarnTaskLifetimeToken);
MoveToNextWaypoint();
}
}
}
Sem CancellationTokenSource, sem OnDestroy, sem descarte. O token gerado é cancelado automaticamente quando o Unity destrói o objeto.
Como o Gerador de Código-Fonte Funciona
O gerador (LifecycleBindingGenerator) é um gerador Roslyn incremental que é executado em tempo de compilação. Seu pipeline tem três estágios.
Estágio 1 — Filtro sintático
O gerador examina toda declaração de classe em seu projeto. Uma classe é considerada candidata se:
- É declarada com a palavra-chave
partial. - Tem uma lista de classes base (isto é, herda de algo).
Este filtro é puramente sintático e muito rápido. Nenhuma análise semântica é executada neste estágio.
Estágio 2 — Transformação semântica
Para cada classe candidata, o gerador usa o modelo semântico Roslyn para:
- Confirmar que a classe deriva de
UnityEngine.MonoBehaviour(percorre a cadeia de herança completa). - Enumerar todos os membros. Para cada membro, verifica se ele é:
- Um método
async. - Retorna
UnaPartidaMas.Valkarn.Tasks.ValkarnTask(ouValkarnTask<T>). - Não carrega
[NoAutoCancel].
- Um método
- Se nenhum método qualificado for encontrado, a classe é silenciosamente ignorada — nada é gerado.
- Apenas a primeira declaração parcial é processada. Se uma classe está dividida em múltiplos arquivos, o gerador emite código uma vez, vinculado à primeira declaração, para evitar membros duplicados.
Estágio 3 — Emissão de código
Para cada classe que passa pelos estágios 1 e 2, o gerador escreve um novo arquivo .g.cs. O código gerado para uma classe chamada EnemyAI no namespace Game.Enemies se parece com:
// <auto-generated/>
#nullable disable
using System.Threading;
namespace Game.Enemies
{
partial class EnemyAI
{
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
CancellationTokenSource __valkarnTaskLifetimeCts;
/// <summary>
/// Token de cancelamento que é disparado quando este MonoBehaviour é destruído.
/// Auto-gerado pelo gerador de código-fonte ValkarnTask.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
protected CancellationToken __ValkarnTaskLifetimeToken
{
get
{
if (__valkarnTaskLifetimeCts == null)
__valkarnTaskLifetimeCts =
CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
return __valkarnTaskLifetimeCts.Token;
}
}
}
}
Detalhes importantes:
- O
CancellationTokenSourceé lazy — alocado apenas na primeira vez que__ValkarnTaskLifetimeTokené acessado. - É vinculado ao
destroyCancellationTokenintegrado do Unity (MonoBehaviour.destroyCancellationToken, disponível desde o Unity 2022). Quando o Unity destrói o objeto,destroyCancellationTokendispara, o que se propaga para__valkarnTaskLifetimeCts, que cancela__ValkarnTaskLifetimeToken. - Tanto o campo quanto a propriedade são marcados com
EditorBrowsable(Never)para que não poluam o IntelliSense para usuários da classe. - A propriedade é
protected, para que subclasses também possam usar o mesmo token.
O Atributo [NoAutoCancel]
Aplique [NoAutoCancel] a qualquer método async ValkarnTask quando você intencionalmente quiser que ele continue executando além do tempo de vida do objeto. Cenários comuns:
- Um método que salva dados em disco e deve ser concluído mesmo que o objeto que o disparou seja destruído.
- Um método gerenciando um recurso compartilhado de propriedade de um sistema diferente.
- Efeitos de transição que intencionalmente sobrevivem ao objeto que os iniciou.
public partial class SaveManager : MonoBehaviour
{
// Este método SERÁ cancelado automaticamente ao destruir
async ValkarnTask AutoSaveLoop()
{
while (true)
{
await ValkarnTask.Delay(30_000, __ValkarnTaskLifetimeToken);
await WriteSaveToDisk();
}
}
// Este método NÃO será cancelado automaticamente — deve terminar de escrever
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk(CancellationToken ct = default)
{
await FileSystem.WriteAsync(_saveData, ct);
}
}
[NoAutoCancel] é um atributo de nível de método. O gerador simplesmente exclui esse método da contagem de métodos qualificados. Se todos os métodos em uma classe carregam [NoAutoCancel], o gerador não emite nada para essa classe.
Analisador: TT014 — NoAutoCancel sem parâmetro CancellationToken
Um analisador complementar (NoAutoCancelAnalyzer) relata o diagnóstico TT014 quando você aplica [NoAutoCancel] a um método que não tem parâmetro CancellationToken. Se não há parâmetro de token, o método não tem como observar o cancelamento — o que significa que [NoAutoCancel] está presente mas não tem efeito prático. Isso geralmente significa que você esqueceu de adicionar o token:
// TT014: [NoAutoCancel] aplicado mas o método não tem parâmetro CancellationToken
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk() // <-- parâmetro ct ausente
{
await FileSystem.WriteAsync(_saveData);
}
Corrija adicionando um parâmetro CancellationToken:
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk(CancellationToken ct = default)
{
await FileSystem.WriteAsync(_saveData, ct);
}
O Atributo [FireAndForget]
[FireAndForget] é um atributo separado e complementar que marca um método async como intencionalmente não aguardado. Serve a dois propósitos:
- Suprime avisos VTASKS-TASK002 e VTASKS-TASK013, que disparam quando chamadores não fazem
awaitem um valor de retornoValkarnTask. - Sinaliza intenção — leitores futuros do código sabem que o descarte é deliberado.
O gerador de código-fonte envolve métodos [FireAndForget] para garantir que quaisquer exceções não observadas sejam publicadas por meio do handler de exceção não observada do ValkarnTasks em vez de serem silenciosamente perdidas.
public partial class SpawnManager : MonoBehaviour
{
void OnPlayerDied()
{
// Sem aviso, a intenção é clara
ShowDeathScreenAsync();
}
[FireAndForget]
async ValkarnTask ShowDeathScreenAsync()
{
await ValkarnTask.Delay(500, __ValkarnTaskLifetimeToken);
_deathScreen.SetActive(true);
}
}
[FireAndForget] e [NoAutoCancel] são independentes e podem ser combinados:
[FireAndForget]
[NoAutoCancel]
async ValkarnTask PlayGlobalMusicAsync(CancellationToken ct = default) { ... }
Analisador: TT010 — Auto-Cancel Ativo
O AutoCancelInfoAnalyzer relata um diagnóstico informacional TT010 em todo método async ValkarnTask em um MonoBehaviour que será cancelado automaticamente (isto é, não tem [NoAutoCancel]). Isso não é um erro ou aviso — é transparência intencional para que desenvolvedores possam ver de relance quais métodos estão vinculados ao ciclo de vida.
Você pode suprimir TT010 por método com [NoAutoCancel], ou desabilitá-lo em todo o projeto via .editorconfig se preferir não vê-lo.
Limitações
A classe deve ser declarada partial. O gerador de código-fonte não pode adicionar membros a uma classe não-partial. Se seu MonoBehaviour não é partial, o gerador o ignora silenciosamente e nenhum vínculo é criado. Os atributos [NoAutoCancel] e [FireAndForget] ainda funcionam como documentação e para os analisadores, mas __ValkarnTaskLifetimeToken não estará disponível.
Classes aninhadas. Se um MonoBehaviour é declarado como uma classe aninhada dentro de outra classe, tanto as declarações de classe externa quanto interna devem ser partial. O Roslyn exige que todos os tipos enclosing sejam partial para que membros gerados compilem corretamente.
Classes base. A propriedade gerada __ValkarnTaskLifetimeToken é protected. Subclasses herdam automaticamente acesso a ela. O gerador é executado para cada classe na hierarquia independentemente; se tanto uma classe base quanto uma classe derivada são MonoBehaviours partial com métodos async, cada uma recebe seu próprio partial gerado, mas compartilham o mesmo token subjacente porque destroyCancellationToken é herdado da base MonoBehaviour.
Herança múltipla. C# não suporta herança múltipla de classes. Um MonoBehaviour só pode ter uma base de classe, portanto não há ambiguidade sobre qual destroyCancellationToken vincular.
ScriptableObjects. O gerador atualmente tem como alvo apenas MonoBehaviour. ScriptableObject não tem um equivalente de destroyCancellationToken na API do Unity, portanto, a geração de auto-cancel não está disponível para eles.