حسین احمدی
بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

آموزش استفاده از Finalizable و Disposable در سی شارپ به زبان ساده

در دوره آموزشی مقدماتی زبان سی شارپ با مفهوم سازنده ها (Constructors) آشنا شدیم و گفتیم که Constructor ها برای مقدار دهی اولیه خصوصیات و فیلد ها زمان ساخت شئ استفاده می شوند. اما مفهوم دیگری نیز وجود دارد به نام Finalize که زمان حذف شئ از داخل حافظه رخ می دهد. برای درک این مفهوم بهتر است ابتدا کمی در مورد مکانیزم مدیریت حافظه در دات نت صحبت کنیم. همانطور که مطالب قبلی گفتیم، زمانی که شما شئ ای از روی یک کلاس ایجاد می کنید، برای متغیر تعریف شده فضایی در Stack ایجاد شده، شئ در حافظه Heap یا Managed Heap که یک حافظه مدیریت شده است ایجاد می شود و آدرس حافظه Heap در Stack قرار می گیرد.

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران

حافظه Heap حافظه ای است که دائماً توسط سرویسی به نام GC یا Garbage Collector مدیریت شده و اشیاء بدون استفاده آن توسط این سرویس حذف می شوند. زمان بندی بررسی و مدیریت حافظه Heap توسط GC، توسط CLR مدیریت می شود و نمی توان زمانبندی دقیقی برای آن ارائه کرد. زمانی که شئ ای توسط GC از حافظه حذف می شود، عملیاتی با نام Finalize بر روی آن انجام می شود که اصطلاحاً عملیات پاک سازی نهایی نام دارد. اما مکانیزم این کار به چه صورت است؟

در کلاس Object که کلاس پایه تمامی Type های دات نت است، متدی وجود دارد با نام Finalize که توسط چشم غیر مسلح قابل دیدن نیست!!!! این متد توسط GC و زمان حذف شئ از حافظه Heap فراخوانی می شود. متد Finalize یک متد virtual است، یعنی می توان رفتار آن را در کلاس های فرزند مجدد تعریف کرد، اما نه با override کردن آن! برای نوشتن متد Finalize در حقیقت باید یک Destructor تعریف کنیم که دقیقاً عکس عملیات Constrcutor رفتار می کند. برای مثال، کلاسی داریم با نام Person و برای آن یک Destructor تعریف می کنیم:

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    ~Person()
    {
        Console.WriteLine("Person destroyed!");
    }        
}

همانطور که مشاهده می کنید، برای نوشتن Destructor، کافیست مانند تعریف سازنده پیش فرض عمل کرده، فقط در ابتدای نام کلاس علامت ~ را قرار دهیم. زمان کامپایل شدن کد، این قطعه کد، به دستور زیر تبدیل می شود:

public override void Finalize()
{
    Console.WriteLine("Person destroyed!");
}

یعنی در حقیقت، با نوشتن Destructor، در حال override کردن متد Finalize در کلاس Object هستیم. این متد تنها در صورتی توسط GC فراخوانی می شود که در کلاس نوشته شده، Destructor نوشته شده باشد. این متد برای پاک سازی دستی حافظه استفاده می شود. برای مثال، فرض کنید داخل یک کلاس از یکسری منابع مانند فایل ها یا بانک های اطلاعاتی استفاده کرده اید و می خواهید پس از اتمام عمر شئ، عملیات پاک سازی منابع به صورت دستی انجام شود.

اینترفیس IDisposable و دستور using

در بخش قبلی با Destructor ها آشنا شدیم. شیوه دیگری برای پاک سازی دستی منابع استفاده شده توسط اشیاء وجود دارد و آن پیاده سازی اینترفیس IDisposable و استفاده از دستور using است. این اینترفیس متدی با نام Dispose دارد و بعد از پیاده سازی می توان دستورات جهت پاک سازی منابع را داخل آن نویست. برای مثال کلاس Person را به صورت Disposable تعریف می کنیم:

public class Person : IDisposable
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    ~Person()
    {
        Console.WriteLine("Person destroyed!");
    }

    public void Dispose()
    {
        // clean up resources here
    }
}

