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
Post a Comment