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
대한 매개 변수 도 확인해야합니다 .null
null
컴파일러가 코드에 대해 수행하는 작업에 대해 궁금한 경우 컴파일러 생성 코드 표시 옵션을 사용하여 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
}