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


119

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것은 동일한 문제가 있습니다.

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


5
코드를 단계별로 실행 해 보셨습니까? 그것은 아마도 꽤 빨리 해결 될 것입니다.
Matthew Haugen

1
무슨 일 일어나나요? ( 예외 발생 합니까? 그렇다면 어떤 줄과 어떤 줄입니까?)
user2864740

@ user2864740 나는 일어나는 모든 일을 설명했습니다. 예외는 없으며 실패한 테스트와 실행 방법 만 있습니다.
ArtOfCode

7
반복자는 반복 될 때까지 실행되지 않습니다.
BlueRaja-Danny Pflughoeft

2
천만에요. 이것은 또한 Jon의 "최악의 문제"목록을 만들었습니다. stackoverflow.com/a/241180/88656 . 이것은 매우 일반적인 문제입니다.
Eric Lippert

답변:


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대한 매개 변수 도 확인해야합니다 .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메서드 호출 ).


9
BTW, 나는 대답에 다음과 같은 중요한 요점을 추가했습니다. 확장 메서드는 값에 대해 호출 될 수 있기 때문에 매개 변수 도 확인해야합니다 . strnullnull
Lucas Trzesniewski

2
yield return원칙적으로는 좋은 생각이지만 이상한 점이 너무 많습니다. 이것을 밝혀 주셔서 감사합니다!
nateirvin

따라서 foreach에서와 같이 enumarator가 실행되면 기본적으로 오류가 발생합니까?
MVCDS

1
@MVCDS 맞습니다. 구조에 MoveNext의해 후드 아래에서 호출됩니다 foreach. 나는 무엇에 대한 설명을 썼다 foreach에서와 수집 의미를 설명하는 내 대답은 당신이 정확한 패턴을보고 싶은 경우입니다.
Lucas Trzesniewski

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
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.