সি # এর একটি লুপে ভেরিয়েবল ক্যাপচার করা হয়েছে


216

আমি সি # সম্পর্কে একটি আকর্ষণীয় ইস্যু পূরণ করেছি। আমার নীচের মত কোড আছে।

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(() => variable * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

আমি এটি 0, 2, 4, 6, 8 আউটপুট আশা করি যাইহোক, এটি আসলে পাঁচ দশকে আউটপুট দেয়।

দেখে মনে হচ্ছে এটি একটি ক্যাপচারেড ভেরিয়েবলের উল্লেখ করে সমস্ত ক্রিয়নের কারণে হয়েছে। ফলস্বরূপ, যখন তারা আহ্বান জানায়, তাদের সবার একই আউটপুট থাকে।

প্রতিটি ক্রিয়াকলাপের নিজস্ব ক্যাপচার ভেরিয়েবলের জন্য এই সীমাটি নিয়ে কাজ করার কোনও উপায় আছে কি?


15
বিষয়টি নিয়ে এরিক লিপার্টের ব্লগ সিরিজটিও দেখুন: লুপের পরিবর্তনশীল হিসাবে বিবেচিত ক্ষতিকারকটিকে বন্ধ করা
ব্রায়ান

10
এছাড়াও, তারা পূর্বাভাসের মধ্যে যেমনটি প্রত্যাশা করে তেমনি কাজ করতে সি # 5 পরিবর্তন করছে। (ভঙ্গ পরিবর্তন)
নিল টিব্রেওয়ালা


3
@ নিল: যদিও এই উদাহরণটি এখনও সি # 5 তে সঠিকভাবে কাজ করে না, কারণ এটি এখনও পাঁচ দশকে আউটপুট দেয়
আয়ান ওকে

6
এটি যাচাই করেছে যে এটি সি # 6.0 (ভিএস 2015) এ আজ অবধি পাঁচ দশকে বেরিয়ে গেছে। আমি সন্দেহ করি যে ক্লোজার ভেরিয়েবলের এই আচরণটি পরিবর্তনের প্রার্থী। Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured
আরবিটি

উত্তর:


196

হ্যাঁ - লুপের ভিতরে ভেরিয়েবলের একটি অনুলিপি নিন:

while (variable < 5)
{
    int copy = variable;
    actions.Add(() => copy * 2);
    ++ variable;
}

আপনি এটিকে ভাবতে পারেন যেন সি # সংকলক যখন পরিবর্তনশীল ঘোষণাকে আঘাত করে তখনই এটি একটি "নতুন" স্থানীয় পরিবর্তনশীল তৈরি করে। প্রকৃতপক্ষে এটি উপযুক্ত নতুন ক্লোজার অবজেক্ট তৈরি করবে এবং আপনি একাধিক স্কোপে ভেরিয়েবল উল্লেখ করলে এটি জটিল হয়ে পড়ে (বাস্তবায়নের ক্ষেত্রে) তবে এটি কার্যকর হয় :)

মনে রাখবেন যে এই সমস্যার আরও সাধারণ ঘটনাটি ব্যবহার করছে forবা foreach:

for (int i=0; i < 10; i++) // Just one variable
foreach (string x in foo) // And again, despite how it reads out loud

এর আরও বিশদ জানতে C # 3.0 টিপির 7.14.4.2 বিভাগের বিভাগটি দেখুন এবং ক্লোজার সম্পর্কে আমার নিবন্ধটিতে আরও উদাহরণ রয়েছে।

নোট করুন যে সি # 5 সংকলক হিসাবে এবং তার বাইরেও (সি # এর পূর্ববর্তী সংস্করণ নির্দিষ্ট করার সময়ও) এর আচরণ foreachবদলেছে যাতে আপনাকে আর স্থানীয় কপি তৈরি করতে হবে না। আরও তথ্যের জন্য এই উত্তর দেখুন ।


32
জনের বইয়েরও এ সম্পর্কে একটি খুব ভাল অধ্যায় রয়েছে (নম্র হওয়া বন্ধ করুন, জোন!)
মার্ক গ্র্যাভেল

35
যদি আমি অন্য লোকেরা এটি প্লাগ করতে দিই তবে এটি দেখতে আরও ভাল লাগবে;) (আমি স্বীকার করি যে আমি এর প্রস্তাব দেওয়ার মত জবাব দিতে চাই না।)
জন স্কিটি

