다음을 통해 공유


이터레이터

작성하는 거의 모든 프로그램은 컬렉션을 순환하거나 처리해야 할 필요가 있습니다. 컬렉션의 모든 항목을 검사하는 코드를 작성합니다.

또한 해당 클래스의 요소에 대한 반복기를 생성하는 메서드인 반복기 메서드를 만듭니다. 반복기는 컨테이너, 특히 목록을 트래버스하는 개체입니다. 반복기는 다음 용도로 사용할 수 있습니다.

  • 컬렉션의 각 항목에 대해 작업을 수행합니다.
  • 사용자 지정 컬렉션을 열거합니다.
  • LINQ 또는 기타 라이브러리 확장
  • 반복기 메서드를 통해 데이터가 효율적으로 흐르는 데이터 파이프라인 만들기

C# 언어는 시퀀스 생성 및 사용 모두에 대한 기능을 제공합니다. 이러한 시퀀스는 동기적으로 또는 비동기적으로 생성 및 사용할 수 있습니다. 이 문서에서는 이러한 기능에 대한 개요를 제공합니다.

foreach를 사용하여 반복

컬렉션을 열거하는 것은 간단합니다. 키워드는 foreach 컬렉션을 열거하고 컬렉션의 각 요소에 대해 포함된 문을 한 번 실행합니다.

foreach (var item in collection)
{
    Console.WriteLine(item?.ToString());
}

그게 전부에요. 컬렉션의 모든 내용을 반복하려면 foreach 문만 있으면 됩니다. 문장은 foreach 물론 마법이 아닙니다. .NET Core 라이브러리에 정의된 IEnumerable<T>IEnumerator<T> 두 개의 제네릭 인터페이스를 사용하여 컬렉션을 반복하는 데 필요한 코드를 생성합니다. 이 메커니즘은 아래에 자세히 설명되어 있습니다.

이러한 두 인터페이스에는 IEnumerableIEnumerator의 제네릭이 아닌 대응이 있습니다. 제네릭 버전은 최신 코드에 선호됩니다.

시퀀스가 비동기적으로 생성되는 경우, await foreach 문을 사용하여 해당 시퀀스를 비동기적으로 소비할 수 있습니다.

await foreach (var item in asyncSequence)
{
Console.WriteLine(item?.ToString());
}

시퀀스가 System.Collections.Generic.IEnumerable<T>인 경우, foreach을 사용합니다. 시퀀스가 System.Collections.Generic.IAsyncEnumerable<T>인 경우, await foreach을 사용합니다. 후자의 경우 시퀀스가 비동기적으로 생성됩니다.

반복기 메서드를 사용하여 열거하는 소스

C# 언어의 또 다른 훌륭한 기능을 사용하면 열거형에 대한 원본을 만드는 메서드를 빌드할 수 있습니다. 이러한 메서드를 반복기 메서드라고 합니다. 반복기 메서드는 요청 시 시퀀스에서 개체를 생성하는 방법을 정의합니다. 컨텍스트 키워드를 yield return 사용하여 반복기 메서드를 정의합니다.

이 메서드를 작성하여 0에서 9까지의 정수 시퀀스를 생성할 수 있습니다.

public IEnumerable<int> GetSingleDigitNumbers()
{
    yield return 0;
    yield return 1;
    yield return 2;
    yield return 3;
    yield return 4;
    yield return 5;
    yield return 6;
    yield return 7;
    yield return 8;
    yield return 9;
}

위의 코드는 반복기 메서드에서 여러 개의 불연속 yield return 문을 사용할 수 있다는 사실을 강조 표시하는 고유 yield return 문을 보여 줍니다. 반복기 메서드의 코드를 간소화하기 위해 다른 언어 구문을 사용할 수 있습니다(종종 사용). 아래 메서드 정의는 정확히 동일한 숫자 시퀀스를 생성합니다.

public IEnumerable<int> GetSingleDigitNumbersLoop()
{
    int index = 0;
    while (index < 10)
        yield return index++;
}

당신은 하나 또는 다른 결정할 필요가 없습니다. 메서드의 요구 사항을 충족하기 위해 필요한 만큼 문장을 포함할 yield return 수 있습니다.

public IEnumerable<int> GetSetsOfNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    index = 100;
    while (index < 110)
        yield return index++;
}

위의 모든 예제에는 비동기 대응 항목이 있습니다. 각각의 경우, 반환 형식을 IEnumerable<T>에서 IAsyncEnumerable<T>로 변경해야 합니다. 예를 들어 이전 예제에는 다음과 같은 비동기 버전이 있습니다.

public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    await Task.Delay(500);

    yield return 50;

    await Task.Delay(500);

    index = 100;
    while (index < 110)
        yield return index++;
}

동기 반복기와 비동기 반복기 모두에 대한 구문입니다. 실제 예제를 살펴보겠습니다. IoT 프로젝트에 있고 디바이스 센서가 매우 큰 데이터 스트림을 생성하고 있다고 상상해 보십시오. 데이터에 대한 느낌을 얻으려면 모든 Nth 데이터 요소를 샘플링하는 메서드를 작성할 수 있습니다. 이 작은 반복기 메서드는 이 문제를 해결합니다.

