خبرهای فارسی

اخبار موسیقی، اخبار آی تی، اخبار ورزشی، اخبار استخدام

خبرهای فارسی

اخبار موسیقی، اخبار آی تی، اخبار ورزشی، اخبار استخدام

Closure چیست؟

اگر برنامه نویس C# باشید حتما به یاد دارید که Anonymous Method ها یا متد های بی نام یکی از ویژگی هایی بود که در نسخه دوم سی شارپ معرفی شد و همینطور در نسخه سوم عبارت های Lambda شکل بهتری به متد های بی نام دادند. مسلما بسیاری از ما کدی یا کد هایی مشابه زیر نوشته ایم:

1
2
3
4
5
6
7
static void Main(string[] args)
{
    int x = 0;
    Action action = delegate { Console.WriteLine(x); };
    x = 1;
    action();
}

یا به فرم ساده تر:

1
2
3
4
5
6
7
static void Main(string[] args)
{
    int x = 0;
    Action action = () => Console.WriteLine(x);
    x = 1;
    action();
}

در ابتدا ممکن است فکر کنید عدد صفر نمایش داده می شود، اما با اجرای برنامه خواهید دید جواب 1 است. اما چطور این اتفاق می افتد؟ در واقع سوال این است که متد های بی نام یا عبارات لامبدا چطور هنگام فراخوانی به پارامتر x و حتی مقدار آپدیت شده ی آن دسترسی پیدا می کنند؟

جواب این سوال ها وجود مفهومی است به نام Closure در پس این سینتکس ساده و روان.

Closure چیست؟

به بیان ساده Closure رفرنسی به یک تابع است، در عین حال که به scop ای که در آن تعریف شده است نیز اشاره دارد. بنابراین می توان عبارت های لامبدا و متد های بی نام را یک Closure دانست.

برای درک این تعریف لازم است چگونگی کار Closure ها را بررسی کنیم:

کامپایلر C# هنگام کامپایل برای هر متد بی نام یک کلاس تولید می کند و متغیر های قابل دسترس در آن scop را نیز به عنوان فیلد هایی از آن کلاس در نظر می گیرد، مثلا برای متد بی نام کد مثال اول، کلاسی مشابه زیر تولید می شود:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[CompilerGenerated]
private sealed class <>c__DisplayClass1
{
   public int x;
 
   public void
b__0()
   {
       Console.WriteLine(this.x);
   }
}
 
private static void Main(string[] args)
{
   <>c__DisplayClass1 class2 = new <>c__DisplayClass1();
   class2.x = 0;
   Action action = new Action(class2.
b__0);
   class2.x = 1;
   action();
}

همانطور که می بینید کلاس تولید شده فیلدی به نام x و متدی با بدنه متد بی نام را دارا می باشد که در تابع Main یک آبجکت از این کلاس ساخته شده و فیلد x مقدار دهی شده است و سپس متد موجود در این کلاس به عنوان متد بی نام به Action اختصاص یافته و در انتها مجددا مقدار x به 1 تغییر داده شده است.

در واقع با این مکانیزم است که یک Anonymous Method به کلیه مقادیر و فیلد ها، به خصوص مقادیر آپدیت شده متغیر ها دسترسی دارد.

بد نیست برای کامل تر شدن بحث مثال دیگری را نیز بررسی کنیم.

کد زیر را ببینید:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace Closure
{
    class Program
    {
        static Action[] actionArr = new Action[10];
 
        static void FillActions(int count)
        {
            for (int i = 0; i < count; i++)
            {
                actionArr[i] = delegate()
                {
                    Console.WriteLine("{0} ", i);
                };
            }
        }
 
        static void Main(string[] args)
        {
            FillActions(actionArr.Length);
 
            for (int i = 0; i < actionArr.Length; i++)
            {
                actionArr[i]();
            }
        }
    }
}

در نگاه اول احتمالا خواهید گفت که عدد 1 تا 10 نمایش داده خواهد شد، اما خروجی به صورت می باشد:

10 10 10 10 10 10 10 10 10 10

همانطور که گفته شد یک Closure رفرنسی به کلیه متغیر ها و خصوصیات قابل مشاهده در آن scop را در خود نگه می دارد. در این مثال حوزه فعالیت متغیر i درون حلقه for بوده و مقدار آن تا 10 افزایش میابد، دقت کنید که scop متغیر i تغییری نمی کند و یا پایان نپذیرفته و متغییر جدیدی ایجاد نمی شود، به همین دلیل است که کلیه متد ها به مقدار نهایی متغییر i اشاره دارند.

فکر می کنم نگاهی به کد تولید شده توسط کامپایلر موضوع را روشن تر کند:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static void FillActions(int count)
{
   Action action = null;
   <>c__DisplayClass2 class2 = new <>c__DisplayClass2();
   class2.i = 0;
   while (class2.i < count)
   {
       if (action == null)
       {
           action = new Action(class2.b__0);
       }
       actionArr[class2.i] = action;
       class2.i++;
   }
}

در واقع به دلیل داشتن یک scop ثابت برای متغیر i، تنها یک شی از کلاس <>c__DisplayClass2  ایجاد شده و بین کلیه action ها به اشتراک گذاشته شده است.

حال فرض کنید حلقه for را به صورت زیر تغییر دهیم:

1
2
3
4
5
6
7
8
for (int i = 0; i < count; i++)
{
    int j = i;
    actionArr[i] = delegate()
    {
        Console.WriteLine("{0} ", j);
    };
}

اینبار مشاهده خواهید کرد که اعداد 1 تا 10 نمایش داده می شود، چرا که scop متعیر j در هر بار اجرای حلقه پایان پذیرفته و مجددا ایجاد خواهد شد، بنابراین کامپایلر برای پیاده سازی درست مفهوم Closure که لازم است هر متد به scop خود اشاره کند، کدی مشابه زیر تولید خواهد کرد:

1
2
3
4
5
6
7
8
9
private static void FillActions(int count)
{
   for (int i = 0; i < count; i++)
   {
       <>c__DisplayClass2 class2 = new <>c__DisplayClass2();
       class2.j = i;
       actionArr[i] = new Action(class2.b__0);
   }
}

مشاهده می کنید که در هر حلقه یک شی جدید تولید شده و متغیر j مقدار دهی می شود.

پ.ن: بد نیست بدانید Closure یکی از مباحث پر کاربرد در زبان های functional محسوب می شود و ایده اولیه آن نیز از همین زبان های نشات گرفته شده و برای اولین بار پیاده سازی آن در زبان Scheme انجام شد.