2
বরাবরের মতো, skeet@pobox.com এর প্রতিক্রিয়া প্রশংসা করা হবে :)
জন স্কিটি

7
গ জন্য # 5.0 আচরণ পৃথক (অধিক যুক্তিসঙ্গত) জন স্কিট দ্বারা নতুন উত্তর দেখার - stackoverflow.com/questions/16264289/...
আলেক্সেই Levenkov

1
@ ফ্লোরাইমন্ড: সি # তে ক্লোজারগুলি কীভাবে কাজ করে তা ঠিক তা নয়। তারা ভেরিয়েবল ক্যাপচার , মান না । (লুপগুলি নির্বিশেষে এটি সত্য, এবং একটি লাম্বদা দিয়ে সহজেই প্রদর্শিত হয় যা একটি ভেরিয়েবল ক্যাপচার করে এবং যখনই এটি কার্যকর করা হয় কেবল বর্তমান মানটি মুদ্রণ করে))
জোন স্কিট

23

আমি বিশ্বাস করি আপনি যেটি অভিজ্ঞতা অর্জন করছেন সেটি হ'ল ক্লোজার http://en.wikedia.org/wiki/Closure_( কম্পিউটার কম্পিউটার_সায়েন্স) নামে পরিচিত । আপনার লাম্বার একটি ভেরিয়েবলের রেফারেন্স রয়েছে যা ফাংশনটির বাইরেই রয়েছে ed আপনার লাম্বা ব্যাখ্যা করা হয় না যতক্ষণ না আপনি এটি চাওয়া হয় এবং এটি একবার হলে এটি কার্যকর হওয়ার সময় ভেরিয়েবলের মান পাবে।


11

পর্দার আড়ালে, সংকলকটি এমন একটি শ্রেণি তৈরি করছে যা আপনার পদ্ধতি কলের জন্য ক্লোজডকে উপস্থাপন করে। এটি লুপের প্রতিটি পুনরাবৃত্তির জন্য ক্লোজার ক্লাসের একক উদাহরণটি ব্যবহার করে। কোডটি দেখতে এমন কিছু দেখাচ্ছে যা বাগটি কেন ঘটে তা দেখতে এটি আরও সহজ করে তোলে:

void Main()
{
    List<Func<int>> actions = new List<Func<int>>();

    int variable = 0;

    var closure = new CompilerGeneratedClosure();

    Func<int> anonymousMethodAction = null;

    while (closure.variable < 5)
    {
        if(anonymousMethodAction == null)
            anonymousMethodAction = new Func<int>(closure.YourAnonymousMethod);

        //we're re-adding the same function 
        actions.Add(anonymousMethodAction);

        ++closure.variable;
    }

    foreach (var act in actions)
    {
        Console.WriteLine(act.Invoke());
    }
}

class CompilerGeneratedClosure
{
    public int variable;

    public int YourAnonymousMethod()
    {
        return this.variable * 2;
    }
}

এটি আসলে আপনার নমুনা থেকে সংকলিত কোড নয়, তবে আমি আমার নিজস্ব কোড পরীক্ষা করেছি এবং এটি সংকলকটি আসলে কী উত্পন্ন করবে তার মতো দেখতে এটির মতো লাগে।


8

এর চারপাশের উপায়টি হ'ল প্রক্সি ভেরিয়েবলে আপনার প্রয়োজনীয় মানটি সংরক্ষণ করা এবং সেই পরিবর্তনশীলটি ক্যাপচার করা উচিত।

আই ই

while( variable < 5 )
{
    int copy = variable;
    actions.Add( () => copy * 2 );
    ++variable;
}

