Подтвердить что ты не робот

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

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

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

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 имеет ту же проблему.

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

4b9b3361

Ответ 1

Вы используете yield return. При этом компилятор переписывает ваш метод в функцию, которая возвращает сгенерированный класс, который реализует конечный автомат.

Вообще говоря, он переписывает locals в поля этого класса, и каждая часть вашего алгоритма между инструкциями 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;
    }
}

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

Обратите внимание, что вы также должны проверить параметр str для null, потому что методы расширения можно вызывать на значения null, так как они всего лишь синтаксический сахар.


Если вам интересно, что компилятор делает с вашим кодом, вот ваш метод, декомпилированный с помощью dotPeek, используя опцию Show Code, генерируемую компилятором.

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()
  {
  }
}

Это недопустимый код С#, потому что компилятору разрешено делать то, что язык не разрешает, но который является законным в IL - например, именовать переменные таким образом, чтобы избежать конфликтов имен.

Но, как вы можете видеть, AllIndexesOf только конструирует и возвращает объект, конструктор которого только инициализирует некоторое состояние. GetEnumerator копирует только объект. Реальная работа выполняется, когда вы начинаете перечисление (путем вызова метода MoveNext).

Ответ 2

У вас есть блок итератора. Ни один из кода этого метода никогда не запускается за пределами вызовов 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;
}

Ответ 3

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

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

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

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