वहाँ एक foreach में चर के पुन: उपयोग के लिए एक कारण है?


1684

C # में लैम्ब्डा एक्सप्रेशन या अनाम विधियों का उपयोग करते समय, हमें संशोधित क्लोजर पिटफुल तक पहुंच से सावधान रहना होगा । उदाहरण के लिए:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

संशोधित क्लोजर के कारण, उपरोक्त कोड Whereक्वेरी के सभी क्लॉज़ के अंतिम मूल्य के आधार पर कारण होगा s

जैसा कि यहां बताया गया है , ऐसा इसलिए होता है क्योंकि ऊपर दिए sगए foreachलूप में घोषित चर का अनुवाद इस तरह किया जाता है:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

इस तरह के बजाय:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

जैसा कि यहां बताया गया है , लूप के बाहर एक वैरिएबल घोषित करने के लिए कोई प्रदर्शन लाभ नहीं हैं, और सामान्य परिस्थितियों में केवल यही कारण है कि मैं ऐसा करने के लिए सोच सकता हूं यदि आप लूप के दायरे से बाहर चर का उपयोग करने की योजना बनाते हैं:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

हालाँकि foreachलूप में परिभाषित चर का उपयोग लूप के बाहर नहीं किया जा सकता है:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

इसलिए संकलक चर को इस तरह से घोषित करता है, जिससे यह एक त्रुटि के लिए अत्यधिक प्रवण होता है, जो कि बिना किसी लाभकारी लाभ के उत्पादन के लिए अक्सर ढूंढना और डिबग करना मुश्किल होता है।

क्या कोई ऐसी चीज है जिसे आप foreachलूप्स के साथ कर सकते हैं जो कि आप नहीं कर सकते अगर उन्हें एक इनर-स्कोप्ड वैरिएबल के साथ संकलित किया गया था, या यह सिर्फ एक अनियंत्रित विकल्प है जो गुमनाम तरीकों और लैम्ब्डा एक्सप्रेशन उपलब्ध होने या सामान्य होने से पहले बनाया गया था, और जो hasn तब से संशोधित नहीं किया गया है?


4
इसमें गलत क्या है String s; foreach (s in strings) { ... }?
ब्रैड क्रिस्टी

5
@ ब्रैडक्रिस्टी ओपी वास्तव में बात नहीं कर रहा है, foreachलेकिन ओपी द्वारा दिखाए गए समान कोड के परिणामस्वरूप लैम्डा अभिव्यक्तियों के बारे में ...
याहिया

22
@ ब्रैडक्रिस्टी: क्या यह संकलन है? ( त्रुटि: टाइप और आइडेंटिफायर दोनों ही मेरे लिए फॉरचेट स्टेटमेंट में आवश्यक हैं )
ऑस्टिन सलोनन

32
@ जाकोबॉट्सचाइनलसेन: यह एक लंबोदर का बाहरी इलाका है; आप यह क्यों मान रहे हैं कि यह स्टैक पर होने वाला है? यह जीवनकाल स्टैक फ्रेम से अधिक लंबा है !
एरिक लिपर्ट

3
@EricLippert: मैं उलझन में हूँ। मैं समझता हूं कि लैम्ब्डा फॉरेक्स वैरिएबल (जो आंतरिक रूप से लूप के बाहर घोषित होता है ) के संदर्भ को कैप्चर करता है और इसलिए आप इसके अंतिम मूल्य के खिलाफ तुलना करते हैं; मुझे लगता है कि मुझे समझ में नहीं आता है कि लूप के अंदर वेरिएबल को कैसे घोषित किया जाए, इससे कोई फर्क नहीं पड़ेगा। एक संकलक-लेखक के दृष्टिकोण से, मैं केवल इस बात पर ध्यान दिए बिना कि क्या घोषणा लूप के अंदर या बाहर है, स्टैक पर एक स्ट्रिंग संदर्भ (var 's') आवंटित कर रहा हूँ; मैं निश्चित रूप से हर पुनरावृत्ति के ढेर पर एक नया संदर्भ धक्का नहीं करना चाहता!
एंथनी