আমার সম্পাদিত উত্তরে ব্যাখ্যাটি দেখুন। আমি এখন অনুমানের প্রাসঙ্গিক বিটটি খুঁজেছি।
জন স্কিটি

হ্যাঁ জন, আমি আসলে আপনার নিবন্ধটি পড়েছি: csharpindepth.com / আর্টিকেলস / অধ্যাপনা 5 / ক্লসারস.এএসপিএক্স আপনি আমার বন্ধু ভাল কাজ করেন।
tjlevine

@ টিজেলেভাইন: অনেক ধন্যবাদ আমি আমার উত্তরে এর সাথে একটি উল্লেখ যুক্ত করব। আমি এটি সম্পর্কে ভুলে যেতে চাই!
জন স্কিটি

এছাড়াও, জোন, আমি বিভিন্ন জাভা 7 বন্ধের প্রস্তাবগুলি সম্পর্কে আপনার চিন্তাভাবনা সম্পর্কে পড়তে চাই আমি আপনাকে উল্লেখ করতে দেখেছি যে আপনি একটি লিখতে চেয়েছিলেন, তবে আমি এটি দেখিনি।
tjlevine

1
@ জেজলাইন: ঠিক আছে, আমি প্রতি বছর শেষ পর্যন্ত এটি লেখার চেষ্টা করার প্রতিশ্রুতি দিচ্ছি :)
জন স্কিটি

6

লুপগুলির সাথে এর কোনও সম্পর্ক নেই।

এই আচরণটি ট্রিগার করা হয়েছে কারণ আপনি ল্যাম্বডা এক্সপ্রেশনটি ব্যবহার করেন () => variable * 2যেখানে বাহ্যিক স্কুপড variableল্যাম্বদার অভ্যন্তরীণ স্কোপটিতে প্রকৃতপক্ষে সংজ্ঞায়িত হয় না।

লাম্বদা এক্সপ্রেশন (সি # 3 + তে পাশাপাশি সি # 2 তে বেনামে পদ্ধতিগুলি) এখনও প্রকৃত পদ্ধতি তৈরি করে। এই পদ্ধতিতে ভেরিয়েবলগুলি পাস করার ক্ষেত্রে কিছু দ্বিধা রয়েছে (মান দ্বারা পাস? রেফারেন্স দিয়ে পাস? সি # রেফারেন্সের সাথে যায় - তবে এটি আরও একটি সমস্যা উন্মুক্ত করে যেখানে রেফারেন্সটি আসল ভেরিয়েবলকে ছাড়িয়ে যেতে পারে)। এই সমস্ত দ্বিধা সমাধান করতে সি # যা করে তা হ'ল ল্যাম্বদা এক্সপ্রেশনগুলিতে ব্যবহৃত স্থানীয় ভেরিয়েবলগুলির সাথে সম্পর্কিত ক্ষেত্রগুলি এবং আসল ল্যাম্বদা পদ্ধতির সাথে সম্পর্কিত পদ্ধতিগুলি সহ একটি নতুন সহায়ক শ্রেণি ("ক্লোজার") তৈরি করা। variableআপনার কোডে যে কোনও পরিবর্তন আসলে এটির পরিবর্তনের জন্য অনুবাদ করা হয়ClosureClass.variable

সুতরাং আপনার সময় লুপটি ClosureClass.variable10 এ পৌঁছানো অবধি আপডেট করে রাখে , তারপরে আপনি লুপগুলির জন্য ক্রিয়াগুলি কার্যকর করেন যা সমস্ত একই কাজ করে ClosureClass.variable

আপনার প্রত্যাশিত ফলাফল পেতে আপনার লুপ ভেরিয়েবল এবং বন্ধ হওয়া ভেরিয়েবলের মধ্যে একটি পৃথকীকরণ তৈরি করতে হবে। আপনি অন্য ভেরিয়েবল প্রবর্তন করে এটি করতে পারেন, যেমন:

List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
    var t = variable; // now t will be closured (i.e. replaced by a field in the new class)
    actions.Add(() => t * 2);
    ++variable; // changing variable won't affect the closured variable t
}
foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

