यह स्ट्रिंग एक्सटेंशन विधि अपवाद क्यों नहीं फेंकती है?


119

मुझे एक C # स्ट्रिंग एक्सटेंशन विधि मिली है जिसे वापस करना चाहिए IEnumerable<int> जो स्ट्रिंग के भीतर सबरिंग के सभी इंडेक्स । यह अपने इच्छित उद्देश्य के लिए पूरी तरह से काम करता है और अपेक्षित परिणाम लौटाए जाते हैं (जैसा कि मेरे परीक्षणों में से एक द्वारा सिद्ध किया गया है, हालांकि नीचे वाला नहीं है), लेकिन एक अन्य इकाई परीक्षण ने इसके साथ एक समस्या की खोज की है: यह अशक्त तर्क को संभाल नहीं सकता है।

यहाँ विस्तार विधि मैं परीक्षण कर रहा हूँ:

public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
    if (searchText == null)
    {
        throw new ArgumentNullException("searchText");
    }
    for (int index = 0; ; index += searchText.Length)
    {
        index = str.IndexOf(searchText, index);
        if (index == -1)
            break;
        yield return index;
    }
}

यहाँ परीक्षण है कि समस्या को ध्वजांकित किया गया है:

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
    string test = "a.b.c.d.e";
    test.AllIndexesOf(null);
}

जब परीक्षण मेरी एक्सटेंशन विधि के विरुद्ध चलता है, तो यह विफल हो जाता है, मानक त्रुटि संदेश के साथ कि विधि "अपवाद नहीं फेंकती"।

यह भ्रामक है: मैं स्पष्ट रूप nullसे फ़ंक्शन में पास हो गया हूं , फिर भी किसी कारण से तुलना null == nullवापस आ रही हैfalse । इसलिए, कोई अपवाद नहीं फेंका गया है और कोड जारी है।

मैंने पुष्टि की है कि यह परीक्षण के साथ बग नहीं है: जब मेरे मुख्य प्रोजेक्ट में विधि Console.WriteLineको नल-तुलना ifब्लॉक में कॉल के साथ चलाया जाता है, तो कंसोल पर कुछ भी नहीं दिखाया जाता है और किसी भी catchब्लॉक को जोड़ने पर कोई अपवाद नहीं पकड़ा जाता है । इसके अलावा, का उपयोग string.IsNullOrEmptyकरने के बजाय == nullएक ही समस्या है।

यह माना-सरल तुलना क्यों विफल हो जाती है?


5
क्या आपने कोड के माध्यम से कदम रखने की कोशिश की है? शायद यह बहुत जल्दी हल हो जाएगा।
मैथ्यू ह्यूजेन

1
क्या होता है? (क्या यह एक अपवाद फेंकता है? यदि हां, तो कौन सी और कौन सी पंक्ति?)
user2864740

@ user2864740 मैंने जो कुछ भी हुआ उसका वर्णन किया है। कोई अपवाद नहीं, बस एक असफल परीक्षा और एक रन विधि।
आर्टऑफकोड

7
Iterators को तब तक निष्पादित नहीं किया जाता है जब तक वे पुनरावृत्त नहीं होते हैं
BlueRaja - Danny Pflughoeft

2
आपका स्वागत है। इसने भी जॉन की "सबसे खराब गेटा" सूची बनाई: स्टैकओवरफ्लो . com/a/241180/ 88656 । यह काफी सामान्य समस्या है।
एरिक लिपर्ट

जवाबों:


158

आप उपयोग कर रहे हैं yield return । ऐसा करते समय, कंपाइलर आपके कार्य को एक फ़ंक्शन में फिर से लिखेगा जो एक उत्पन्न वर्ग को लौटाता है जो एक राज्य मशीन को लागू करता है।

मोटे तौर पर, यह स्थानीय लोगों को उस वर्ग के क्षेत्रों में फिर से लिखता है और yield returnनिर्देशों के बीच आपके एल्गोरिथ्म का प्रत्येक हिस्सा एक राज्य बन जाता है। आप एक डिकम्पॉइलर के साथ जांच कर सकते हैं कि संकलन के बाद यह विधि क्या हो जाती है (स्मार्ट डीकॉम्पिलेशन को बंद करना सुनिश्चित करें जो उत्पादन करेगाyield return )।

