작성하는 거의 모든 프로그램은 컬렉션을 순환하거나 처리해야 할 필요가 있습니다. 컬렉션의 모든 항목을 검사하는 코드를 작성합니다.
또한 해당 클래스의 요소에 대한 반복기를 생성하는 메서드인 반복기 메서드를 만듭니다. 반복기는 컨테이너, 특히 목록을 트래버스하는 개체입니다. 반복기는 다음 용도로 사용할 수 있습니다.
- 컬렉션의 각 항목에 대해 작업을 수행합니다.
- 사용자 지정 컬렉션을 열거합니다.
- 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 프로젝트에 있고 디바이스 센서가 매우 큰 데이터 스트림을 생성하고 있다고 상상해 보십시오. 데이터에 대한 느낌을 얻으려면 모든 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
문장은 foreach
및 IEnumerable<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
문장은 당신을 위해 모든 뉘앙스를 처리합니다. 컴파일러는 이러한 구문에 대한 올바른 코드를 생성합니다.
.NET