Zum Hauptinhalt springen

Auto-Abbruch-Lebensdauer-Bindung

Einer der häufigsten Fehler im asynchronen Unity-Code ist das Starten einer Task von einem MonoBehaviour aus und das anschließende Vergessen, sie abzubrechen, wenn das Objekt zerstört wird. Die Task läuft weiter, versucht, auf zerstörte Unity-Objekte zuzugreifen, und wirft MissingReferenceException — oder schlimmer, korrumpiert lautlos den Zustand.

ValkarnTasks eliminiert diese Klasse von Fehlern durch einen Roslyn-Quellgenerator, der automatisch asynchrone Methoden mit der Destroy-Lebensdauer des Objekts verdrahtet.

Das Problem

Ohne Infrastruktur erfordert jede asynchrone Methode in einem MonoBehaviour, dass der Entwickler manuell ein CancellationToken durchfädelt:

// Manueller Ansatz — leicht zu vergessen, mühsam zu pflegen
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();
}
}
}

Je mehr asynchrone Methoden wachsen, desto mehr Boilerplate gibt es. Das Weglassen von OnDestroy — oder das Entsorgen in der falschen Reihenfolge — verursacht die oben beschriebenen Lecks.

Der generierte Ansatz

Deklarieren Sie Ihre Klasse als partial und ValkarnTasks kümmert sich um den Rest:

// Danach — partial deklarieren und der Generator übernimmt die Verdrahtung
public partial class EnemyAI : MonoBehaviour
{
void OnEnable()
{
RunPatrolLoop().Forget();
}

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

Kein CancellationTokenSource, kein OnDestroy, keine Entsorgung. Das generierte Token wird automatisch abgebrochen, wenn Unity das Objekt zerstört.

Wie der Quellgenerator funktioniert

Der Generator (LifecycleBindingGenerator) ist ein inkrementeller Roslyn-Generator, der zur Kompilierzeit läuft. Seine Pipeline hat drei Stufen.

Stufe 1 — Syntax-Filter

Der Generator untersucht jede Klassendeklaration in Ihrem Projekt. Eine Klasse gilt als Kandidat, wenn:

  • Sie mit dem partial-Schlüsselwort deklariert ist.
  • Sie eine Basisklassenliste hat (d. h. sie erbt von etwas).

Dieser Filter ist rein syntaktisch und sehr schnell. In dieser Phase läuft keine semantische Analyse.

Stufe 2 — Semantische Transformation

Für jede Kandidatenklasse verwendet der Generator das Roslyn-Semantikmodell, um:

  1. Zu bestätigen, dass die Klasse von UnityEngine.MonoBehaviour ableitet (geht die gesamte Vererbungskette durch).
  2. Alle Member aufzuzählen. Für jeden Member prüft es, ob er:
    • Eine async-Methode ist.
    • UnaPartidaMas.Valkarn.Tasks.ValkarnTask (oder ValkarnTask<T>) zurückgibt.
    • Kein [NoAutoCancel] trägt.
  3. Wenn keine qualifizierenden Methoden gefunden werden, wird die Klasse lautlos übersprungen — es wird nichts generiert.
  4. Nur die erste partielle Deklaration wird verarbeitet. Wenn eine Klasse über mehrere Dateien verteilt ist, gibt der Generator einmal Code aus, gebunden an die erste Deklaration, um doppelte Member zu vermeiden.

Stufe 3 — Code-Ausgabe

Für jede Klasse, die Stufen 1 und 2 besteht, schreibt der Generator eine neue .g.cs-Datei. Der generierte Code für eine Klasse namens EnemyAI im Namespace Game.Enemies sieht so aus:

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

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

/// <summary>
/// Abbruch-Token, das ausgelöst wird, wenn dieses MonoBehaviour zerstört wird.
/// Automatisch generiert durch den ValkarnTask-Quellgenerator.
/// </summary>
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
protected CancellationToken __ValkarnTaskLifetimeToken
{
get
{
if (__valkarnTaskLifetimeCts == null)
__valkarnTaskLifetimeCts =
CancellationTokenSource.CreateLinkedTokenSource(destroyCancellationToken);
return __valkarnTaskLifetimeCts.Token;
}
}
}
}

Wichtige Details:

  • Die CancellationTokenSource ist lazy — wird nur beim ersten Zugriff auf __ValkarnTaskLifetimeToken alloziert.
  • Sie ist verknüpft mit Unitys eingebautem destroyCancellationToken (MonoBehaviour.destroyCancellationToken, seit Unity 2022 verfügbar). Wenn Unity das Objekt zerstört, löst destroyCancellationToken aus, was sich auf __valkarnTaskLifetimeCts überträgt, das __ValkarnTaskLifetimeToken abbricht.
  • Sowohl das Feld als auch die Eigenschaft sind mit EditorBrowsable(Never) markiert, sodass sie IntelliSense für Benutzer der Klasse nicht verunreinigen.
  • Die Eigenschaft ist protected, sodass Unterklassen dasselbe Token verwenden können.

Das [NoAutoCancel]-Attribut

Wenden Sie [NoAutoCancel] auf eine beliebige asynchrone ValkarnTask-Methode an, wenn Sie möchten, dass sie absichtlich über die Lebensdauer des Objekts hinaus weiterläuft. Häufige Szenarien:

  • Eine Methode, die Daten auf Disk speichert und abgeschlossen werden muss, auch wenn das auslösende Objekt zerstört wird.
  • Eine Methode, die eine gemeinsame Ressource verwaltet, die einem anderen System gehört.
  • Übergangseffekte, die absichtlich das Objekt überleben, das sie gestartet hat.
