بعض الكائنات تمسك موارد لا يديرها جامع النفايات: ملفات مفتوحة، ومقابس شبكة، واتصالات قواعد بيانات، ومقابض نظام التشغيل. تنفّذ هذه IDisposable، وتحرّر المورد باستدعاء Dispose(). وتستدعيها العبارة using عنك في نقطة ثابتة، حتى إن رُمي استثناء.
هذه الصفحة عن العبارة using. أما أسطر using System; في أعلى الملف فميزة مختلفة، هي الموجّهة using، التي تستورد فضاء أسماء.
كتلة using
تنشئ using (declaration) { body } الكائن، وتنفّذ الجسم، ثم تستدعي Dispose. يُظهر صنف صغير يطبع من Dispose بالضبط متى يحدث ذلك:
المخرجات:
open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled
ترمي الكتلة الثانية استثناءً، وما زالت "close api" تُطبع، قبل أن تعالج catch الاستثناء. هذا الضمان سبب استخدام using بدل استدعاء Dispose() بنفسك في السطر الأخير: الاستدعاء اليدوي يتخطاه أي استثناء يُرمى قبله.
ما تُترجم إليه using
يوسّع المترجم كتلة using إلى try/finally. هذان متكافئان:
using (var writer = new StreamWriter("log.txt"))
{
writer.WriteLine("started");
}
// is compiled as:
{
var writer = new StreamWriter("log.txt");
try
{
writer.WriteLine("started");
}
finally
{
if (writer != null) ((IDisposable)writer).Dispose();
}
}
ثلاثة تفاصيل تنتج عن هذا التوسيع. المتغيّر محصور في الكتلة وللقراءة فقط داخلها (لا يمكنك إعادة الإسناد إلى writer). ويجب أن ينفّذ الكائن IDisposable، وإلا لا تُترجم الشيفرة. والقيمة null مسموحة: يُتخطّى Dispose ببساطة، وهذا مفيد حين قد لا يعيد مصنع شيئًا.
الملفات والتدفقات
أصناف الملفات والتدفقات هي الحالة اليومية. يخزّن StreamWriter النص مؤقتًا في الذاكرة، وتدفع Dispose المخزن المؤقت إلى القرص وتغلق مقبض الملف:
المخرجات:
id,total
1001,59.90
1002,12.50
دون Dispose قد تظل الأسطر الأخيرة للكاتب في مخزنه المؤقت، وعلى Windows يمنع المقبض المفتوح البرامج الأخرى (وFile.Delete التالية لديك) من لمس الملف. أما الدوال المساعدة مثل File.WriteAllText وFile.ReadAllLines فتفتح وتتخلّص داخليًا، فلا تحتاج إلى using. راجع الملفات للمزيد.
عدة موارد: ترتيب التخلّص
كدّس عبارات using دون أقواس بينها لفتح عدة موارد في كتلة واحدة. يُتخلّص منها بعكس ترتيب الإنشاء:
المخرجات:
acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a
الترتيب المعكوس هو ما تحتاجه الموارد الطبقية: كاتب يغلّف مخزنًا يغلّف ملفًا، فيجب أن يدفع الكاتب إلى المخزن قبل أن يكتب المخزن إلى الملف، قبل أن يُغلق الملف.
تصريح using (C# 8)
أضافت C# 8 شكلًا بلا أقواس. يُتخلّص من المتغيّر حين ينتهي النطاق المحيط، عادة نهاية الدالة:
static void ExportReport(string path, IEnumerable<string> rows)
{
using var writer = new StreamWriter(path);
writer.WriteLine("Report");
foreach (string row in rows)
{
writer.WriteLine(row);
}
} // writer.Dispose() runs here, at the end of the method
يزيل مستوى من الإزاحة، وهذا يتراكم حين تستخدم دالة موردين أو ثلاثة. وقاعدة ترتيب التخلّص نفسها: التصريحات في نطاق واحد يُتخلّص منها بعكس الترتيب. والمقايضة أن المورد يبقى مفتوحًا حتى ينتهي النطاق، فحين يجب إغلاق ملف قبل أن تؤدي الدالة عملًا آخر فأبقِ شكل الكتلة أو ضع التصريح في كتلة { } خاصة به.
وللكائنات التي تنظيفها غير متزامن (IAsyncDisposable، مثل كثير من اتصالات قواعد البيانات والتدفقات) لدى C# 8 أيضًا await using، التي تنتظر DisposeAsync() في الموضع نفسه.
تنفيذ IDisposable في صنفك
نفّذ IDisposable حين يملك صنفك شيئًا قابلًا للتخلّص منه (أنشأ تدفقًا أو مؤقّتًا أو اتصالًا) ويجب عليه بالتالي التخلّص منه بدوره. للحالة الشائعة تكون دالة قصيرة:
المخرجات:
1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException
القواعد التي يتبعها: تتخلّص Dispose من كل ما يملكه الكائن؛ واستدعاؤها أكثر من مرة لا يفعل شيئًا في المرة الثانية؛ واستخدام الكائن بعد التخلّص يرمي ObjectDisposedException. وتُظهر using (log) أيضًا أن الكتلة يمكن أن تأخذ متغيّرًا موجودًا بدل تصريح.
"نمط التخلّص" الأطول بدالة protected virtual void Dispose(bool disposing) ومُنهٍ لا يلزم إلا حين يمسك صنف مقبضًا غير مُدار مباشرة (مؤشرًا خامًا من شيفرة أصلية). وفي .NET الحديثة غلّف مثل هذه المقابض في صنف فرعي من SafeHandle بدلًا من ذلك، فتكون النسخة البسيطة أعلاه كل ما يحتاجه صنفك.
أخطاء شائعة
- إعادة كائن أُنشئ في كتلة
using. يُتخلّص منه حين تخرج الكتلة، فيتلقى المستدعي تدفقًا مغلقًا. أعد البيانات، أو دع المستدعي يملكusing. - نسيان
usingمع التدفقات والكتّاب. قد تضيع البيانات المخزّنة مؤقتًا وتبقى الملفات مقفلة. - التخلّص من كائن مشترك. لا تتخلّص إلا مما تملكه. فـ
HttpClientطويل العمر مثلًا مقصود أن يُشارك ويُعاد استخدامه، لا أن يُنشأ ويُتخلّص منه لكل طلب. - استخدام كائن بعد الكتلة. الدوال على كائن متخلَّص منه ترمي
ObjectDisposedException. - الاعتماد على جامع النفايات. يحرّر الذاكرة، لا الملفات ولا المقابس، ولا يستدعي
Disposeأبدًا.
الأسئلة الشائعة
ماذا تفعل العبارة using في C#؟
تستدعي using (var x = ...) { ... } الدالة x.Dispose() حين تنتهي الكتلة، سواء انتهت بشكل طبيعي أو عبر return أو لأن استثناءً رُمي. يحوّلها المترجم إلى try/finally مع استدعاء Dispose في finally، فيُحرَّر المورد في نقطة معروفة بدل أي وقت يعمل فيه جامع النفايات.
ما هي IDisposable في C#؟
IDisposable واجهة بدالة واحدة، void Dispose(). ينفّذها الصنف حين يمسك شيئًا يجب تحريره صراحة: مقبض ملف، أو مقبس شبكة، أو اتصال قاعدة بيانات، أو مؤقّت. وأي كائن ينفّذ نوعه IDisposable يجب التخلّص منه حين تنتهي منه، عادة بـ using.
ما الفرق بين using وusing var؟
تتخلّص using (var x = ...) { } في نهاية كتلتها. أما تصريح using using var x = ...; (C# 8 وما بعده) فلا كتلة خاصة به: يتخلّص حين ينتهي النطاق المحيط، عادة نهاية الدالة. يوفّر التداخل حين يجب أن يعيش المورد بقية الدالة.
بأي ترتيب يُتخلّص من عدة كائنات using؟
بعكس ترتيب الإنشاء. مع using (var a = ...) using (var b = ...) { } يُتخلّص من b أولًا ثم من a. وهذا الترتيب الذي تريده حين يعتمد b على a، مثل StreamWriter يغلّف FileStream.
هل يستدعي جامع النفايات Dispose؟
لا. يحرّر جامع النفايات الذاكرة، وقد يشغّل مُنهيًا (finalizer) في وقت لاحق غير متوقع، لكنه لا يستدعي Dispose أبدًا. الملف الذي لم يُتخلّص منه قد يبقى مقفلًا، وقد لا تُكتب بياناته المخزّنة مؤقتًا أبدًا. ولهذا وُجدت using.