public static IEnumerable<T> Sample<T>(this IEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

IoT 디바이스에서 읽는 경우 비동기 시퀀스를 생성하는 경우 다음 메서드와 같이 메서드를 수정합니다.

public static async IAsyncEnumerable<T> Sample<T>(this IAsyncEnumerable<T> sourceSequence, int interval)
{
    int index = 0;
    await foreach (T item in sourceSequence)
    {
        if (index++ % interval == 0)
            yield return item;
    }
}

반복기 메서드에는 중요한 제한 사항이 하나 있습니다. 동일한 메서드에서 return 문장과 yield return 문장을 모두 사용할 수 없습니다. 다음 코드는 컴파일되지 않습니다.

public IEnumerable<int> GetSingleDigitNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    // generates a compile time error:
    var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
    return items;
}

이 제한은 일반적으로 문제가 되지 않습니다. 메서드 전체에서 yield return를 사용하거나, 원래 메서드를 여러 개로 나누어 일부는 return을 사용하고, 일부는 yield return를 사용하는 방법을 선택할 수 있습니다.

모든 위치에서 사용하도록 yield return 마지막 메서드를 약간 수정할 수 있습니다.

public IEnumerable<int> GetFirstDecile()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    var items = new int[] {100, 101, 102, 103, 104, 105, 106, 107, 108, 109 };
    foreach (var item in items)
        yield return item;
}

경우에 따라 올바른 대답은 반복기 메서드를 두 개의 다른 메서드로 분할하는 것입니다. return을 사용하는 것 하나와 yield return을 사용하는 두 번째 것 부울 인수에 따라 빈 컬렉션 또는 처음 5개의 홀수 값을 반환할 수 있는 상황을 고려합니다. 다음 두 가지 방법으로 작성할 수 있습니다.

public IEnumerable<int> GetSingleDigitOddNumbers(bool getCollection)
{
    if (getCollection == false)
        return new int[0];
    else
        return IteratorMethod();
}

private IEnumerable<int> IteratorMethod()
{
    int index = 0;
    while (index < 10)
    {
        if (index % 2 == 1)
            yield return index;
        index++;
    }
}

위의 메서드를 살펴보세요. 첫 번째는 표준 return 문을 사용하여 빈 컬렉션 또는 두 번째 메서드에서 만든 반복기를 반환합니다. 두 번째 메서드는 요청된 시퀀스를 만들기 위해 yield return 문장을 사용합니다.

심층 분석 foreach

문장은 foreachIEnumerable<T> 인터페이스를 사용하여 컬렉션의 모든 요소를 반복하는 표준 관용구로 확장됩니다. 또한 개발자가 리소스를 제대로 관리하지 않아 발생하는 오류를 최소화합니다.

컴파일러는 첫 번째 예제에 표시된 루프를 이 구문과 유사한 루프로 변환 foreach 합니다.

IEnumerator<int> enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
    var item = enumerator.Current;
    Console.WriteLine(item.ToString());
}

컴파일러에서 생성된 정확한 코드는 더 복잡하며, 반환된 개체가 GetEnumerator() 인터페이스를 구현하는 경우를 처리합니다 IDisposable. 전체 확장은 다음과 같은 코드를 생성합니다.

{
    var enumerator = collection.GetEnumerator();
    try
    {
        while (enumerator.MoveNext())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    }
    finally
    {
        // dispose of enumerator.
    }
}

컴파일러는 첫 번째 비동기 샘플을 이 구문과 유사한 것으로 변환합니다.

{
    var enumerator = collection.GetAsyncEnumerator();
    try
    {
        while (await enumerator.MoveNextAsync())
        {
            var item = enumerator.Current;
            Console.WriteLine(item.ToString());
        }
    }
    finally
    {
        // dispose of async enumerator.
    }
}

열거자가 삭제되는 방식은 형식 enumerator의 특성에 따라 달라집니다. 일반적인 동기 사례에서 finally 절은 확장되어 다음과 같이 됩니다.

finally
{
   (enumerator as IDisposable)?.Dispose();
}

일반적인 비동기 사례는 다음으로 확장됩니다.

finally
{
    if (enumerator is IAsyncDisposable asyncDisposable)
        await asyncDisposable.DisposeAsync();
}

그러나 형식 enumerator이 봉인된 형식이고 형식 enumerator에서 형식 IDisposable 또는 IAsyncDisposable로의 암시적 변환이 없는 경우, finally 절은 빈 블록으로 확장됩니다.

finally
{
}

enumerator에서 IDisposable로 암시적 변환이 있는 경우, 그리고 enumerator가 nullable이 아닌 값 형식인 경우, finally 절은 다음으로 확장됩니다.

finally
{
   ((IDisposable)enumerator).Dispose();
}

고맙게도, 당신은 이러한 모든 세부 사항을 기억할 필요가 없습니다. foreach 문장은 당신을 위해 모든 뉘앙스를 처리합니다. 컴파일러는 이러한 구문에 대한 올바른 코드를 생성합니다.