लेकिन लब्बोलुआब यह है: आपके तरीके का कोड तब तक निष्पादित नहीं किया जाएगा जब तक आप पुनरावृत्ति शुरू नहीं करते।

पूर्व शर्त के लिए जाँच करने का सामान्य तरीका दो में अपनी विधि को विभाजित करना है:

public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
    if (str == null)
        throw new ArgumentNullException("str");
    if (searchText == null)
        throw new ArgumentNullException("searchText");

    return AllIndexesOfCore(str, searchText);
}

private static IEnumerable<int> AllIndexesOfCore(string str, string searchText)
{
    for (int index = 0; ; index += searchText.Length)
    {
        index = str.IndexOf(searchText, index);
        if (index == -1)
            break;
        yield return index;
    }
}

यह काम करता है क्योंकि पहला तरीका आपकी अपेक्षा (तत्काल निष्पादन) की तरह व्यवहार करेगा, और दूसरी विधि द्वारा कार्यान्वित राज्य मशीन को वापस कर देगा।

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


यदि आप इस बात को लेकर उत्सुक हैं कि कंपाइलर आपके कोड का क्या करता है, तो यहां पर आपकी विधि, कंपाइलर-जनरेटेड कोड विकल्प का उपयोग करके dotPeek के साथ विघटित हो गई है।

public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
  Test.<AllIndexesOf>d__0 allIndexesOfD0 = new Test.<AllIndexesOf>d__0(-2);
  allIndexesOfD0.<>3__str = str;
  allIndexesOfD0.<>3__searchText = searchText;
  return (IEnumerable<int>) allIndexesOfD0;
}

[CompilerGenerated]
private sealed class <AllIndexesOf>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
  private int <>2__current;
  private int <>1__state;
  private int <>l__initialThreadId;
  public string str;
  public string <>3__str;
  public string searchText;
  public string <>3__searchText;
  public int <index>5__1;

  int IEnumerator<int>.Current
  {
    [DebuggerHidden] get
    {
      return this.<>2__current;
    }
  }

  object IEnumerator.Current
  {
    [DebuggerHidden] get
    {
      return (object) this.<>2__current;
    }
  }

  [DebuggerHidden]
  public <AllIndexesOf>d__0(int <>1__state)
  {
    base..ctor();
    this.<>1__state = param0;
    this.<>l__initialThreadId = Environment.CurrentManagedThreadId;
  }

  [DebuggerHidden]
  IEnumerator<int> IEnumerable<int>.GetEnumerator()
  {
    Test.<AllIndexesOf>d__0 allIndexesOfD0;
    if (Environment.CurrentManagedThreadId == this.<>l__initialThreadId && this.<>1__state == -2)
    {
      this.<>1__state = 0;
      allIndexesOfD0 = this;
    }
    else
      allIndexesOfD0 = new Test.<AllIndexesOf>d__0(0);
    allIndexesOfD0.str = this.<>3__str;
    allIndexesOfD0.searchText = this.<>3__searchText;
    return (IEnumerator<int>) allIndexesOfD0;
  }

  [DebuggerHidden]
  IEnumerator IEnumerable.GetEnumerator()
  {
    return (IEnumerator) this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
  }

  bool IEnumerator.MoveNext()
  {
    switch (this.<>1__state)
    {
      case 0:
        this.<>1__state = -1;
        if (this.searchText == null)
          throw new ArgumentNullException("searchText");
        this.<index>5__1 = 0;
        break;
      case 1:
        this.<>1__state = -1;
        this.<index>5__1 += this.searchText.Length;
        break;
      default:
        return false;
    }
    this.<index>5__1 = this.str.IndexOf(this.searchText, this.<index>5__1);
    if (this.<index>5__1 != -1)
    {
      this.<>2__current = this.<index>5__1;
      this.<>1__state = 1;
      return true;
    }
    goto default;
  }

  [DebuggerHidden]
  void IEnumerator.Reset()
  {
    throw new NotSupportedException();
  }

  void IDisposable.Dispose()
  {
  }
}

