서적에서 또 신기한 것을 발견하여 조금 심화하여 기록해 보겠습니다.
아래와 같은 모든 열거된 내용을 foreach로 출력하는 코드는 index를 지정하거나 length를 알지 않아도 사용할 수 있어서 자주 사용하게 됩니다.
foreach(item in items)
{
Console.WriteLine(item);
}
그런데 이 foreach 는 정해진 영역의 것들을 하나하나 출력하는 것이라고만 느껴 왔는데, 무한하게 사용할 수도 있다는 사실을 알게 되었습니다. 비결은 바로 IEnumerator, IEnumerable을 사용하는 것입니다. 아직은 사용하기 어려운 개념입니다.
클래스를 열거형으로 만들기 위해 IEnumerable 을 상속하였더니, 아래와 같은 문구가 뜹니다.
IEnumerable 은 추상 클래스인데, 자식 클래스에서는 GetEnumerator를 구현해 주지 않으면 위와 같이 에러가 발생합니다.
그래서 구현을 해 주면, 이번에는 또 다른 에러가 뜹니다.
아주 기초적인 실수를 했습니다. IEnumerator<string> 타입으로 반환을 해야 하는데, string 으로 반환을 했습니다.
그렇다면 IEnumerator<string> 타입은 뭘까요? IEnumerator<T> 타입을 상속받은 클래스라는 것은, foreach가 가능한 클래스임을 의미합니다. 여기서 주목할 점은, 이처럼 생성된 클래스는 배열이 아니고 단지 foreach를 사용할 수 있는 클래스라는 것입니다.
그리고, 사실 지금 위에 적은 예시도 함정이 있습니다. 위에 적힌 class A는 IEnumerator<T> 를 상속받은 클래스가 아니고, IEnumerator를 상속 받은 클래스를 리턴하는 다른 클래스입니다. 정말 어렵죠 ... 요약하면 이렇습니다.
IEnumerable 을 상속받은 클래스는
foreach를 사용하여
IEnumerator 를 상속받은 클래스를 리턴
잘 이해가 되지 않으신다면, 아마 포스팅 최하단의 예시 코드를 보셔야 이해가 되실 겁니다. 이게 무슨 말인지 !
모쪼록 IEnumerator<T>를 상속받은 클래스는 foreach를 구현하기 위한 총 네 가지 맴버의 구현이 필요합니다.
public void Reset() {}
public T Current;
object IEnumerator.Current;
public void Dispose() {}
- bool MoveNext()
foreach 로 넘길 다음이 있는지를 리턴합니다.
foreach를 중단하고 싶을 때 false 를 리턴합니다. - void Reset()
foreach를 처음 시작할 때의 클래스 상태입니다. - T Current
foreach에서 찾을 각각의 현재 상황입니다. - object IEnumerator.Curreunt
이것 또한 foreach에서 찾을 각각의 현재 상황인 것으로 보이는데,
3의 Current와 분리되어 있는 이유는 제너릭 때문인 것으로 유추됩니다. - void Dispose()
이 클래스가 해제될때 실행되는 함수
이제 좀 감이 옵니다! 여기서 foreach가 멈추지 않고 실행되게 하기 위해서는 MoveNext() 가 영원히 true 를 리턴하도록 만들면 되겠네요. 완성된 코드는 아래와 같습니다.
public static void Main(string[] args)
{
B items = new B();
foreach (var item in items)
{
Console.WriteLine(item as string);
}
}
public class B : IEnumerable<string>
{
public IEnumerator<string> GetEnumerator()
{
return new A();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public class A : IEnumerator<string>
{
private string data = "";
public bool MoveNext()
{
data += "A";
// 아래 주석을 풀면 유한해짐
// if (data.Length > 10)
// {
// return false;
// }
return true;
}
public void Reset()
{
data = "";
}
public string Current {
get
{
return this.data;
}
}
object IEnumerator.Current => Current;
public void Dispose()
{
data = null;
}
}
출력
A
AA
AAA
AAAA
... (무한히 반복)
그리고 위 코드는 좋은 코드가 아닙니다. yield 를 사용하면 대폭 줄일 수 있기 때문입니다.
하지만 yield에 대해서는 완벽히 이해가 되지 않아서 코드만 남기고 다음 기회에 글을 작성해 보도록 하겠습니다!
public static void Main(string[] args)
{
B items = new B();
foreach (var item in items.Next())
{
Console.WriteLine(item as string);
}
}
public class B
{
public IEnumerable<string> Next()
{
string data = "";
while (true)
{
data+="A";
// 마찬가지로 이 쿠석을 풀면 유한해짐
// if (data.Length > 10)
// {
// yield break;
// }
yield return data;
}
}
}
정말 간단하죠.. 빨리 yield를 정복해야겠습니다.
어째 열심히 공부했는데도 속이 시원하지 않습니다...
무언가 복잡하다는 찜찜한 때문이기도 하고, GetEnumerator() 함수의 중복 때문이기도 하고 ... 신경 쓰이는 부분이 너무 많습니다.
이런 어렵고 복잡한 사용은 여러 번 읽어 보아야 완전히 내 것이 될 것 같습니다.
끝