반복기
작성하는 거의 모든 프로그램에서 컬렉션을 반복해야 하는 경우가 있습니다. 컬렉션에 있는 모든 항목을 조사하는 코드를 작성합니다.
또한 해당 클래스의 요소에 대해 반복기를 생성하는 메서드인 반복기 메서드를 만듭니다. 반복기는 컨테이너, 특히 목록을 트래버스하는 개체입니다. 반복기는 다음과 같은 경우에 사용할 수 있습니다.
- 컬렉션의 각 항목에 대한 작업 수행.
- 사용자 지정 컬렉션 열거.
- LINQ 또는 다른 라이브러리 확장.
- 데이터가 반복기 메서드를 통해 효율적으로 흐르는 데이터 파이프라인 만들기.
C# 언어는 시퀀스를 생성하고 사용하는 기능을 제공합니다. 이러한 시퀀스는 동기적 또는 비동기적으로 생성 및 사용될 수 있습니다. 이 문서에서는 해당 기능에 대한 개요를 제공합니다.
foreach로 반복 처리
컬렉션 열거는 간단합니다. foreach
키워드는 컬렉션을 열거하여 컬렉션의 각 요소에 대해 포함된 문을 한 번 실행합니다.
foreach (var item in collection)
{
Console.WriteLine(item?.ToString());
}
그게 다입니다. 컬렉션의 모든 내용을 반복하려면 foreach
문만 있으면 됩니다. 하지만 foreach
문이 마법은 아닙니다. 이 명령문은 컬렉션을 반복하는 데 필요한 코드를 생성하기 위해 .NET core 라이브러리에 정의된 두 개의 제네릭 인터페이스인 IEnumerable<T>
및 IEnumerator<T>
를 사용합니다. 이 메커니즘은 아래에 더 자세히 설명되어 있습니다.
이러한 인터페이스 둘 다에는 제네릭이 아닌 인터페이스 IEnumerable
및 IEnumerator
도 있습니다. 최신 코드에는 제네릭 버전이 기본적으로 사용됩니다.
시퀀스를 비동기적으로 생성하는 경우 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 프로젝트를 진행하고 있고 디바이스 센서는 매우 큰 데이터 스트림을 생성한다고 가정합니다. 데이터를 파악하려면 N번째 데이터 요소마다 샘플링하는 메서드를 작성할 수 있습니다. 이 작은 반복기 메서드면 충분합니다.
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
심층 분석
foreach
문은 IEnumerable<T>
및 IEnumerator<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
의 형식이 sealed 형식이고 enumerator
의 형식에서 IDisposable
또는 IAsyncDisposable
로의 암시적 변환이 없는 경우 finally
절은 빈 블록으로 확장됩니다.
finally
{
}
enumerator
의 형식에서 IDisposable
로의 암시적 변환이 있고 enumerator
형식이 nullable이 아닌 값 형식인 경우 finally
절은 다음과 같이 확장됩니다.
finally
{
((IDisposable)enumerator).Dispose();
}
다행히도 이러한 세부 정보를 모두 기억할 필요가 없습니다. foreach
문에서 이러한 차이를 모두 처리합니다. 컴파일러는 이러한 구문에 대한 올바른 코드를 생성합니다.
.NET