Чому цей метод розширення рядків не кидає виняток?


119

У мене є метод розширення рядка на 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має ту саму проблему.

Чому це нібито-просте порівняння не вдається?


5
Ви спробували перейти через код? Це, ймовірно, буде вирішено досить швидко.
Метью Хауген

1
Що ж сталося? (Чи має він кинути в виняток, якщо це так, що один і якій лінії?)
user2864740

@ user2864740 Я описав усе, що відбувається. Немає винятків, просто невдалий тест та метод запуску.
ArtOfCode

7
Ітератори не виконуються , поки вони не итеративно-над
BlueRaja - Денні Pflughoeft

2
Ласкаво просимо. Цей також склав список "найстрашніших" Йона: stackoverflow.com/a/241180/88656 . Це досить поширена проблема.
Ерік Ліпперт

Відповіді:


158

Ви використовуєте 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;
    }
}

Це працює, тому що перший метод буде вести себе так, як ви очікуєте (негайне виконання), і поверне стан машини, реалізований другим методом.

Зауважте, що ви також повинні перевірити strпараметр для null, тому що методи розширень можуть бути викликані nullзначеннями, оскільки вони просто синтаксичний цукор.


Якщо вам цікаво, що компілятор робить до вашого коду, ось ваш метод, декомпільований за допомогою dotPeek, використовуючи параметр Show Code-generated 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()
  {
  }
}

Це недійсний код C #, оскільки компілятору дозволено робити те, що мова не дозволяє, але які є законними в IL - наприклад, називати змінні таким чином, щоб ви не могли уникнути зіткнень імен.

Але, як бачите, AllIndexesOfєдиний конструює та повертає об’єкт, конструктор якого лише ініціалізує деякий стан. GetEnumeratorлише копіює об’єкт. Справжня робота виконується, коли ви починаєте перерахувати (викликаючи MoveNextметод).


9
До речі, я додав наступний важливий момент у відповідь: Зауважте, що ви також повинні перевірити strпараметр для null, тому що методи розширень можуть бути викликані nullзначеннями, оскільки вони просто синтаксичний цукор.
Лукас Трушевський

2
yield returnце приємна ідея в принципі, але в ній так багато дивних готчей. Дякуємо за те, що ви з'явилися на світ!
nateirvin

Отже, в основному помилка була б викинута, якби запускався енумаратор, як в передбаченні?
MVCDS

1
@MVCDS Рівно. MoveNextназивається під капотом foreachконструкцією. Я написав пояснення того, що foreachвідповідає у своїй відповіді, пояснюючи семантику колекції, якщо ви хочете побачити точну схему.
Лукас Тшесневський

34

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

0

Як сказали інші, перелічувачі не оцінюються до моменту початку їх перерахування (тобто IEnumerable.GetNextметод називається). Таким чином це

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

не оцінюється, поки ви не почнете перераховувати, тобто

foreach(int index in indexes)
{
    // ArgumentNullException
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.