بستار یا Closure در C# (قسمت اول)
سید منصور عمرانی
تاریخ انتشار: 1392/10/17
در این پست میخواهیم ببینیم بستار چیست و نحوهی استفاده از آن در زبان C# چگونه است.
بستار یا closure (کلوژر) یکی از قابلیتهای زبانهای تابعی یا functional مانند LISP یا ML است. مفهوم بستار در اواسط دههی 1960 تعریف شد، اما نخستین بار در سال 1975 در زبان تابعی Scheme پیادهسازی شد. در زبانهای تابعی توابع میتوانند خود را به محیطی که در آن تعریف شدهاند متصل کرده و از متغیرهایی که بیرون آنها در آن محیط وجود دارد استفاده کنند، حتی با وجودی که حوزهی دید آن محیط در دسترس نباشد یا خاتمه پیدا کند.
تعریف بستار
بستار یا closure (کلوژر) یکی از قابلیتهای زبانهای تابعی یا functional مانند LISP یا ML است. مفهوم بستار در اواسط دههی 1960 تعریف شد، اما نخستین بار در سال 1975 در زبان تابعی Scheme پیادهسازی شد. در زبانهای تابعی توابع میتوانند خود را به محیطی که در آن تعریف شدهاند متصل کرده و از متغیرهایی که بیرون آنها در آن محیط وجود دارد استفاده کنند، حتی با وجودی که حوزهی دید آن محیط در دسترس نباشد یا خاتمه پیدا کند.
تعریف بستار در Wikipedia چنین است:
In programming languages, a closure (also lexical closure or function closure) is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
یعنی « در علم کامپیوتر، بستار (یا بستار لغوی یا تابع بستار) تابع یا ارجاعی به یک تابع به همراه یک محیط مورد ارجاع است - محیط مورد ارجاع نیز جدولی است که ارجاعی به متغیرهای غیر محلی استفاده شده توسط تابع را نگهداری میکند که به آنها متغیرهای آزاد یا upvalue گفته میشود »
این تعریف علمی بستار است. ممکن است این تعریف کمی گنگ باشد، لذا بگذارید آن را کمی سادهتر کنیم:
« بستار تابعی مستقل است که از متغیرهای غیر محلی استفاده میکند (متغیرهایی که بیرون و در محل تعریف او و نه داخل او تعریف شدهاند). »
پیشینهی بستار
این قسمت را بر اساس ترجمه از Wikipedia نقل میکنیم:
بستار در سال 1964 توسط پیتر لندین به عنوان چیزی که یک بخش محیطی و یک بخش کنترلی دارد تعریف شد. او این مفهوم را در ماشین SECD خود برای ارزیابی عبارتها به کار برد. بعدا جوول موزز بستار را برای ارجاع به عبارت لامبدایی که قیود باز آن (متغیرهای آزاد آن) توسط محیط لغوی، بسته شده یا در محیط لغوی مقید شده است و باعث شده است یک عبارت بسته یا یک بستار شکل بگیرد. سپس بستار با این تعریف توسط ساسمن و استیل در توسعهی زبان Scheme به کار گرفته شد و به دنبال آن در جوامع نرمافزار، رواج پیدا کرد.
گاهی به بستار به اشتباه « تابع ناشناس » (توابعی که فاقد نام هستند) گفته میشود. اما این صحیح نیست. بستار تابعی است که متغیر آزاد هم دارد.
بستار در C#
بستار در C# 2.0 (.NET 2.0) با استفاده از متدهای ناشناس برای زبان C# فراهم شد و در C# 3.0 (.NET 3.5) با استفاده از جملهها و عبارتهای لامبدا بهبود پیدا کرد.
در C# 2.0 میتوانیم بستار را با استفاده از متدهای ناشناس ایجاد کنیم:
Func<int, double> sqrt = delegate(n) { return Math.Sqrt(n); };
متدهای ناشناس متدهای مستقلی هستند که با استفاده از کلمهی delegate تعریف شده و به کلاسی مشخصی تعلق ندارند. این متدها در .NET 2.0 معرفی شدند. در C# 3.0 نیز میتوانیم بستار را با استفاده از عبارت لامبدا به شکل سادهتر تعریف کنیم:
Func<int, double> sqrt = n => Math.Sqrt(n);
پس از تعریف متد ناشناس یا عبارت لامبدا میتوانیم آن را مانند سایر متدها و توابع عادی صدا بزنیم:
int n = sqrt(5); // n will contain 25
توجه کنید این مثالها بستار نبوده و تنها نمونهای از کاربرد متد ناشناس یا عبارت لامبدا هستند. زیرا هیچ یک از این مثالها از متغیر بیرونی یا آزاد استفاده نکردهاند. همان گونه که گفته شد بستار تابعی است که علاوه بر مستقل بودنش از متغیر آزاد هم استفاده کند.
سوال: فرق بستار با نماینده (delegate) در C# چیست؟
پاسخ: بستار یک تابع است، اما نماینده کلاسی است که نمایندگی مجموعهای از متدها را بر عهده دارد که امضایشان با امضای نماینده هماهنگی دارد. در مثال بالا، Func<int, double> یک نماینده و عبارت لامبدای n => Math.Sqrt(n)، خود بستار است (که البته گفتیم واقعا بستار نیست، زیرا از متغیر بیرونی استفاده نمیکند، ولی فعلا از این مساله صرفنظر میکنیم).
به منظور درک بهتر فرق بستار با نماینده به مثال زیر توجه کنید:
Action action = null;string name = "Closure";action += ()=> Console.WriteLine(name + " 1");action += ()=> Console.WriteLine(name + " 2");action();// OUTPUT:// Closure 1// Closure 2
در اینجا Action یک نماینده است که از جنس آن متغیری به نام action تعریف کردهایم. از آنجایی که Action در حقیقت یک کلاس است میتوانیم به متغیر action مقدار null نسبت بدهیم. سپس دو بستار تعریف کرده و با استفاده از عملگر += به متغیر action نسبت دادهایم. اگر این مثال را اجرا کنید خواهید دید فراخوانی دستور action() باعث فراخوانی هر دو بستار میشود.
بستار و var
متدهای ناشناس و عبارتهای لامبدا و در نتیجه بستار را نمیتوانید در زبان C# با استفاده از کلمهی کلیدی var تعریف کنید و به متغیرهای محلی دارای نوع ضمنی (implicitly-typed local variables) نسبت بدهید.
var sqrt1 = n => Math.Sqrt(n); // won't compilevar sqrt2 = delegate (int n) { return Math.Sqrt(n); }; // won't compile too!
این که کامپایلر نمیتواند خط اول را کامپایل کند روشن است. زیرا نمیداند نوع n چیست. اما چرا نمیتواند sqrt2 را حتی با وجودی که نوع پارامتر آن مشخص است کامپایل کند؟ پاسخ این است که نمیداند نوع مقدار برگشتی این بستار چیست. ممکن است بپرسید آیا نمیتواند با استفاده از قابلیت استنتاج نوع (type inference) بر اساس نوع n، نوع مقدار برگشتی را استنتاج کرده و مثلا آن را double در نظر بگیرد؟ کُد زیر مشخص میکند چرا نمیتواند:
var sqrt2 = delegate (int n){ if (n >= 0) return Math.Sqrt(n); else return "Invalid input"};
در اینجا متد ناشناس، دو نوع دادهی مختلف بر میگرداند. لذا کامپایلر نمیتواند نتیجهگیری کند نوع برگشتی این متد ناشناس واقعا چیست (double است یا string). ممکن است بگویید عدم توانایی کامپایلر در استنتاج این نوع متدهای ناشناس را که در آنها چندین return وجود دارد میپذیریم. اما آیا کامپایلر نمیتواند اجازه بدهد حداقل متدی را که فقط یک return دارد و امکان تشخیص و استنتاج نوع برگشتیاش تا حدودی میسر است با استفاده از var تعریف کنیم؟ اتفاقا این سوال برای خودم هم پیش آمد. اما جوابی برایش پیدا نکردم. شاید علتش این باشد که کامپایلر (به دلایلی) نمیتواند استثناء قائل شود.
متغیرهای بیرونی یا آزاد
یکی از نکاتی که بستار را پیچیده میکند، استفادهی آن از متغیرهای آزاد یا متغیرهایی است که به خودش تعلق ندارد. یعنی داخل خودش تعریف نشده یا از طریق پارامتر به آن ارسال نشده است. وقتی یک بستار، متغیرهای بیرون خود را استفاده میکند، آن متغیرها به تسخیر بستار در میآیند. به مثال زیر توجه کنید:
class Test{ public Action Foo() { int a = 5; return () => Console.WriteLine("The value of 'a' is " + a); } public static void Main() { Action action = Foo(); action(); // writes: The value if 'a' is 5 }}
در اینجا متد Foo() بستاری بر میگرداند که متغیر محلی a را استفاده کرده است. سپس متد Foo() را فراخوانی میکنیم و مقدار برگشتیاش را به متغیر action نسبت میدهیم. با وجودی که فراخوانی متد Foo() به طور کامل خاتمه یافته است، اما وقتی در خط بعدی، بستار موجود در متغیر action را فراخوانی میکنیم، متغیر محلی a با پشتیبانی کامپایلر در حافظه باقی نگه داشته شده و حفظ میشود، حتی با وجودی که حوزهی دید آن محلی است و پس از اجرای متد Foo() حوزهی دیدش خاتمه پیدا میکند. متغیر a در اینجا یک متغیر آزاد یا بیرونی است و بستاری که Foo() بر میگرداند نسبت به آن بسته است.