为什么这个字符串扩展方法不引发异常?

我有一个 C # 字符串扩展方法,它应该返回一个字符串中子字符串的所有索引的 IEnumerable<int>。它完美地达到了预期的目的,并且返回了预期的结果(我的一个测试已经证明了这一点,尽管下面的测试没有) ,但是另一个单元测试发现了它的一个问题: 它不能处理 null 参数。

下面是我正在测试的扩展方法:

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

When the test runs against my extension method, it fails, with the standard error message that the method "did not throw an exception".

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

我已经通过测试证实了这不是一个 bug: 当在我的主项目中运行这个方法时,在 null 比较 if块中调用 Console.WriteLine时,控制台上没有显示任何东西,我添加的任何 catch块也不会捕获任何异常。此外,使用 string.IsNullOrEmpty而不是 == null也存在同样的问题。

为什么这个看似简单的比较会失败呢?

7537 次浏览

You have an iterator block. None of the code in that method is ever run outside of calls to MoveNext on the returned iterator. Calling the method does noting but create the state machine, and that won't ever fail (outside of extremes such as out of memory errors, stack overflows, or thread abort exceptions).

当您实际尝试迭代序列时,您将得到异常。

这就是为什么 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。这样做时,编译器将把您的方法重写为一个函数,该函数返回实现状态机的生成类。

广义地说,它将局部变量重写到该类的字段中,并且在 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;
}
}

这是因为第一个方法的行为与您预期的一样(即时执行) ,并且将返回由第二个方法实现的状态机。

Note that you should also check the str parameter for null, because extensions methods 可以 be called on null values, as they're just syntactic sugar.


如果你想知道编译器对你的代码做了什么,这是你的方法,使用 显示编译器生成的代码选项用 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()
{
}
}

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.

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

正如其他人所说的,枚举器直到它们开始被枚举时才进行计算(即调用 IEnumerable.GetNext方法)。所以这个

List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>();

在开始枚举之前不会被计算,即。

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