Aller au contenu principal

Liaison auto-annulation au cycle de vie

L'un des bugs les plus courants dans le code async Unity est de lancer une tâche depuis un MonoBehaviour puis d'oublier de l'annuler quand l'objet est détruit. La tâche continue de s'exécuter, essaie d'accéder à des objets Unity détruits, et lève des MissingReferenceException — ou pire, corrompt silencieusement l'état.

ValkarnTasks élimine cette classe de bugs grâce à un générateur de source Roslyn qui câble automatiquement les méthodes async à la durée de vie de destruction de l'objet.

Le problème

Sans infrastructure, chaque méthode async dans un MonoBehaviour nécessite que le développeur propage manuellement un CancellationToken :

// Approche manuelle — facile à oublier, fastidieuse à maintenir
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();
}
}
}

Au fur et à mesure que le nombre de méthodes async augmente, le code passe-partout augmente aussi. Oublier OnDestroy — ou disposer dans le mauvais ordre — cause les fuites décrites ci-dessus.

L'approche générée

Déclarez votre classe partial et ValkarnTasks s'occupe du reste :

// Après — déclarer partial et le générateur fait le câblage
public partial class EnemyAI : MonoBehaviour
{
void OnEnable()
{
RunPatrolLoop().Forget();
}

async ValkarnTask RunPatrolLoop()
{
while (true)
{
await ValkarnTask.Delay(2000, __ValkarnTaskLifetimeToken);
MoveToNextWaypoint();
}
}
}

Pas de CancellationTokenSource, pas de OnDestroy, pas de disposition. Le token généré est annulé automatiquement quand Unity détruit l'objet.

Comment fonctionne le générateur de source

Le générateur (LifecycleBindingGenerator) est un générateur Roslyn incrémental qui s'exécute à la compilation. Son pipeline a trois étapes.

Étape 1 — Filtre syntaxique

Le générateur examine chaque déclaration de classe dans votre projet. Une classe est considérée comme candidate si :

  • Elle est déclarée avec le mot-clé partial.
  • Elle a une liste de classes de base (c'est-à-dire qu'elle hérite de quelque chose).

Ce filtre est purement syntaxique et très rapide. Aucune analyse sémantique ne s'exécute à cette étape.

Étape 2 — Transformation sémantique

