C # में एक लूप में कैप्टिव वेरिएबल


216

मुझे C # के बारे में एक दिलचस्प मुद्दा मिला। मेरे पास नीचे जैसा कोड है।

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 से आउटपुट की उम्मीद करता हूं। हालांकि, यह वास्तव में पांच 10s आउटपुट करता है।

ऐसा लगता है कि यह एक कैप्चर किए गए चर का संदर्भ देने वाली सभी क्रियाओं के कारण है। परिणामस्वरूप, जब वे आह्वान करते हैं, तो उन सभी का उत्पादन समान होता है।

क्या इस सीमा को पूरा करने का एक तरीका है कि प्रत्येक क्रिया के पास अपने स्वयं के कैद किए गए चर हों?


15
इस विषय पर एरिक लिपर्ट की ब्लॉग श्रृंखला को भी देखें: लूप वेरिएबल पर विचार करना हानिकारक माना जाता है
ब्रायन

10
साथ ही, वे C # 5 को बदलने के लिए काम कर रहे हैं जैसा कि आप एक फॉर्च्यूनर में उम्मीद करते हैं। (ब्रेकिंग चेंज)
नील टिबरेला


3
@ नील: हालांकि यह उदाहरण अभी भी C # 5 में ठीक से काम नहीं करता है, क्योंकि यह अभी भी पांच 10s का उत्पादन करता है
इयान ओक्स

6
यह सत्यापित करता है कि यह C # 6.0 (VS 2015) पर आज तक पांच 10s का आउटपुट देता है। मुझे संदेह है कि क्लोजर चर का यह व्यवहार परिवर्तन का एक उम्मीदवार है। Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured
RBT

जवाबों:


196

हां - लूप के अंदर चर की एक प्रति लें:

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

आप इसे ऐसे समझ सकते हैं जैसे कि C # कंपाइलर हर बार "नया" लोकल वैरिएबल बनाता है जब भी वह वेरिएबल डिक्लेरेशन हिट करता है। वास्तव में यह उपयुक्त नई बंद वस्तुओं का निर्माण करेगा, और यदि आप कई स्कोप में चर का संदर्भ देते हैं, तो यह जटिल (कार्यान्वयन के संदर्भ में) हो जाता है, लेकिन यह काम करता है :)

ध्यान दें कि इस समस्या का अधिक सामान्य घटना उपयोग 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 देखें, और क्लोजर पर मेरे लेख के और भी उदाहरण हैं।

ध्यान दें कि C # 5 संकलक और उससे परे (यहां तक ​​कि C # के पुराने संस्करण को निर्दिष्ट करते समय), foreachबदले हुए व्यवहार को अब आपको स्थानीय प्रतिलिपि बनाने की आवश्यकता नहीं है। देखें इस उत्तर अधिक जानकारी के लिए।


32
जॉन की पुस्तक भी इस पर एक बहुत अच्छा अध्याय (!, विनम्र किया जा रहा रोक जॉन) है
मार्क Gravell

35
यह बेहतर लगता है अगर मैं अन्य लोगों को इसे प्लग करने देता हूं;) (मैं स्वीकार करता हूं कि मैं हालांकि इसकी सिफारिश करने वाले उत्तरों को वोट देने की प्रवृत्ति रखता हूं।)
जॉन स्कीट

2
हमेशा की तरह, skeet@pobox.com पर प्रतिक्रिया की सराहना की जाएगी :)
जॉन स्कीट

7
C # 5.0 के लिए व्यवहार भिन्न है (अधिक उचित) जॉन स्कीट द्वारा नया उत्तर देखें - stackoverflow.com/questions/16264289/…
अलेक्सई लेवेनकोव

1
@ फ़्लोरिमॉन्ड: यह सिर्फ सी # में काम करने के तरीके को बंद नहीं करता है। वे चर को पकड़ते हैं , मूल्यों को नहीं । (यह सच है कि लूप की परवाह किए बिना, और आसानी से एक लैम्ब्डा के साथ प्रदर्शित किया जाता है जो एक चर को कैप्चर करता है, और जब भी इसे निष्पादित किया जाता है, तो बस वर्तमान मूल्य प्रिंट करता है।)
जॉन स्कीट