जवाबों:


1407

संकलक चर को इस तरह से घोषित करता है जो इसे एक त्रुटि के लिए अत्यधिक प्रवण बनाता है जो कि अक्सर बिना किसी लाभकारी लाभ के उत्पादन और डिबग करना मुश्किल होता है।

आपकी आलोचना पूरी तरह से उचित है।

मैं इस समस्या पर यहां विस्तार से चर्चा करता हूं:

लूप चर पर बंद हानिकारक माना जाता है

वहाँ कुछ है कि आप इस तरह से foreach छोरों के साथ कर सकते हैं कि आप नहीं कर सकते हैं अगर वे एक आंतरिक स्कोप चर के साथ संकलित किया गया था? या यह सिर्फ एक मनमाना विकल्प है जो गुमनाम तरीकों से पहले किया गया था और लैम्ब्डा भाव उपलब्ध थे या आम थे, और जो तब से संशोधित नहीं किया गया है?

बाद वाला। C # 1.0 विनिर्देशन ने वास्तव में यह नहीं कहा कि लूप वैरिएबल लूप बॉडी के अंदर या बाहर था, क्योंकि इसमें कोई अवलोकनीय अंतर नहीं था। जब क्लोजर शब्दार्थ को C # 2.0 में पेश किया गया था, तो लूप के बाहर लूप वेरिएबल को चुनने के लिए विकल्प बनाया गया था, जो "लूप" के अनुरूप था।

मुझे लगता है कि यह कहना उचित है कि सभी को उस निर्णय पर पछतावा है। यह सी # में सबसे खराब "गोचैस" में से एक है, और हम इसे ठीक करने के लिए ब्रेकिंग परिवर्तन लेने जा रहे हैं। C # 5 में foreach लूप वेरिएबल तार्किक रूप से लूप के शरीर के अंदर होगा , और इसलिए क्लोजर को हर बार एक नई कॉपी मिलेगी।

forपाश बदला नहीं जाएगा, और परिवर्तन नहीं किया जाएगा सी # के पिछले संस्करणों के लिए "वापस भेजा"। इसलिए आपको इस मुहावरे का उपयोग करते समय सावधानी बरतनी चाहिए।


177
हमने वास्तव में C # 3 और C # 4 में इस बदलाव को पीछे धकेल दिया था। जब हमने C # 3 को डिज़ाइन किया था तब हमें पता चला था कि समस्या (जो पहले से ही C # 2 में मौजूद थी) खराब होने वाली थी क्योंकि इतने सारे लैम्ब्डा (और क्वेरी) होंगे समझ में आता है, जो प्रच्छन्नता में भेड़ के बच्चे हैं) LINQ के लिए धन्यवाद। मुझे खेद है कि हमने इस समस्या को इतनी देर से ठीक करने के लिए पर्याप्त रूप से खराब होने का इंतजार किया, बजाय इसे सी # 3 में ठीक करने के। 3.
एरिक लिपर्ट

75
और अब हमें याद रखना होगा कि foreach'सुरक्षित' है, लेकिन forनहीं है।
लेपी

22
@ मचिअलवू: बदलाव इस मायने में टूट रहा है कि यह पिछड़ा संगत नहीं है। पुराने कंपाइलर का उपयोग करने पर नया कोड सही तरीके से नहीं चलेगा।
लेप्पी

41
@ बैंजोल: नहीं, यही वजह है कि हम इसे लेने के लिए तैयार हैं। जॉन स्कीट ने मुझे एक महत्वपूर्ण ब्रेकिंग परिवर्तन परिदृश्य की ओर इशारा किया, जो यह है कि कोई व्यक्ति C # 5 में कोड लिखता है, उसका परीक्षण करता है, और फिर इसे उन लोगों के साथ साझा करता है जो अभी भी C # 4 का उपयोग कर रहे हैं, जो तब भोलेपन से इसे सही मानते हैं। उम्मीद है कि ऐसे परिदृश्य से प्रभावित लोगों की संख्या कम है।
एरिक लिपर्ट