Pour chaque classe candidate, le générateur utilise le modèle sémantique Roslyn pour :

  1. Confirmer que la classe dérive de UnityEngine.MonoBehaviour (parcourt la chaîne d'héritage complète).
  2. Énumérer tous les membres. Pour chaque membre, il vérifie si c'est :
    • Une méthode async.
    • Retourne UnaPartidaMas.Valkarn.Tasks.ValkarnTask (ou ValkarnTask<T>).
    • Ne porte pas [NoAutoCancel].
  3. Si aucune méthode qualifiante n'est trouvée, la classe est silencieusement ignorée — rien n'est généré.
  4. Seule la première déclaration partielle est traitée. Si une classe est répartie sur plusieurs fichiers, le générateur émet du code une fois, lié à la première déclaration, pour éviter les membres dupliqués.

Étape 3 — Émission de code

Pour chaque classe qui passe les étapes 1 et 2, le générateur écrit un nouveau fichier .g.cs. Le code généré pour une classe nommée EnemyAI dans l'espace de noms Game.Enemies ressemble à ceci :

// <auto-generated/>
#nullable disable
using System.Threading;

namespace Game.Enemies
{
partial class EnemyAI
{
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
CancellationTokenSource __valkarnTaskLifetimeCts;

/// <summary>
/// Token d'annulation déclenché quand ce MonoBehaviour est détruit.
/// Généré automatiquement par le générateur de source ValkarnTask.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
protected CancellationToken __ValkarnTaskLifetimeToken
{
get
{
if (__valkarnTaskLifetimeCts == null)
__valkarnTaskLifetimeCts =
CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
return __valkarnTaskLifetimeCts.Token;
}
}
}
}

Détails clés :

  • Le CancellationTokenSource est paresseux — alloué seulement la première fois que __ValkarnTaskLifetimeToken est accédé.
  • Il est lié au destroyCancellationToken intégré d'Unity (MonoBehaviour.destroyCancellationToken, disponible depuis Unity 2022). Quand Unity détruit l'objet, destroyCancellationToken se déclenche, ce qui se propage à __valkarnTaskLifetimeCts, qui annule __ValkarnTaskLifetimeToken.
  • Le champ et la propriété sont tous les deux marqués EditorBrowsable(Never) pour ne pas polluer IntelliSense pour les utilisateurs de la classe.
  • La propriété est protected, donc les sous-classes peuvent également utiliser le même token.

L'attribut [NoAutoCancel]

Appliquez [NoAutoCancel] à toute méthode async ValkarnTask quand vous voulez intentionnellement qu'elle continue à s'exécuter au-delà de la durée de vie de l'objet. Scénarios courants :

  • Une méthode qui sauvegarde des données sur disque et doit se terminer même si l'objet déclencheur est détruit.
  • Une méthode gérant une ressource partagée appartenant à un système différent.
  • Des effets de transition qui survivent intentionnellement à l'objet qui les a démarrés.
public partial class SaveManager : MonoBehaviour
{
// Cette méthode SERA auto-annulée à la destruction
async ValkarnTask AutoSaveLoop()
{
while (true)
{
await ValkarnTask.Delay(30_000, __ValkarnTaskLifetimeToken);
await WriteSaveToDisk();
}
}

// Cette méthode NE SERA PAS auto-annulée — elle doit finir d'écrire
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk(CancellationToken ct = default)
{
await FileSystem.WriteAsync(_saveData, ct);
}
}

[NoAutoCancel] est un attribut au niveau de la méthode. Le générateur exclut simplement cette méthode de son décompte de méthodes qualifiantes. Si toutes les méthodes d'une classe portent [NoAutoCancel], le générateur n'émet rien pour cette classe.

Analyseur : TT014 — NoAutoCancel sans paramètre CancellationToken

Un analyseur compagnon (NoAutoCancelAnalyzer) rapporte le diagnostic TT014 quand vous appliquez [NoAutoCancel] à une méthode qui n'a pas de paramètre CancellationToken. S'il n'y a pas de paramètre token, la méthode n'a aucun moyen d'observer l'annulation — ce qui signifie que [NoAutoCancel] est présent mais n'a aucun effet pratique. Cela signifie généralement que vous avez oublié d'ajouter le token :

// TT014 : [NoAutoCancel] appliqué mais la méthode n'a pas de paramètre CancellationToken
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk() // <-- paramètre ct manquant
{
await FileSystem.WriteAsync(_saveData);
}

Corrigez en ajoutant un paramètre CancellationToken :

[NoAutoCancel]
async ValkarnTask WriteSaveToDisk(CancellationToken ct = default)
{
await FileSystem.WriteAsync(_saveData, ct);
}

L'attribut [FireAndForget]

[FireAndForget] est un attribut séparé et complémentaire qui marque une méthode async comme intentionnellement non attendue. Il sert deux objectifs :

  1. Supprime les avertissements VTASKS-TASK002 et VTASKS-TASK013, qui se déclenchent quand les appelants n'attendent pas une valeur de retour ValkarnTask.
  2. Signale l'intention — les futurs lecteurs du code savent que l'abandon est délibéré.

Le générateur de source enveloppe les méthodes [FireAndForget] pour s'assurer que toutes les exceptions non observées sont publiées via le gestionnaire d'exceptions non observées de ValkarnTasks plutôt que d'être silencieusement perdues.

public partial class SpawnManager : MonoBehaviour
{
void OnPlayerDied()
{
// Pas d'avertissement, l'intention est claire
ShowDeathScreenAsync();
}

[FireAndForget]
async ValkarnTask ShowDeathScreenAsync()
{
await ValkarnTask.Delay(500, __ValkarnTaskLifetimeToken);
_deathScreen.SetActive(true);
}
}

[FireAndForget] et [NoAutoCancel] sont indépendants et peuvent être combinés :

[FireAndForget]
[NoAutoCancel]
async ValkarnTask PlayGlobalMusicAsync(CancellationToken ct = default) { ... }

Analyseur : TT010 — Auto-Cancel actif

L'AutoCancelInfoAnalyzer rapporte un diagnostic informationnel TT010 sur chaque méthode async ValkarnTask dans un MonoBehaviour qui sera auto-annulée (c'est-à-dire qui ne porte pas [NoAutoCancel]). Ce n'est pas une erreur ou un avertissement — c'est une transparence intentionnelle afin que les développeurs puissent voir d'un coup d'œil quelles méthodes sont liées au cycle de vie.

Vous pouvez supprimer TT010 par méthode avec [NoAutoCancel], ou le désactiver à l'échelle du projet via .editorconfig si vous préférez ne pas le voir.

Limitations

La classe doit être déclarée partial. Le générateur de source ne peut pas ajouter des membres à une classe non-partielle. Si votre MonoBehaviour n'est pas partial, le générateur le ignore silencieusement et aucune liaison n'est créée. Les attributs [NoAutoCancel] et [FireAndForget] fonctionnent toujours comme documentation et pour les analyseurs, mais __ValkarnTaskLifetimeToken ne sera pas disponible.

Classes imbriquées. Si un MonoBehaviour est déclaré comme classe imbriquée dans une autre classe, les déclarations de classe externe et interne doivent toutes deux être partial. Roslyn exige que tous les types englobants soient partial pour que les membres générés se compilent correctement.

Classes de base. La propriété __ValkarnTaskLifetimeToken générée est protected. Les sous-classes y accèdent automatiquement. Le générateur s'exécute pour chaque classe dans la hiérarchie indépendamment ; si une classe de base et une classe dérivée sont toutes deux des partial MonoBehaviours avec des méthodes async, chacune obtient sa propre génération partielle, mais elles partagent le même token sous-jacent parce que destroyCancellationToken est hérité de la base MonoBehaviour.

Héritage multiple. C# ne supporte pas l'héritage multiple de classes. Un MonoBehaviour ne peut avoir qu'une seule classe de base, donc il n'y a pas d'ambiguïté sur quel destroyCancellationToken lier.

ScriptableObjects. Le générateur cible actuellement uniquement MonoBehaviour. ScriptableObject n'a pas d'équivalent destroyCancellationToken dans l'API Unity, donc la génération d'auto-annulation n'est pas disponible pour eux.