23

मेरा मानना ​​है कि आप जो अनुभव कर रहे हैं वह क्लोजर http://en.wikipedia.org/wiki/Closure_(computer_science) के नाम से जाना जाता है । आपके लांबा में एक चर का संदर्भ है जो फ़ंक्शन के बाहर ही स्कूप किया गया है। आपके लांबा की व्याख्या तब तक नहीं की जाती है जब तक आप इसे लागू नहीं करते हैं और एक बार जब यह होता है तो निष्पादन के समय चर का मूल्य मिल जाएगा।


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;
}

मेरे संपादित उत्तर में स्पष्टीकरण देखें। मैं अब प्रासंगिक सा की खोज कर रहा हूँ।
जॉन स्कीट

Haha jon, मैं वास्तव में सिर्फ आपका लेख पढ़ता हूं : csharpindepth.com/Articles/Chapter5/Closures.aspx आप मेरे दोस्त के अच्छे काम करते हैं।
tjlevine

@tjlevine: बहुत बहुत धन्यवाद। मैं अपने उत्तर में इसका एक संदर्भ जोड़ूंगा। मैं इसके बारे में भूल गया था!
जॉन स्कीट

इसके अलावा, जॉन, मुझे विभिन्न जावा 7 क्लोजर प्रस्तावों पर आपके विचारों के बारे में पढ़ना अच्छा लगेगा। मैंने आपको उल्लेख किया है कि आप एक लिखना चाहते थे, लेकिन मैंने इसे नहीं देखा है।
tjlevine

1
@tjlevine: ठीक है, मैं इसे वर्ष के अंत तक लिखने का प्रयास करने का वादा करता हूं :)
जॉन स्कीट

6

इससे छोरों का कोई लेना-देना नहीं है।

इस व्यवहार को ट्रिगर किया जाता है क्योंकि आप एक लैम्ब्डा अभिव्यक्ति का उपयोग करते हैं () => variable * 2जहां बाहरी स्कॉप्ड variableवास्तव में लैम्बडा के आंतरिक दायरे में परिभाषित नहीं किया गया है।

लैम्ब्डा एक्सप्रेशन (C # 3 + में, साथ ही C # 2 में अनाम तरीके) अभी भी वास्तविक तरीके बनाते हैं। इन विधियों के चर को पास करने में कुछ दुविधाएँ होती हैं (मान से गुज़रना; संदर्भ से गुज़रना? C # संदर्भ के साथ जाता है - लेकिन इससे एक और समस्या खुलती है जहाँ संदर्भ वास्तविक चर को रेखांकित कर सकता है)। इन सभी दुविधाओं को हल करने के लिए C # क्या करता है एक नया सहायक वर्ग ("क्लोजर") बनाना है जिसमें लैम्ब्डा एक्सप्रेशंस में उपयोग किए जाने वाले स्थानीय वैरिएबल्स और वास्तविक लैम्ब्डा विधियों के अनुरूप तरीके हैं। 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

मल्टी-थ्रेडिंग (C #, .NET 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

चूंकि यहाँ किसी ने सीधे ECMA-334 का हवाला नहीं दिया था :

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

ध्यान दें कि कंपाइलर को एक ही प्रतिनिधि उदाहरण ()11.7.2) में तीन तात्कालिकता का अनुकूलन करने की अनुमति है (लेकिन आवश्यक नहीं)।

यदि एक फॉर-लूप एक पुनरावृत्ति चर घोषित करता है, तो उस चर को लूप के बाहर घोषित किया जाता है। [उदाहरण: इस प्रकार, यदि उदाहरण को परिवर्तनशील चर पर कब्जा करने के लिए बदल दिया जाता है:

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

अंत उदाहरण]

अरे हाँ, मुझे लगता है कि यह उल्लेख किया जाना चाहिए कि C ++ में यह समस्या इसलिए नहीं है क्योंकि आप चुन सकते हैं कि चर मान द्वारा या संदर्भ द्वारा कैप्चर किया गया है (देखें: लैम्ब्डा कैप्चर )।


-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
किस तरह से आपका उत्तर उपरोक्त किसी व्यक्ति द्वारा दिए गए उत्तर से अलग है?
थंगादुरई
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.