关于c#:为什么这个字符串扩展方法不会抛出异常?

Why does this string extension method not throw an exception?

我有一个C字符串扩展方法,它应该返回字符串中子字符串的所有索引中的IEnumerable。它的工作完全符合其预期目的,并且返回了预期的结果(正如我的一个测试所证明的那样,尽管不是下面的测试),但是另一个单元测试发现了一个问题:它不能处理空参数。

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

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

当测试针对我的扩展方法运行时,它失败了,标准的错误消息是该方法"没有引发异常"。

这让人困惑:我清楚地将null传递给了函数,但是出于某种原因,比较null == null返回false。因此,不会引发异常,代码将继续。

我已经确认这不是测试中的错误:在我的主项目中运行方法时,在空比较if块中调用Console.WriteLine时,控制台上没有显示任何内容,我添加的任何catch块也没有捕获任何异常。此外,使用string.IsNullOrEmpty而不是== null也有同样的问题。

为什么这个简单的比较失败了?


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

从广义上讲,它将局部变量重写为该类的字段,并且您的算法的每个部分在yield return指令之间变成一个状态。您可以使用反编译程序检查编译后该方法会变成什么(确保关闭智能反编译,它将生成yield return)。

但底线是:在开始迭代之前,不会执行方法的代码。

检查前提条件的常用方法是将您的方法分为两部分:

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

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

注意,您还应该检查nullstr参数,因为扩展方法可以在null值上调用,因为它们只是语法上的糖分。

如果你想知道编译器对你的代码做了什么,这里是你的方法,用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中是合法的——例如,以一种无法避免名称冲突的方式命名变量。

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


您有一个迭代器块。该方法中的任何代码都不会在对返回迭代器的MoveNext调用之外运行。调用该方法只会注意到,但会创建状态机,而且不会失败(超出极端情况,如内存不足错误、堆栈溢出或线程中止异常)。

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

这就是为什么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;
}


正如其他人所说,枚举器直到开始枚举(即调用IEnumerable.GetNext方法)时才会进行评估。这样

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

直到开始枚举,即

1
2
3
4
foreach(int index in indexes)
{
    // ArgumentNullException
}