为什么这个string扩展方法不会抛出exception?

我有一个C#string扩展方法,它应该返回一个string中子string的所有索引的IEnumerable<int> 。 它完美的预期目的和预期的结果返回(如我的一个testing,虽然不是下面的一个certificate),但另一个unit testing发现它的问题:它不能处理空参数。

这是我testing的扩展方法:

 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; } } 

这是标记问题的testing:

 [TestMethod] [ExpectedException(typeof(ArgumentNullException))] public void Extensions_AllIndexesOf_HandlesNullArguments() { string test = "abcde"; test.AllIndexesOf(null); } 

当testing针对我的扩展方法运行时,它会失败,并显示标准的错误消息,即方法“没有抛出exception”。

这是令人困惑的:我已经清楚地将null传递给函数,但由于某种原因比较null == null返回false 。 因此,不会抛出exception,代码会继续。

我已经证实这不是一个错误与testing:当在我的主要项目中运行方法调用Console.WriteLine在null比较if块,没有任何显示在控制台上并没有任何catch块捕获exception我加。 此外,使用string.IsNullOrEmpty而不是== null具有相同的问题。

为什么这个被认为是简单的比较失败?

您正在使用yield return 。 当这样做的时候,编译器会把你的方法重写成一个返回一个实现了状态机的生成类的函数。

一般来说,它将当地人重写为该类的字段,并且在yield return指令之间的每个algorithm部分都成为一个状态。 你可以用一个反编译器检查这个方法在编译之后变成什么样子(确保closures智能反编译,这会产生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值上调用,因为它们只是语法糖。


如果您对编译器对代码的操作感到好奇,可以使用Show Compiler生成的代码选项,使用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() { } } 

这是无效的C#代码,因为编译器被允许执行语言不允许的操作,但在IL中是合法的,例如命名variables的方式不能避免名称冲突。

但是你可以看到, AllIndexesOf只构造并返回一个对象,它的构造函数只是初始化一些状态。 GetEnumerator只复制对象。 当你开始枚举时(通过调用MoveNext方法),真正的工作就完成了。

你有一个迭代器块。 该方法中的任何代码都不会在返回的迭代器上调用MoveNext之外运行。 调用方法注意到,但是创build状态机,并且不会失败(超出内存错误,堆栈溢出或线程中止exception等极端情况)。

当你实际上尝试迭代序列时,你会得到exception。

这就是为什么LINQ方法实际上需要两种方法来实现他们所期望的error handling语义。 他们有一个私有方法,它是一个迭代器块,然后是一个非迭代器块方法,除了进行参数validation之外(这样它可以急切地完成,而不是延迟),而仍然推迟所有其他function。

所以这是一般的模式:

 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; } 

正如其他人所说的,枚举器只有在它们开始枚举(即IEnumerable.GetNext方法被调用)时才会被评估。 因此,

 List<int> indexes = "abcde".AllIndexesOf(null).ToList<int>(); 

直到开始枚举才会被评估,即

 foreach(int index in indexes) { // ArgumentNullException }