为什么这个字符串扩展方法没有抛出异常?
Why does this string extension method not throw an exception?
我有一个 C# 字符串扩展方法,它应该 return 一个字符串中子字符串的所有索引的 IEnumerable<int>
。它完美地实现了它的预期目的,预期的结果是 returned(正如我的一个测试所证明的,虽然不是下面的那个),但是另一个单元测试发现了它的一个问题:它无法处理空参数。
这是我正在测试的扩展方法:
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;
}
}
这是标记问题的测试:
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
string test = "a.b.c.d.e";
test.AllIndexesOf(null);
}
当针对我的扩展方法运行测试时,它失败了,标准错误消息是方法 "did not throw an exception".
这很令人困惑:我已经明确地将 null
传递给函数,但出于某种原因,比较 null == null
是 returning false
。因此,没有抛出异常,代码继续。
我已经确认这不是测试的错误:当 运行 我的主项目中的方法调用空比较 if
块中的 Console.WriteLine
时,控制台上没有显示任何内容,我添加的任何 catch
块也没有捕获到任何异常。此外,使用 string.IsNullOrEmpty
而不是 == null
也有同样的问题。
为什么这个看似简单的比较会失败?
您有一个迭代器块。 None 该方法中的代码曾经 运行 在返回的迭代器上调用 MoveNext
之外。调用该方法不会注意但会创建状态机,并且永远不会失败(除了内存不足错误、堆栈溢出或线程中止异常等极端情况)。
当您实际尝试迭代序列时,您会遇到异常。
这就是为什么 LINQ 方法实际上需要两种方法来拥有它们想要的错误处理语义。他们有一个私有方法,它是一个迭代器块,然后是一个非迭代器块方法,它除了进行参数验证之外什么都不做(这样它就可以急切地完成,而不是被推迟),同时仍然推迟所有其他功能。
所以这是一般模式:
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;
}
您正在使用 yield return
。这样做时,编译器会将您的方法重写为一个函数,该函数 return 是一个生成的 class 实现状态机的函数。
从广义上讲,它将局部变量重写为 class 的字段,并且 yield return
指令之间算法的每个部分都变成了一个状态。你可以用反编译器检查这个方法在编译后变成了什么(确保关闭智能反编译会产生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;
}
}
这是可行的,因为第一种方法的行为与您预期的一样(立即执行),并且 return 第二种方法实现的状态机。
请注意,您还应该检查 null
的 str
参数,因为扩展方法 可以 在 null
值上调用,因为它们只是语法糖。
如果您对编译器对您的代码做了什么感到好奇,这是您的方法,使用 显示编译器生成的代码 选项使用 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 中合法的操作 - 例如,以无法避免名称冲突的方式命名变量。
但是如你所见,AllIndexesOf
只是构造和return一个对象,它的构造函数只是初始化一些状态。 GetEnumerator
只复制对象。真正的工作在您开始枚举时完成(通过调用 MoveNext
方法)。
正如其他人所说,枚举数在开始被枚举之前不会被评估(即调用 IEnumerable.GetNext
方法)。因此这
List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>();
在您开始枚举之前不会进行评估,即
foreach(int index in indexes)
{
// ArgumentNullException
}
我有一个 C# 字符串扩展方法,它应该 return 一个字符串中子字符串的所有索引的 IEnumerable<int>
。它完美地实现了它的预期目的,预期的结果是 returned(正如我的一个测试所证明的,虽然不是下面的那个),但是另一个单元测试发现了它的一个问题:它无法处理空参数。
这是我正在测试的扩展方法:
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;
}
}
这是标记问题的测试:
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
string test = "a.b.c.d.e";
test.AllIndexesOf(null);
}
当针对我的扩展方法运行测试时,它失败了,标准错误消息是方法 "did not throw an exception".
这很令人困惑:我已经明确地将 null
传递给函数,但出于某种原因,比较 null == null
是 returning false
。因此,没有抛出异常,代码继续。
我已经确认这不是测试的错误:当 运行 我的主项目中的方法调用空比较 if
块中的 Console.WriteLine
时,控制台上没有显示任何内容,我添加的任何 catch
块也没有捕获到任何异常。此外,使用 string.IsNullOrEmpty
而不是 == null
也有同样的问题。
为什么这个看似简单的比较会失败?
您有一个迭代器块。 None 该方法中的代码曾经 运行 在返回的迭代器上调用 MoveNext
之外。调用该方法不会注意但会创建状态机,并且永远不会失败(除了内存不足错误、堆栈溢出或线程中止异常等极端情况)。
当您实际尝试迭代序列时,您会遇到异常。
这就是为什么 LINQ 方法实际上需要两种方法来拥有它们想要的错误处理语义。他们有一个私有方法,它是一个迭代器块,然后是一个非迭代器块方法,它除了进行参数验证之外什么都不做(这样它就可以急切地完成,而不是被推迟),同时仍然推迟所有其他功能。
所以这是一般模式:
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;
}
您正在使用 yield return
。这样做时,编译器会将您的方法重写为一个函数,该函数 return 是一个生成的 class 实现状态机的函数。
从广义上讲,它将局部变量重写为 class 的字段,并且 yield return
指令之间算法的每个部分都变成了一个状态。你可以用反编译器检查这个方法在编译后变成了什么(确保关闭智能反编译会产生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;
}
}
这是可行的,因为第一种方法的行为与您预期的一样(立即执行),并且 return 第二种方法实现的状态机。
请注意,您还应该检查 null
的 str
参数,因为扩展方法 可以 在 null
值上调用,因为它们只是语法糖。
如果您对编译器对您的代码做了什么感到好奇,这是您的方法,使用 显示编译器生成的代码 选项使用 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 中合法的操作 - 例如,以无法避免名称冲突的方式命名变量。
但是如你所见,AllIndexesOf
只是构造和return一个对象,它的构造函数只是初始化一些状态。 GetEnumerator
只复制对象。真正的工作在您开始枚举时完成(通过调用 MoveNext
方法)。
正如其他人所说,枚举数在开始被枚举之前不会被评估(即调用 IEnumerable.GetNext
方法)。因此这
List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>();
在您开始枚举之前不会进行评估,即
foreach(int index in indexes)
{
// ArgumentNullException
}