29
एक तरफ के रूप में, ReSharper ने हमेशा इसे पकड़ा है और इसे "संशोधित क्लोजर तक पहुंच" के रूप में रिपोर्ट करता है। फिर, Alt + Enter दबाकर, क्या यह स्वचालित रूप से आपके लिए अपना कोड ठीक कर देगा। जेटब्राइन्स.com
माइक चैंबरलेन

191

आप जो पूछ रहे हैं, वह एरिक लिपर्ट द्वारा अपने ब्लॉग पोस्ट में पूरी तरह से कवर किया गया है, जिसे लूप वेरिएबल पर बंद करना हानिकारक माना जाता है और इसकी अगली कड़ी।

मेरे लिए, सबसे ठोस तर्क यह है कि प्रत्येक पुनरावृत्ति में नया चर होना for(;;)स्टाइल लूप के साथ असंगत होगा । क्या आप int iप्रत्येक पुनरावृत्ति में एक नया होने की उम्मीद करेंगे for (int i = 0; i < 10; i++)?

इस व्यवहार के साथ सबसे आम समस्या पुनरावृत्ति चर पर एक बंद कर रहा है और यह एक आसान समाधान है:

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

मेरा ब्लॉग इस मुद्दे के बारे में पोस्ट करता है: C # में फ़ॉरवर्ड चर पर बंद होना


18
अंत में, लोग वास्तव में क्या चाहते हैं जब वे लिखते हैं कि इसमें कई चर नहीं हैं, यह मूल्य से अधिक है । और सामान्य मामले में इसके लिए एक उपयोगी वाक्यविन्यास के बारे में सोचना मुश्किल है।
रैंडम 832

1
हां, मूल्य द्वारा बंद करना संभव नहीं है, हालांकि एक बहुत आसान समाधान है जिसे मैंने सिर्फ शामिल करने के लिए अपना उत्तर संपादित किया है।
क्रियाज

6
यह C # बंद संदर्भों में बहुत बुरा है। यदि वे डिफ़ॉल्ट रूप से मानों को बंद कर देते हैं, तो हम आसानी से इसके बजाय चर पर समापन को निर्दिष्ट कर सकते हैं ref
सीन यू

2
@ क्रिज़, यह एक ऐसा मामला है, जिसमें असंगतता असंगत होने की तुलना में अधिक हानिकारक है। लोगों की अपेक्षा के अनुसार इसे "बस काम" करना चाहिए, और स्पष्ट रूप से लोग कुछ अलग होने की उम्मीद करते हैं जब फॉरच का उपयोग करते हुए लूप के विपरीत होता है, उन लोगों की संख्या को देखते हुए, जो समस्याओं को हिट कर चुके हैं, इससे पहले कि हम संशोधित क्लोजर इश्यू की पहुंच के बारे में जानते थे (जैसे कि खुद) ।
एंडी १

2
@ Random832 को C # के बारे में पता नहीं है, लेकिन आम LISP में इसके लिए एक सिंटैक्स है, और यह आंकड़े देता है कि किसी भी भाषा में परिवर्तनशील चर और क्लोजर (nay, must ) के पास भी होगा। हम या तो बदलते स्थान के संदर्भ में बंद हो जाते हैं, या एक निश्चित समय पर इसका मूल्य होता है (एक समापन का निर्माण)। यह पायथन और स्कीम ( cutरेफ्स / var के लिए और आंशिक रूप से मूल्यांकन किए गए क्लोजर में मूल्यांकन किए गए मानों को cuteरखने के लिए) में समान सामान पर चर्चा करता है ।
विल नेस

103