اما دلیل پیاده سازی اینترفیس IDisposable چیست؟ به طور حتم تمامی اینترفیس های موجود در کتابخانه دات نت وجودشان دلیل خاصی دارد. مثل اینترفیس IEnumerable که امکان پیمایش مجموعه ها توسط foreach را می دهد. اینترفیس IDisposable نیز به ما امکان استفاده از یک شئ در ساختار using را می دهد. نحوه استفاده از این دستور به شکل زیر است:

using (Person person = new Person())
{
    // write your code here
}

تنها امکان ساخت اشیاء ای در قسمت using وجود دارد که اینترفیس IDisposable را پیاده سازی کرده باشند. بعد از ورود به بدنه using، کدهای نوشته داخل بدنه using اجرا شده و به محض خروج از بدنه using، دستور Dispose که با پیاده سازی IDisposable ایجاد شده، فراخوانی می شود.اما تفاوت مکانیزم Finalize و Dispose چیست؟ در حقیقت Finalize توسط GC فراخوانی می شود و اصطلاحاً به آن Implicit Cleanup گفته می شود، اما امکان فراخوانی متد Dispose توسط کاربر و داخل کد وجود دارد که اصطلاحاً به آن Explicit Cleanup گفته می شود.

ترکیب قابلیت های Finalize و Disposable

تا اینجا با دو مکانیزم پاک سازی منابع، یعنی Finalize و Disposable و کارآیی هر یک آشنا شدیم. بهترین روش برای پاک سازی منابع، ترکیب استفاده از متدهای Finalize و اینترفیس IDisposable است که در زیر نمونه کد پیاده سازی آن را مشاهده می کنید:

public class Person : IDisposable
{
    private bool disposed = false;

    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    ~Person()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // free managed objects
            }
            // free unmanaged objects
            // other cleanups
            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

ابتدا فیلدی در کلاس تعریف کردیم با نام disposed که وضعیت کلاس که آیا عملیات Dispose بر روی آن انجام شده یا خیر را مشخص می کند. بعد یک متد virtual با نام Dispose تعریف می کنیم که این متد یک پارامتر ورودی به نام disposing را قبول می کند. این پارامتر مشخص می کند که آیا فراخوانی متد توسط بخش Dispose انجام شده یا بخش Finalize. داخل بدنه Dispose که پارامتری را به عنوان ورودی قبول می کند در صورت dispose نشده بودن شئ عملیات پاک سازی انجام می شود. در متد Dispose اصلی، ابتدا متد Dispose فراخوانی شده و بعد از آن با متد SuppressFinlize از فراخوانی متد Finalize توسط GC جلوگیری می شود. همچنین در Destructor، عملیات Dispose فراخوانی می شود.برای پیاده سازی Destructor ها، موارد زیر را حتماً مد نظر داشته باشید:

  1. تنها در صورتی عملیات Finalize را پیاده سازی کنید که به آن حتماً نیاز است، چون این عمل بر روی Performance برنامه شما تاثیر می گذارد.
  2. در صورت نیاز به عملیات Finalize، حتماً اینترفیس IDisposable را نیز پیاده سازی کنید تا در حد امکان از فراخوانی متد Finalize جلوگیری شود.
  3. متدی که برای عملیات Finalize می نویسید حتماً به صورت protected تعریف شود نه public!
  4. در صورتی که متد حاوی پروسه Finalize در کلاس پدر نوشته شده و در کلاس های فرزند آن را override کرده اید، حتماً نسخه موجود در کلاس پدر را نیز در کلاس فرزند فراخوانی کنید.

امیدوارم که این آموزش مورد توجه دوستان قرار گرفته باشد. ITPRO باشید

نویسنده: حسین احمدی

انجمن تخصصی فناوری اطلاعات ایران


حسین احمدی
حسین احمدی

بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

حسین احمدی ، بنیانگذار TOSINSO ، توسعه دهنده وب و برنامه نویس ، بیش از 12 سال سابقه فعالیت حرفه ای در سطح کلان ، مشاور ، مدیر پروژه و مدرس نهادهای مالی و اعتباری ، تخصص در پلتفرم دات نت و زبان سی شارپ ، طراحی و توسعه وب ، امنیت نرم افزار ، تحلیل سیستم های اطلاعاتی و داده کاوی ...

نظرات