[C#] 이 문자열 확장 메서드가 예외를 throw하지 않는 이유는 무엇입니까?

IEnumerable<int>문자열 내에서 하위 문자열의 모든 인덱스를 반환해야하는 C # 문자열 확장 메서드가 있습니다. 의도 한 목적에 완벽하게 작동하고 예상 결과가 반환되지만 (아래 테스트는 아니지만 내 테스트 중 하나에서 입증 됨) 다른 단위 테스트에서 문제를 발견했습니다. 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);
}

내 확장 메서드에 대해 테스트를 실행하면 메서드가 “예외를 throw하지 않았습니다”라는 표준 오류 메시지와 함께 실패합니다.

이것은 혼란 스럽습니다. 명확하게 null함수에 전달 했지만 어떤 이유로 비교 null == null가 반환 false됩니다. 따라서 예외가 발생하지 않고 코드가 계속됩니다.

나는 이것이 테스트의 버그가 아니라는 것을 확인했다. 내 메인 프로젝트 Console.WriteLine에서 null-comparison if블록을 호출하여 메서드를 실행할 때 콘솔에 아무것도 표시되지 않고 catch내가 추가 한 블록에 예외가 발생하지 않는다 . 또한 string.IsNullOrEmpty대신 사용 하는 == null것은 동일한 문제가 있습니다.

이 단순한 비교가 실패하는 이유는 무엇입니까?



답변

을 (를) 사용하고 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대한 매개 변수 도 확인해야합니다 .nullnull


컴파일러가 코드에 대해 수행하는 작업에 대해 궁금한 경우 컴파일러 생성 코드 표시 옵션을 사용하여 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()
  {
  }
}

이것은 잘못된 C # 코드입니다. 컴파일러는 언어가 허용하지 않지만 IL에서는 합법적입니다. 예를 들어 이름 충돌을 피할 수없는 방식으로 변수 이름을 지정합니다.

그러나 보시 AllIndexesOf다시피 유일한 생성자는 객체를 생성하고 반환하며 생성자는 일부 상태 만 초기화합니다. GetEnumerator개체 만 복사합니다. 실제 작업은 열거를 시작할 때 수행됩니다 ( MoveNext메서드 호출 ).


답변

반복자 블록이 있습니다. 해당 메서드의 코드 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;
}


답변

다른 사람들이 말했듯이 열거자는 열거되기 시작할 때까지 (즉, IEnumerable.GetNext메서드가 호출 될 때까지) 평가되지 않습니다 . 따라서 이것은

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

열거를 시작할 때까지 평가되지 않습니다.

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


답변