यह अमान्य सी # कोड है, क्योंकि कंपाइलर को उन चीजों को करने की अनुमति है जो भाषा अनुमति नहीं देती है, लेकिन जो आईएल में कानूनी हैं - उदाहरण के लिए चर का नामकरण एक तरह से आप नाम टकराव से बचने के लिए नहीं कर सकते।

लेकिन जैसा कि आप देख सकते हैं, AllIndexesOfएकमात्र निर्माण करता है और एक वस्तु लौटाता है, जिसका निर्माता केवल कुछ राज्य को इनिशियलाइज़ करता है। GetEnumeratorकेवल वस्तु की प्रतिलिपि बनाता है। असली काम तब किया जाता है जब आप गणना ( MoveNextविधि को बुलाकर ) शुरू करते हैं।


9
BTW, मैंने निम्नलिखित महत्वपूर्ण बिंदु को उत्तर में जोड़ा: ध्यान दें कि आपको strपैरामीटर के लिए भी जांच करनी चाहिए null, क्योंकि एक्सटेंशन विधियों को nullमूल्यों पर कहा जा सकता है , क्योंकि वे सिर्फ वाक्यगत चीनी हैं।
लुकास ट्रेज़न्यूस्की

2
yield returnसिद्धांत रूप में एक अच्छा विचार है, लेकिन इसमें बहुत सारे अजीबोगरीब विश्वासघात हैं। इस एक को प्रकाश में लाने के लिए धन्यवाद!
नैटविरिन

तो, मूल रूप से एक त्रुटि फेंक दी जाएगी यदि एनुमर चलाया गया था, जैसे कि एक फ़ॉरचैन में?
एमवीसीडीएस

1
@MVCDS बिल्कुल। निर्माण MoveNextद्वारा हुड के नीचे कहा जाता है foreach। मैं क्या का एक विवरण लिखा foreachमें करता है मेरा उत्तर संग्रह अर्थ विज्ञान समझा अगर आपको सटीक पैटर्न देखना चाहते हैं।
लुकास ट्रेजेनिव्स्की

34

आपके पास एक इटरेटर ब्लॉक है। उस पद्धति का कोई भी कोड कभी भी MoveNextलौटे हुए पुनरावृत्ति पर कॉल के बाहर नहीं चलाया जाता है। कॉलिंग विधि नोटिंग नहीं है, लेकिन राज्य मशीन बनाएं, और वह कभी भी विफल नहीं होगी (चरम सीमा के बाहर जैसे मेमोरी त्रुटियां, स्टैक ओवरफ्लो, या थ्रेड एबॉर्ट अपवाद)।

जब आप वास्तव में अनुक्रम को पुनरावृत्त करने का प्रयास करेंगे तो आपको अपवाद मिलेगा।

यही कारण है कि LINQ विधियों को वास्तव में दो तरीकों की आवश्यकता होती है, जिसमें त्रुटि शब्दार्थ को संभालने की उनकी इच्छा होती है। उनके पास एक निजी विधि है जो एक इटरेटर ब्लॉक है, और फिर एक नॉन-इटरेटर ब्लॉक विधि है जो तर्क सत्यापन (कुछ भी नहीं करता है, ताकि यह सभी अन्य कार्यक्षमता को समाप्त करते हुए भी इसे स्थगित किए जाने के बजाय उत्सुकता से किया जा सके)।

तो यह सामान्य पैटर्न है:

public static IEnumerable<T> Foo<T>(
    this IEnumerable<T> souce, Func<T, bool> anotherArgument)
{
    //note, not an iterator block
    if(anotherArgument == null)
    {
        //TODO make a fuss
    }
    return FooImpl(source, anotherArgument);
}

private static IEnumerable<T> FooImpl<T>(
    IEnumerable<T> souce, Func<T, bool> anotherArgument)
{
    //TODO actual implementation as an iterator block
    yield break;
}

0

Enumerators, जैसा कि दूसरों ने कहा है, मूल्यांकन नहीं किया जाता है जब तक वे enumerated (यानी IEnumerable.GetNextविधि कहा जाता है) हो रही है। इस प्रकार यह

List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>();

जब तक आप गणना शुरू नहीं करते हैं, तब तक इसका मूल्यांकन नहीं किया जाता है

foreach(int index in indexes)
{
    // ArgumentNullException
}
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.