بررسی مفهوم Captured Variable در زبان سی شارپ
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: سه دقیقه

Capturing Outer Variables  
یک عبارت لامبدا می‌تواند از متغیرهای محلی و یا پارامترهای متدی که در آن تعریف شده است، استفاده نماید (Outer Variables). این متغیرها را captured variables می‌نامند. عبارت لامبدایی که از این متغیرها استفاده می‌کند، closure نامیده می‌شود. برای مثال:
static void Main()
{
 int factor = 2;
 Func<int, int> multiplier = n => n * factor;
 Console.WriteLine (multiplier (3)); // 6
}
در کد فوق multiplier یک delegate می‌باشد که ورودی صحیح n را گرفته و در مقدار factor ضرب کرده و بر می‌گرداند.

عبارت لامبدا زمانی ارزیابی می‌شود که delegate متناظر فراخوانی (Invoke) گردد؛ نه زمانیکه متغیر اصطلاحا capture می‌شود:
int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30
در کد فوق در زمانی که multiplier فراخوانی می‌شود مقدار factor برابر 10 ارزیابی شده و لذا عدد 30 چاپ خواهد شد.

عبارات لامبدا خود می‌توانند captured variable‌ها را تغییر دهند:
int seed = 0;
Func<int> natural = () => seed++;
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
Console.WriteLine (seed); // 2
در کد فوق natural یک delegate بدون ورودی و با یک خروجی integer می‌باشد. در ابتدا متغیر محلی seed تعریف شده و با مقدار اولیه 0 مقداردهی می‌شود. با هر بار اجرای natural مقدار seed به اندازه 1 واحد افزایش می‌یابد.
طول عمر(lifetime) متغیرهای captured شده در حد طول عمر delegate افزایش پیدا می‌کند. در مثال زیر متغیر محلی seed در حالت معمول، محدوده دیدی (scope) در حد تعریف این متغیر تا پایان اجرای متد دارد. اما از آنجاییکه در اینجا متغیر captured شده است، طول عمر آن در حدا طول عمر delegate افزایش می‌یابد: theNatural
static Func<int> Natural()
{
 int seed = 0;
 return () => seed++; // Returns a closure
}
static void Main()
{
 Func<int> theNatural = Natural();
 Console.WriteLine (theNatural ()); // 0
 Console.WriteLine (theNatural ()); // 1
}
اگر متغیر seed را در بدنه عبارت لامبدا تعریف نماییم، این متغیر برای هر بار اجرای delegate یکتا خواهد بود:
static Func<int> Natural()
{
 return() => { int seed = 0; return seed++; };
}
static void Main()
{
 Func<int> natural = Natural();
 Console.WriteLine (natural()); // 0
 Console.WriteLine (natural()); // 0
}

نکته: پیاده سازی پروسه Capture شدن متغیر، به این صورت است که این متغیرها به عنوان یک فیلد از یک کلاس (با سطح دسترسی private) در نظر گرفته می‌شوند. زمانیکه متد فراخوانی شد، کلاس مزبور وهله سازی شده و طول عمر آن به  طول عمر delegate گره می‌خورد.

Capturing iteration variables
در حلقه for، وقتی که متغیر حلقه توسط یک عبارت لامبدا capture می‌گردد، #C با آن متغیر طوری رفتار می‌کند که گویی در خارج از حلقه تعریف شده‌است و این بدان معناست که در هر بار تکرار حلقه، مقدار یکسانی برای متغیر در نظر گرفته می‌شود. کد زیر 333 را در خروجی چاپ می‌کند(بجای 012). 
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
actions [i] = () => Console.Write (i);
foreach (Action a in actions) a(); // 333
دلیل این موضوع این است که در هنگام اجرای delegate ها، هر delegate مقدار i را برابر مقدار آن در زمان اجرا می‌بیند و این مقدار در زمان اجرا برابر با 3 می‌باشد.
با نوشتن کد زیر می‌توان درک بهتری از موضوع پیدا کرد. 
Action[] actions = new Action[3];
int i = 0;
actions[0] = () => Console.Write (i);
i = 1;
actions[1] = () => Console.Write (i);
i = 2;
actions[2] = () => Console.Write (i);
i = 3;
foreach (Action a in actions) a(); // 333
اگر بخواهیم خروجی 012 چاپ شود راه حل به شرح زیر خواهد بود:
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
 int loopScopedi = i;
 actions [i] = () => Console.Write (loopScopedi);
}
foreach (Action a in actions) a(); // 012
زیرا هر متغیر loopScopedi در هر بار تکرار حلقه مجددا تعریف می‌گردد و لذا هر بار متغیر متفاوتی capture می‌گردد.
  • #
    ‫۶ سال و ۱ ماه قبل، دوشنبه ۲۹ مرداد ۱۳۹۷، ساعت ۱۶:۲۵
    یک نکته‌ی تکمیلی
    تشخیص این مورد به VS 2017 نگارش 15.8 اضافه شده‌است: