Why does this string extension method not throw an exception?
我有一个C字符串扩展方法,它应该返回字符串中子字符串的所有索引中的
下面是我正在测试的扩展方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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; } } |
下面是标记问题的测试:
1 2 3 4 5 6 7 | [TestMethod] [ExpectedException(typeof(ArgumentNullException))] public void Extensions_AllIndexesOf_HandlesNullArguments() { string test ="a.b.c.d.e"; test.AllIndexesOf(null); } |
当测试针对我的扩展方法运行时,它失败了,标准的错误消息是该方法"没有引发异常"。
这让人困惑:我清楚地将
我已经确认这不是测试中的错误:在我的主项目中运行方法时,在空比较
为什么这个简单的比较失败了?
您正在使用
从广义上讲,它将局部变量重写为该类的字段,并且您的算法的每个部分在
但底线是:在开始迭代之前,不会执行方法的代码。
检查前提条件的常用方法是将您的方法分为两部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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; } } |
这是因为第一个方法的行为与您期望的一样(立即执行),并且将返回由第二个方法实现的状态机。
注意,您还应该检查
如果你想知道编译器对你的代码做了什么,这里是你的方法,用dotpeek使用show compiler generated code选项进行反编译。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | 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中是合法的——例如,以一种无法避免名称冲突的方式命名变量。
但如您所见,
您有一个迭代器块。该方法中的任何代码都不会在对返回迭代器的
当您实际尝试迭代序列时,您将得到异常。
这就是为什么LINQ方法实际上需要两个方法来拥有他们想要的错误处理语义。它们有一个私有的方法,它是一个迭代器块,然后是一个非迭代器块方法,它只做参数验证(这样它可以很快完成,而不是被延迟),同时仍然延迟所有其他功能。
这就是一般的模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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; } |
正如其他人所说,枚举器直到开始枚举(即调用
1 | List<int> indexes ="a.b.c.d.e".AllIndexesOf(null).ToList<int>(); |
直到开始枚举,即
1 2 3 4 | foreach(int index in indexes) { // ArgumentNullException } |