Почему этот метод расширения строки не исключение?


у меня есть метод расширения строки C#, который должен возвращать IEnumerable<int> всех индексов подстроки внутри строки. Он отлично работает по назначению, и ожидаемые результаты возвращаются (как доказано одним из моих тестов, хотя и не ниже), но другой модульный тест обнаружил проблему с ним: он не может обрабатывать нулевые аргументы.

вот метод расширения, который я тестирую:

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

когда тест выполняется против моего метода расширения, он терпит неудачу со стандартным сообщением об ошибке, что метод"не выдал исключение".

это сбивает с толку: я четко передают null в функцию, но почему-то сравнение null == null возвращается false. Поэтому исключение не создается, и код продолжается.

Я подтвердил, что это не ошибка с тестом: при запуске метода в моем основном проекте с призывом к Console.WriteLine в нулевом сравнении if блок, ничего не отображается на консоли, и никакое исключение не поймано ни catch блок я добавить. Кроме того, используя string.IsNullOrEmpty вместо == null та же проблема.

почему это якобы-простое сравнение неудачно?

3 116

3 ответа:

вы используете 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;
    }
}

это работает, потому что первый метод будет вести себя так же, как вы ожидаете (немедленное выполнение), и вернет конечный автомат, реализованный вторым методом.

обратите внимание, что вы должны также проверить .

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 на возвращенный iterator. Вызов метода не замечает, но создает конечный автомат, и это никогда не приведет к сбою (за пределами крайностей, таких как ошибки из памяти, переполнения стека или исключения прерывания потока).

когда вы на самом деле пытаетесь повторить последовательность, вы получите исключения.

вот почему методы 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;
}

Перечислители, как сказали другие, не оцениваются до тех пор, пока они не начнут перечисляться (т. е. IEnumerable.GetNext метод называется). Таким образом, это

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

не оценивается, пока вы не начнете перечислять, т. е.

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