c# - Why does this string extension method not throw an exception? -


i've got c# string extension method should return ienumerable<int> of indexes of substring within string. works intended purpose , expected results returned (as proven 1 of tests, although not 1 below), unit test has discovered problem it: can't handle null arguments.

here's extension method i'm testing:

public static ienumerable<int> allindexesof(this string str, string searchtext) {     if (searchtext == null)     {         throw new argumentnullexception("searchtext");     }     (int index = 0; ; index += searchtext.length)     {         index = str.indexof(searchtext, index);         if (index == -1)             break;         yield return index;     } } 

here test flagged problem:

[testmethod] [expectedexception(typeof(argumentnullexception))] public void extensions_allindexesof_handlesnullarguments() {     string test = "a.b.c.d.e";     test.allindexesof(null); } 

when test runs against extension method, fails, standard error message method "did not throw exception".

this confusing: have passed null function, yet reason comparison null == null returning false. therefore, no exception thrown , code continues.

i have confirmed not bug test: when running method in main project call console.writeline in null-comparison if block, nothing shown on console , no exception caught catch block add. furthermore, using string.isnullorempty instead of == null has same problem.

why supposedly-simple comparison fail?

you using yield return. when doing so, compiler rewrite method function returns generated class implements state machine.

broadly speaking, rewrites locals fields of class , each part of algorithm between yield return instructions becomes state. can check decompiler method becomes after compilation (make sure turn off smart decompilation produce yield return).

but bottom line is: the code of method won't executed until start iterating.

the usual way check preconditions split method in two:

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) {     (int index = 0; ; index += searchtext.length)     {         index = str.indexof(searchtext, index);         if (index == -1)             break;         yield return index;     } } 

this works because first method behave expect (immediate execution), , return state machine implemented second method.

note should check str parameter null, because extensions methods can called on null values, they're syntactic sugar.


if you're curious compiler code, here's method, decompiled dotpeek using show compiler-generated code option.

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]     {       return this.<>2__current;     }   }    object ienumerator.current   {     [debuggerhidden]     {       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()   {   } } 

this invalid c# code, because compiler allowed things language doesn't allow, legal in il - instance naming variables in way couldn't avoid name collisions.

but can see, allindexesof constructs , returns object, constructor initializes state. getenumerator copies object. real work done when start enumerating (by calling movenext method).


Comments

Popular posts from this blog

android - MPAndroidChart - How to add Annotations or images to the chart -

javascript - Add class to another page attribute using URL id - Jquery -

firefox - Where is 'webgl.osmesalib' parameter? -