مقدمه
در درسهای گذشته، به تفاوت بین حافظهی مدیریتشده (managed) که توسط زبالهروب (GC) کنترل میشود،
و منابع مدیریتنشده (unmanaged) مانند فایلها یا اتصالات شبکه اشاره
کردیم. یاد گرفتیم که ما مسئول آزادسازی قطعی این منابع مدیریتنشده هستیم.
اینترفیس IDisposable ابزار استاندارد .NET برای این کار
است. در این درس، به بررسی الگوی صحیح و کاملی میپردازیم که برای پیادهسازی این اینترفیس به کار
میرود. این الگو که به "Dispose Pattern" معروف است، تضمین میکند که منابع به شیوهای ایمن و
کارآمد آزاد میشوند و همچنین یک مکانیزم پشتیبان با استفاده از تخریبگر (Finalizer) برای مواقع
ضروری فراهم میکند.
الگوی استاندارد پیادهسازی Dispose
پیادهسازی صحیح IDisposable کمی پیچیدهتر از تعریف یک متد Dispose ساده است، به خصوص زمانی که
کلاس شما هم منابع مدیریتشده و هم مدیریتنشده در اختیار دارد. الگوی استاندارد شامل چند بخش کلیدی
است که با هم کار میکنند تا پاکسازی را در هر شرایطی تضمین کنند.
اجزای الگو
- پیادهسازی اینترفیس IDisposable: کلاس شما باید اینترفیس IDisposable را
پیادهسازی کند.
- یک متد Dispose عمومی: این همان متدی است که توسط کاربر کلاس (معمولاً از
طریق بلوک using) فراخوانی میشود.
- یک متد protected virtual Dispose(bool): این متد قلب الگو است. منطق اصلی
پاکسازی در اینجا قرار دارد. پارامتر bool مشخص میکند که آیا فراخوانی از طرف کاربر بوده یا
از طرف GC.
- یک تخریبگر (Finalizer) (اختیاری): این متد که با ~ مشخص میشود، به عنوان
یک شبکهی اطمینان عمل میکند و فقط در صورتی که کاربر فراموش به فراخوانی Dispose کرده باشد،
توسط GC اجرا میشود.
- یک فیلد بولین برای جلوگیری از فراخوانی تکراری: برای جلوگیری از اجرای
چندبارهی منطق پاکسازی، از یک فیلد خصوصی برای ردگیری اینکه آیا Dispose قبلاً فراخوانی شده
یا نه، استفاده میکنیم.
پیادهسازی کامل الگو
بیایید تمام این اجزا را در قالب یک کلاس نمونه به نام ResourceWrapper که یک منبع فرضی را مدیریت
میکند، پیادهسازی کنیم.
Program.cs
public class ResourceWrapper : IDisposable
{
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
Console.WriteLine("Disposing managed resources.");
}
Console.WriteLine("Disposing unmanaged resources.");
_disposed = true;
}
~ResourceWrapper()
{
Dispose(false);
}
}
بررسی منطق الگو
بیایید ببینیم در هر سناریو چه اتفاقی میافتد:
- سناریوی عادی (فراخوانی از طریق using):
- کلاینت شیء را در یک بلوک using ایجاد میکند.
- پس از خروج از بلوک، متد عمومی Dispose() فراخوانی میشود.
- این متد، Dispose(true) را فراخوانی میکند.
- درون Dispose(bool disposing)، چون disposing برابر با true است، هم منابع
مدیریتشده و هم مدیریتنشده آزاد میشوند.
- در نهایت، GC.SuppressFinalize(this) فراخوانی میشود و GC دیگر تخریبگر را برای این
شیء اجرا نخواهد کرد. این یک بهینهسازی مهم است.
- سناریوی فراموشی (کاربر Dispose را فراخوانی نمیکند):
- شیء ایجاد میشود اما کاربر هرگز Dispose را فراخوانی نمیکند.
- پس از مدتی، GC شیء را غیرقابل دسترس تشخیص میدهد.
- چون شیء یک تخریبگر دارد، GC قبل از پاک کردن حافظه، تخریبگر را فراخوانی میکند.
- تخریبگر، Dispose(false) را فراخوانی میکند.
- درون Dispose(bool disposing) چون disposing برابر با false است، بلوک `if` اجرا
نمیشود و فقط منابع مدیریتنشده آزاد میگردند. این بسیار مهم است،
زیرا در این مرحله نباید به دیگر اشیاء مدیریتشده دست زد، چون ممکن است خود آنها نیز
توسط GC پاک شده باشند.
استفاده از الگوی پیادهسازی شده
پس از پیادهسازی صحیح این الگو، استفاده از آن بسیار ساده است و باید همیشه از طریق بلوک using انجام شود تا از آزادسازی قطعی منابع اطمینان حاصل شود.
Program.cs
static void Main()
{
Console.WriteLine("Using the object with a 'using' statement...");
using (ResourceWrapper rw = new ResourceWrapper())
{
Console.WriteLine("Inside the using block.");
}
Console.WriteLine("\nUsing the object without 'using' statement...");
ResourceWrapper rw2 = new ResourceWrapper();
}
اجرای این کد نشان میدهد که در حالت اول، پیامهای پاکسازی بلافاصله پس از خروج از بلوک using
چاپ میشوند، در حالی که در حالت دوم، پیام تخریبگر ممکن است با تأخیر یا اصلاً (بسته به زمانبندی
GC) چاپ شود که نشاندهندهی اهمیت آزادسازی قطعی است.