مقدمه

در درس‌های گذشته، به تفاوت بین حافظه‌ی مدیریت‌شده (managed) که توسط زباله‌روب (GC) کنترل می‌شود، و منابع مدیریت‌نشده (unmanaged) مانند فایل‌ها یا اتصالات شبکه اشاره کردیم. یاد گرفتیم که ما مسئول آزادسازی قطعی این منابع مدیریت‌نشده هستیم. اینترفیس IDisposable ابزار استاندارد .NET برای این کار است. در این درس، به بررسی الگوی صحیح و کاملی می‌پردازیم که برای پیاده‌سازی این اینترفیس به کار می‌رود. این الگو که به "Dispose Pattern" معروف است، تضمین می‌کند که منابع به شیوه‌ای ایمن و کارآمد آزاد می‌شوند و همچنین یک مکانیزم پشتیبان با استفاده از تخریب‌گر (Finalizer) برای مواقع ضروری فراهم می‌کند.

الگوی استاندارد پیاده‌سازی Dispose

پیاده‌سازی صحیح IDisposable کمی پیچیده‌تر از تعریف یک متد Dispose ساده است، به خصوص زمانی که کلاس شما هم منابع مدیریت‌شده و هم مدیریت‌نشده در اختیار دارد. الگوی استاندارد شامل چند بخش کلیدی است که با هم کار می‌کنند تا پاکسازی را در هر شرایطی تضمین کنند.

اجزای الگو

  1. پیاده‌سازی اینترفیس IDisposable: کلاس شما باید اینترفیس IDisposable را پیاده‌سازی کند.
  2. یک متد Dispose عمومی: این همان متدی است که توسط کاربر کلاس (معمولاً از طریق بلوک using) فراخوانی می‌شود.
  3. یک متد protected virtual Dispose(bool): این متد قلب الگو است. منطق اصلی پاکسازی در اینجا قرار دارد. پارامتر bool مشخص می‌کند که آیا فراخوانی از طرف کاربر بوده یا از طرف GC.
  4. یک تخریب‌گر (Finalizer) (اختیاری): این متد که با ~ مشخص می‌شود، به عنوان یک شبکه‌ی اطمینان عمل می‌کند و فقط در صورتی که کاربر فراموش به فراخوانی Dispose کرده باشد، توسط GC اجرا می‌شود.
  5. یک فیلد بولین برای جلوگیری از فراخوانی تکراری: برای جلوگیری از اجرای چندباره‌ی منطق پاکسازی، از یک فیلد خصوصی برای ردگیری اینکه آیا Dispose قبلاً فراخوانی شده یا نه، استفاده می‌کنیم.

پیاده‌سازی کامل الگو

بیایید تمام این اجزا را در قالب یک کلاس نمونه به نام ResourceWrapper که یک منبع فرضی را مدیریت می‌کند، پیاده‌سازی کنیم.

Copy Icon Program.cs
public class ResourceWrapper : IDisposable
{
    // A flag to detect redundant calls.
    private bool _disposed = false;

    // Public implementation of Dispose pattern.
    public void Dispose()
    {
        // Call the protected Dispose method with 'true'
        Dispose(true);
        // Tell the GC that the finalizer is no longer needed.
        GC.SuppressFinalize(this);
    }

    // Protected implementation of Dispose pattern.
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return; // If already disposed, do nothing.

        if (disposing)
        {
            // Free any managed objects here.
            // Example: if you have a List or other IDisposable objects.
            Console.WriteLine("Disposing managed resources.");
        }

        // Free any unmanaged resources here (e.g., file handles, pointers).
        Console.WriteLine("Disposing unmanaged resources.");
        
        _disposed = true;
    }

    // A finalizer (destructor) as a safety net.
    ~ResourceWrapper()
    {
        // Call the protected Dispose method with 'false'.
        Dispose(false);
    }
}

بررسی منطق الگو

بیایید ببینیم در هر سناریو چه اتفاقی می‌افتد:

  1. سناریوی عادی (فراخوانی از طریق using):
    • کلاینت شیء را در یک بلوک using ایجاد می‌کند.
    • پس از خروج از بلوک، متد عمومی Dispose() فراخوانی می‌شود.
    • این متد، Dispose(true) را فراخوانی می‌کند.
    • درون Dispose(bool disposing)، چون disposing برابر با true است، هم منابع مدیریت‌شده و هم مدیریت‌نشده آزاد می‌شوند.
    • در نهایت، GC.SuppressFinalize(this) فراخوانی می‌شود و GC دیگر تخریب‌گر را برای این شیء اجرا نخواهد کرد. این یک بهینه‌سازی مهم است.
  2. سناریوی فراموشی (کاربر Dispose را فراخوانی نمی‌کند):
    • شیء ایجاد می‌شود اما کاربر هرگز Dispose را فراخوانی نمی‌کند.
    • پس از مدتی، GC شیء را غیرقابل دسترس تشخیص می‌دهد.
    • چون شیء یک تخریب‌گر دارد، GC قبل از پاک کردن حافظه، تخریب‌گر را فراخوانی می‌کند.
    • تخریب‌گر، Dispose(false) را فراخوانی می‌کند.
    • درون Dispose(bool disposing) چون disposing برابر با false است، بلوک `if` اجرا نمی‌شود و فقط منابع مدیریت‌نشده آزاد می‌گردند. این بسیار مهم است، زیرا در این مرحله نباید به دیگر اشیاء مدیریت‌شده دست زد، چون ممکن است خود آن‌ها نیز توسط GC پاک شده باشند.

استفاده از الگوی پیاده‌سازی شده

پس از پیاده‌سازی صحیح این الگو، استفاده از آن بسیار ساده است و باید همیشه از طریق بلوک using انجام شود تا از آزادسازی قطعی منابع اطمینان حاصل شود.

Copy Icon 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.");
    } // rw.Dispose() is called automatically here.
    
    Console.WriteLine("\nUsing the object without 'using' statement...");
    ResourceWrapper rw2 = new ResourceWrapper();
    // Here we "forget" to call Dispose(). The finalizer will eventually run.
}

اجرای این کد نشان می‌دهد که در حالت اول، پیام‌های پاکسازی بلافاصله پس از خروج از بلوک using چاپ می‌شوند، در حالی که در حالت دوم، پیام تخریب‌گر ممکن است با تأخیر یا اصلاً (بسته به زمانبندی GC) چاپ شود که نشان‌دهنده‌ی اهمیت آزادسازی قطعی است.