এই বিচ্ছেদ তৈরির জন্য আপনি বন্ধটিকে অন্য পদ্ধতিতে সরিয়ে নিতে পারেন:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    actions.Add(Mult(variable));
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

আপনি মাল্টকে ল্যাম্বডা এক্সপ্রেশন হিসাবে প্রয়োগ করতে পারেন (অন্তর্নিহিত বন্ধ)

static Func<int> Mult(int i)
{
    return () => i * 2;
}

বা প্রকৃত সহায়ক শ্রেণীর সাথে:

public class Helper
{
    public int _i;
    public Helper(int i)
    {
        _i = i;
    }
    public int Method()
    {
        return _i * 2;
    }
}

static Func<int> Mult(int i)
{
    Helper help = new Helper(i);
    return help.Method;
}

যাইহোক , "ক্লোজারগুলি" লুপ সম্পর্কিত কোনও ধারণা নয় , বরং স্থানীয় স্কোপড ভেরিয়েবলগুলির বেনামে পদ্ধতি / ল্যাম্বডা এক্সপ্রেশন হিসাবে ব্যবহার করা যায় - যদিও লুপগুলির কিছু অযৌক্তিক ব্যবহার ক্লোজার ট্র্যাপগুলি প্রদর্শন করে।


5

হ্যাঁ আপনাকে variableলুপের মধ্যে সুযোগ তৈরি করতে হবে এবং ল্যাম্বডায় সেভাবে প্রেরণ করতে হবে:

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int variable1 = variable;
    actions.Add(() => variable1 * 2);
    ++variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

Console.ReadLine();

5

একই অবস্থা মাল্টি-থ্রেডিংয়ে ঘটছে (সি # ,। নেট 4.0]।

নিম্নলিখিত কোডটি দেখুন:

উদ্দেশ্যটি 1,2,3,4,5 ক্রমানুসারে প্রিন্ট করা হবে।

for (int counter = 1; counter <= 5; counter++)
{
    new Thread (() => Console.Write (counter)).Start();
}

আউটপুট আকর্ষণীয়! (এটি 21334 এর মতো হতে পারে ...)

একমাত্র সমাধান হ'ল স্থানীয় ভেরিয়েবল ব্যবহার করা।

for (int counter = 1; counter <= 5; counter++)
{
    int localVar= counter;
    new Thread (() => Console.Write (localVar)).Start();
}

এটি আমার সাহায্য করবে বলে মনে হয় না। তবুও অবিচ্ছিন্ন।
ম্লাদেন মিহাজলভিক

0

যেহেতু এখানে কেউ সরাসরি ইসিএমএ -৩৩৪ উদ্ধৃত হয়নি :

10.4.4.10 বিবৃতি জন্য

ফর্মের একটি বিবৃতি জন্য চিরকালীন নিয়োগ পরীক্ষা করা:

for (for-initializer; for-condition; for-iterator) embedded-statement

বিবৃতি লিখিত ছিল যেমন সম্পন্ন করা হয়:

{
    for-initializer;
    while (for-condition) {
        embedded-statement;
    LLoop: for-iterator;
    }
}

অনুমানে আরও,

12.16.6.3 স্থানীয় ভেরিয়েবল ইনস্ট্যান্টেশন

যখন স্থানীয় প্রয়োগটি ভেরিয়েবলের স্কোপে প্রবেশ করে তখন তা তাত্ক্ষণিক বলে বিবেচিত হয়।

[উদাহরণস্বরূপ: উদাহরণস্বরূপ, যখন নিম্নলিখিত পদ্ধতিটি চালু করা হয়, স্থানীয় ভেরিয়েবলটি xতাত্ক্ষণিকভাবে শুরু করা হয় এবং তিনবার শুরু করা হয় - একবার লুপের প্রতিটি পুনরাবৃত্তির জন্য।

static void F() {
  for (int i = 0; i < 3; i++) {
    int x = i * 2 + 1;
    ...
  }
}

যাইহোক, xলুপের ঘোষণাপত্রের বাইরে যাওয়ার ফলে একক তাত্ক্ষণিক ফলাফল হয় x:

static void F() {
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    ...
  }
}

শেষ উদাহরণ]