इसे काटे जाने के बाद, मुझे अंतरतम रूप से स्थानीय रूप से परिभाषित चरों को शामिल करने की आदत है, जिसका उपयोग मैं किसी भी करीबी को स्थानांतरित करने के लिए करता हूं। आपके उदाहरण में:

foreach (var s in strings)
    query = query.Where(i => i.Prop == s); // access to modified closure

मैं करता हूँ:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}        

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


24
यह योगदान के लिए विशिष्ट समाधान है धन्यवाद। इस पैटर्न को पहचानने और इसे अपने ध्यान में लाने के लिए रेस्परर काफी स्मार्ट है, जो अच्छा है। मैं थोड़ी देर में इस पैटर्न से नहीं मिला, लेकिन चूंकि यह एरिक लिपर्ट के शब्दों में है, "सबसे आम गलत बग रिपोर्ट हमें मिलती है," मैं यह जानने के लिए उत्सुक था कि इससे बचने के तरीके से अधिक क्यों
स्ट्रिपिंगवर्यर

62

C # 5.0 में, यह समस्या ठीक हो गई है और आप लूप वेरिएबल्स को बंद कर सकते हैं और आपको अपेक्षित परिणाम मिल सकते हैं।

भाषा विनिर्देश कहता है:

8.8.4 फॉर्च्यूमेंट स्टेटमेंट

(...)

प्रपत्र का एक फ़ार्च स्टेटमेंट

foreach (V v in x) embedded-statement

उसके बाद इसका विस्तार किया जाता है:

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
       // Dispose e
  }
}

(...)

इन- vलूप के अंदर का प्लेसमेंट इस बात के लिए महत्वपूर्ण है कि एम्बेडेड-स्टेटमेंट में होने वाले किसी भी अनाम फ़ंक्शन द्वारा इसे कैसे कैप्चर किया जाता है। उदाहरण के लिए:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

यदि vलूप के बाहर घोषित किया गया था, तो इसे सभी पुनरावृत्तियों के बीच साझा किया जाएगा, और लूप के लिए इसका मान अंतिम मान होगा 13, जो कि fप्रिंट का आह्वान है । इसके बजाय, क्योंकि प्रत्येक पुनरावृत्ति का अपना चर होता है v, fपहले पुनरावृति में कैप्चर किया गया मान जारी रहेगा 7, जो कि मुद्रित होगा। ( नोट: पिछले vलूप के बाहर के # घोषित किए गए संस्करण । )


1
सी # लूप के इस प्रारंभिक संस्करण को लूप के अंदर क्यों घोषित किया गया? msdn.microsoft.com/en-GB/library/aa664754.aspx
colinfang

4
@colinfang एरिक के जवाब को पढ़ना सुनिश्चित करें : सी # 1.0 विनिर्देश ( आपके लिंक में हम वीएस 2003, यानी सी # 1.2 के बारे में बात कर रहे हैं ) वास्तव में यह नहीं कहा था कि लूप वेरिएबल लूप बॉडी के अंदर या बाहर था, क्योंकि यह कोई अवलोकन अंतर नहीं करता है । जब क्लोजर शब्दार्थ को C # 2.0 में पेश किया गया था, तो लूप के बाहर लूप वेरिएबल को चुनने के लिए विकल्प बनाया गया था, जो "लूप" के अनुरूप था।
पाओलो मोरेटी

1
तो आप कह रहे हैं कि लिंक में उदाहरण उस समय निश्चित नहीं थे?
कॉलिनफैंग

4
@colinfang वे निश्चित विनिर्देश थे। समस्या यह है कि हम एक विशेषता (यानी फ़ंक्शन क्लोजर) के बारे में बात कर रहे हैं जिसे बाद में (सी # 2.0 के साथ) पेश किया गया था। जब C # 2.0 आया तो उन्होंने लूप वेरिएबल को लूप के बाहर रखने का फैसला किया। और जब उन्होंने C # 5.0 :)
पाओलो मोरेटी
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.