I\'ve got a C# string extension method that should return an IEnumerable
of all the indexes of a substring within a string. It works perfectly for it
You are using yield return
. When doing so, the compiler will rewrite your method into a function that returns a generated class that implements a state machine.
Broadly speaking, it rewrites locals to fields of that class and each part of your algorithm between the yield return
instructions becomes a state. You can check with a decompiler what this method becomes after compilation (make sure to turn off smart decompilation which would produce yield return
).
But the bottom line is: the code of your method won't be executed until you start iterating.
The usual way to check for preconditions is to split your method in two:
public static IEnumerable 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 AllIndexesOfCore(string str, string searchText)
{
for (int index = 0; ; index += searchText.Length)
{
index = str.IndexOf(searchText, index);
if (index == -1)
break;
yield return index;
}
}
This works because the first method will behave just like you expect (immediate execution), and will return the state machine implemented by the second method.
Note that you should also check the str
parameter for null
, because extensions methods can be called on null
values, as they're just syntactic sugar.
If you're curious about what the compiler does to your code, here's your method, decompiled with dotPeek using the Show Compiler-generated Code option.
public static IEnumerable AllIndexesOf(this string str, string searchText)
{
Test.d__0 allIndexesOfD0 = new Test.d__0(-2);
allIndexesOfD0.<>3__str = str;
allIndexesOfD0.<>3__searchText = searchText;
return (IEnumerable) allIndexesOfD0;
}
[CompilerGenerated]
private sealed class d__0 : IEnumerable, IEnumerable, IEnumerator, 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 5__1;
int IEnumerator.Current
{
[DebuggerHidden] get
{
return this.<>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden] get
{
return (object) this.<>2__current;
}
}
[DebuggerHidden]
public d__0(int <>1__state)
{
base..ctor();
this.<>1__state = param0;
this.<>l__initialThreadId = Environment.CurrentManagedThreadId;
}
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
Test.d__0 allIndexesOfD0;
if (Environment.CurrentManagedThreadId == this.<>l__initialThreadId && this.<>1__state == -2)
{
this.<>1__state = 0;
allIndexesOfD0 = this;
}
else
allIndexesOfD0 = new Test.d__0(0);
allIndexesOfD0.str = this.<>3__str;
allIndexesOfD0.searchText = this.<>3__searchText;
return (IEnumerator) allIndexesOfD0;
}
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator) this.System.Collections.Generic.IEnumerable.GetEnumerator();
}
bool IEnumerator.MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
if (this.searchText == null)
throw new ArgumentNullException("searchText");
this.5__1 = 0;
break;
case 1:
this.<>1__state = -1;
this.5__1 += this.searchText.Length;
break;
default:
return false;
}
this.5__1 = this.str.IndexOf(this.searchText, this.5__1);
if (this.5__1 != -1)
{
this.<>2__current = this.5__1;
this.<>1__state = 1;
return true;
}
goto default;
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
void IDisposable.Dispose()
{
}
}
This is invalid C# code, because the compiler is allowed to do things the language doesn't allow, but which are legal in IL - for instance naming the variables in a way you couldn't to avoid name collisions.
But as you can see, the AllIndexesOf
only constructs and returns an object, whose constructor only initializes some state. GetEnumerator
only copies the object. The real work is done when you start enumerating (by calling the MoveNext
method).