ربط دورة الحياة بالإلغاء التلقائي
من أكثر الأخطاء شيوعًا في كود Unity غير المتزامن هو إطلاق مهمة من MonoBehaviour ثم نسيان إلغائها عند تدمير الكائن. تستمر المهمة في التشغيل وتحاول الوصول إلى كائنات Unity المدمرة وترمي MissingReferenceException — أو الأسوأ من ذلك، تُفسد الحالة بصمت.
تُزيل ValkarnTasks هذا النوع من الأخطاء عبر مولّد كود Roslyn الذي يربط تلقائيًا الطرق غير المتزامنة بعمر تدمير الكائن.
المشكلة
بدون أي بنية تحتية، كل طريقة غير متزامنة في 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، لا تخلص. الرمز المُولَّد يُلغى تلقائيًا عندما تُدمّر Unity الكائن.
كيف يعمل مولّد الكود
المولّد (LifecycleBindingGenerator) هو مولّد Roslyn تدريجي يعمل عند الترجمة. خطوطه الأنابيب لها ثلاث مراحل.
المرحلة 1 — فلتر البنية
يفحص المولّد كل إعلان فئة في مشروعك. تُعتبَر الفئة مرشحة إذا:
- أُعلنت بكلمة المفتاح
partial. - لها قائمة فئة أساسية (أي ترث من شيء ما).
هذا الفلتر بنيوي بحت وسريع جدًا. لا يجري أي تحليل دلالي في هذه المرحلة.
المرحلة 2 — التحويل الدلالي
لكل فئة مرشحة، يستخدم المولّد نموذج Roslyn الدلالي لـ:
- التأكد من أن الفئة مشتقة من
UnityEngine.MonoBehaviour(يسير عبر سلسلة الوراثة الكاملة). - تعداد جميع الأعضاء. لكل عضو، يفحص ما إذا كان:
- طريقة
async. - تُرجع
UnaPartidaMas.Valkarn.Tasks.ValkarnTask(أوValkarnTask<T>). - لا تحمل
[NoAutoCancel].
- طريقة
- إذا لم تُوجَد طرق مؤهلة، تُتخطى الفئة بصمت — لا شيء يُولَّد.
- يُعالج الإعلان الأول فقط المُشارَك. إذا كانت فئة منقسمة عبر ملفات متعددة، يُصدر المولّد الكود مرة واحدة، مرتبطًا بالإعلان الأول، لتجنب الأعضاء المكررة.
المرحلة 3 — إصدار الكود
لكل فئة تجتاز المرحلتين 1 و2، يكتب المولّد ملف .g.cs جديدًا. الكود المُولَّد لفئة باسم EnemyAI في مساحة الأسماء Game.Enemies يبدو هكذا:
// <auto-generated/>
#nullable disable
using System.Threading;
namespace Game.Enemies
{
partial class EnemyAI
{
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
CancellationTokenSource __valkarnTaskLifetimeCts;
/// <summary>
/// رمز الإلغاء الذي يُطلَق عند تدمير هذا MonoBehaviour.
/// مُولَّد تلقائيًا بواسطة مولّد كود 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.- إنه مرتبط بـ
destroyCancellationTokenالمدمج في Unity (MonoBehaviour.destroyCancellationToken، متاح منذ Unity 2022). عندما تُدمّر Unity الكائن، يُطلَقdestroyCancellationToken، مما ينقل إلى__valkarnTaskLifetimeCts، مما يُلغي__ValkarnTaskLifetimeToken. - كلٌّ من الحقل والخاصية مُعلَّمان بـ
EditorBrowsable(Never)حتى لا يُلوّثا IntelliSense لمستخدمي الفئة. - الخاصية
protected، لذا الفئات الفرعية يمكنها استخدام نفس الرمز.
السمة [NoAutoCancel]
طبّق [NoAutoCancel] على أي طريقة ValkarnTask غير متزامنة عندما تريد عمدًا أن تستمر في التشغيل بعد انتهاء عمر الكائن. السيناريوهات الشائعة:
- طريقة تحفظ البيانات على القرص ويجب أن تكتمل حتى إذا دُمّر الكائن المُطلِق.
- طريقة تدير مورداً مشتركاً مملوكًا لنظام آخر.
- تأثيرات انتقالية تتجاوز عمدًا الكائن الذي بدأها.
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 — [NoAutoCancel] بدون معامل CancellationToken
محلل مرافق (NoAutoCancelAnalyzer) يُبلّغ عن التشخيص TT014 عندما تطبّق [NoAutoCancel] على طريقة لا تملك معامل CancellationToken. إذا لم يكن هناك معامل رمز، فليس للطريقة أي طريقة لملاحظة الإلغاء — مما يعني وجود [NoAutoCancel] دون تأثير عملي. هذا عادةً يعني أنك نسيت إضافة الرمز:
// 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، التي تُطلَق عندما لا ينتظر المُستدعون قيمة إرجاع
ValkarnTask. - تُشير إلى النية — قراء الكود المستقبليون يعرفون أن الإهمال مقصود.
يُغلّف مولّد الكود طرق [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 عن تشخيص معلوماتي TT010 على كل طريقة async ValkarnTask في MonoBehaviour ستُلغى تلقائيًا (أي لا تملك [NoAutoCancel]). هذا ليس خطأ أو تحذيرًا — بل شفافية مقصودة حتى يتمكن المطورون من رؤية الطرق المرتبطة بدورة الحياة دفعةً واحدة.
يمكنك كتم TT010 لكل طريقة بـ[NoAutoCancel]، أو تعطيله على مستوى المشروع عبر .editorconfig إذا فضّلت عدم رؤيته.
القيود
يجب إعلان الفئة partial. لا يمكن لمولّد الكود إضافة أعضاء لفئة غير partial. إذا لم تكن MonoBehaviour الخاصة بك partial، يتجاهلها المولّد بصمت ولا يُنشأ أي ربط. لا تزال سمتا [NoAutoCancel] و[FireAndForget] تعملان كتوثيق وللمحللين، لكن __ValkarnTaskLifetimeToken لن يكون متاحًا.
الفئات المتداخلة. إذا أُعلنت MonoBehaviour كفئة متداخلة داخل فئة أخرى، يجب أن يكون كلٌّ من إعلانَي الفئة الخارجية والداخلية partial. يتطلب Roslyn من جميع الأنواع المحيطة أن تكون partial حتى يُترجم الأعضاء المُولَّدون بشكل صحيح.
الفئات الأساسية. الخاصية المُولَّدة __ValkarnTaskLifetimeToken هي protected. الفئات الفرعية ترث الوصول إليها تلقائيًا. يعمل المولّد لكل فئة في التسلسل الهرمي بشكل مستقل؛ إذا كانت كلٌّ من فئة أساسية وفئة مشتقة partial من MonoBehaviours مع طرق غير متزامنة، تحصل كل منهما على partial مُولَّد، لكنهما يشتركان في نفس الرمز الأساسي لأن destroyCancellationToken موروث من أساس MonoBehaviour.
الوراثة المتعددة. C# لا تدعم وراثة متعددة من الفئات. لـMonoBehaviour قاعدة فئة واحدة فقط، لذا لا غموض حول أي destroyCancellationToken يجب الربط به.
ScriptableObjects. المولّد يستهدف MonoBehaviour فقط حاليًا. ScriptableObject ليس له ما يعادل destroyCancellationToken في Unity API، لذا لا يتوفر توليد الإلغاء التلقائي لها.