যখন ধরা পড়ে না, স্থানীয় ভেরিয়েবলটি প্রায়শই ইনস্ট্যান্ট করা হয় তা পর্যবেক্ষণ করার কোনও উপায় নেই — কারণ ইনস্ট্যান্টেশনের জীবনকাল বিচ্ছিন্ন, প্রতিটি তাত্ক্ষণিকভাবে কেবল একই স্টোরেজ অবস্থানটি ব্যবহার করা সম্ভব। যাইহোক, যখন কোনও বেনামী ফাংশন স্থানীয় ভেরিয়েবল ক্যাপচার করে তখন ইনস্ট্যান্টেশনের প্রভাবগুলি সুস্পষ্ট হয়ে যায়।

[উদাহরণ: উদাহরণ

using System;

delegate void D();

class Test{
  static D[] F() {
    D[] result = new D[3];
    for (int i = 0; i < 3; i++) {
      int x = i * 2 + 1;
      result[i] = () => { Console.WriteLine(x); };
    }
  return result;
  }
  static void Main() {
    foreach (D d in F()) d();
  }
}

আউটপুট উত্পাদন করে:

1
3
5

যাইহোক, যখন ঘোষণাটি xলুপের বাইরে সরানো হয়:

static D[] F() {
  D[] result = new D[3];
  int x;
  for (int i = 0; i < 3; i++) {
    x = i * 2 + 1;
    result[i] = () => { Console.WriteLine(x); };
  }
  return result;
}

আউটপুটটি হ'ল:

5
5
5

নোট করুন যে সংকলকটি তিনটি ইনস্ট্যান্টেশনকে একক প্রতিনিধি উদাহরণে (.711.7.2) অনুকূলকরণের জন্য অনুমোদিত (তবে প্রয়োজনীয় নয়)।

যদি কোনও for-loop একটি পুনরাবৃত্তি ভেরিয়েবল ঘোষণা করে তবে সেই পরিবর্তনশীলটি নিজেই লুপের বাইরে ঘোষিত বলে মনে করা হয়। [উদাহরণ: এইভাবে, যদি পুনরাবৃত্তির পরিবর্তনশীল নিজেই ক্যাপচারের জন্য উদাহরণটি পরিবর্তন করা হয়:

static D[] F() {
  D[] result = new D[3];
  for (int i = 0; i < 3; i++) {
    result[i] = () => { Console.WriteLine(i); };
  }
  return result;
}

পুনরাবৃত্তি ভেরিয়েবলের কেবল একটি উদাহরণ ধরা পড়ে, যা আউটপুট উত্পাদন করে:

3
3
3

শেষ উদাহরণ]

হ্যাঁ, আমি অনুমান করি যে এটি উল্লেখ করা উচিত যে সি ++ এ এই সমস্যাটি দেখা দেয় না কারণ আপনি যদি ভেরিয়েবলটি মান বা রেফারেন্সের মাধ্যমে ক্যাপচার করা হয় তবে আপনি চয়ন করতে পারেন (দেখুন: ল্যাম্বদা ক্যাপচার )।


-1

একে ক্লোজার সমস্যা বলা হয়, কেবল একটি অনুলিপি ব্যবহার করুন এবং এটি হয়ে যায়।

List<Func<int>> actions = new List<Func<int>>();

int variable = 0;
while (variable < 5)
{
    int i = variable;
    actions.Add(() => i * 2);
    ++ variable;
}

foreach (var act in actions)
{
    Console.WriteLine(act.Invoke());
}

4
আপনার উত্তরটি উপরের কারও দ্বারা সরবরাহ করা উত্তর থেকে কীভাবে আলাদা?
থানগাদুরাই
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.