public partial class SaveManager : MonoBehaviour
{
// Diese Methode WIRD bei Destroy auto-abgebrochen
async ValkarnTask AutoSaveLoop()
{
while (true)
{
await ValkarnTask.Delay(30_000, __ValkarnTaskLifetimeToken);
await WriteSaveToDisk();
}
}

// Diese Methode wird NICHT auto-abgebrochen — sie muss das Schreiben beenden
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk(CancellationToken ct = default)
{
await FileSystem.WriteAsync(_saveData, ct);
}
}

[NoAutoCancel] ist ein Methoden-Ebenen-Attribut. Der Generator schließt diese Methode einfach aus seiner Anzahl qualifizierender Methoden aus. Wenn alle Methoden in einer Klasse [NoAutoCancel] tragen, gibt der Generator nichts für diese Klasse aus.

Analyzer: TT014 — NoAutoCancel ohne CancellationToken-Parameter

Ein Begleiter-Analyzer (NoAutoCancelAnalyzer) meldet die Diagnose TT014, wenn Sie [NoAutoCancel] auf eine Methode ohne CancellationToken-Parameter anwenden. Wenn kein Token-Parameter vorhanden ist, hat die Methode keine Möglichkeit, einen Abbruch zu beobachten — das bedeutet, [NoAutoCancel] ist vorhanden, hat aber keine praktische Wirkung. Das bedeutet normalerweise, dass Sie den Token vergessen haben:

// TT014: [NoAutoCancel] angewendet, aber die Methode hat keinen CancellationToken-Parameter
[NoAutoCancel]
async ValkarnTask WriteSaveToDisk() // <-- fehlender ct-Parameter
{
await FileSystem.WriteAsync(_saveData);
}

Beheben Sie es, indem Sie einen CancellationToken-Parameter hinzufügen:

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

Das [FireAndForget]-Attribut

[FireAndForget] ist ein separates, ergänzendes Attribut, das eine asynchrone Methode als absichtlich nicht abgewartet markiert. Es dient zwei Zwecken:

  1. Unterdrückt Warnungen VTASKS-TASK002 und VTASKS-TASK013, die ausgelöst werden, wenn Aufrufer einen ValkarnTask-Rückgabewert nicht awaiten.
  2. Signalisiert die Absicht — zukünftige Leser des Codes wissen, dass das Verwerfen beabsichtigt ist.

Der Quellgenerator umhüllt [FireAndForget]-Methoden, um sicherzustellen, dass alle nicht beobachteten Ausnahmen über ValkarnTasks' nicht-beobachteten-Ausnahme-Handler veröffentlicht werden, anstatt lautlos verloren zu gehen.

public partial class SpawnManager : MonoBehaviour
{
void OnPlayerDied()
{
// Keine Warnung, die Absicht ist klar
ShowDeathScreenAsync();
}

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

[FireAndForget] und [NoAutoCancel] sind unabhängig und können kombiniert werden:

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

Analyzer: TT010 — Auto-Abbruch aktiv

Der AutoCancelInfoAnalyzer meldet eine informatorische Diagnose TT010 auf jeder asynchronen ValkarnTask-Methode in einem MonoBehaviour, die automatisch abgebrochen wird (d. h. kein [NoAutoCancel] hat). Dies ist kein Fehler oder keine Warnung — es ist beabsichtigte Transparenz, damit Entwickler auf einen Blick sehen können, welche Methoden lebensdauergebunden sind.

Sie können TT010 pro Methode mit [NoAutoCancel] unterdrücken oder es projektweit über .editorconfig deaktivieren, wenn Sie es nicht sehen möchten.

Einschränkungen

Die Klasse muss als partial deklariert sein. Der Quellgenerator kann keine Member zu einer nicht-partialen Klasse hinzufügen. Wenn Ihr MonoBehaviour nicht partial ist, überspringt der Generator es lautlos und es wird keine Bindung erstellt. Die Attribute [NoAutoCancel] und [FireAndForget] funktionieren weiterhin als Dokumentation und für die Analyzer, aber __ValkarnTaskLifetimeToken wird nicht verfügbar sein.

Verschachtelte Klassen. Wenn ein MonoBehaviour als verschachtelte Klasse innerhalb einer anderen Klasse deklariert ist, müssen sowohl die äußere als auch die innere Klassendeklaration partial sein. Roslyn erfordert, dass alle umschließenden Typen partial sind, damit generierte Member korrekt kompilieren.

Basisklassen. Die generierte __ValkarnTaskLifetimeToken-Eigenschaft ist protected. Unterklassen erben automatisch den Zugriff darauf. Der Generator läuft für jede Klasse in der Hierarchie unabhängig; wenn sowohl eine Basis- als auch eine abgeleitete Klasse partial MonoBehaviours mit asynchronen Methoden sind, erhält jede ihre eigene generierte Partial, aber sie teilen dasselbe zugrunde liegende Token, da destroyCancellationToken von der MonoBehaviour-Basis vererbt wird.

Mehrfachvererbung. C# unterstützt keine Mehrfachvererbung von Klassen. Ein MonoBehaviour kann nur eine Klassenbase haben, daher gibt es keine Mehrdeutigkeit darüber, welches destroyCancellationToken verknüpft werden soll.

ScriptableObjects. Der Generator zielt derzeit nur auf MonoBehaviour ab. ScriptableObject hat kein destroyCancellationToken-Äquivalent in der Unity-API, daher ist die Auto-Abbruch-Generierung für sie nicht verfügbar.