yield return은 컬렉션 전체를 미리 만들어 두지 않고, 필요한 순간에 하나씩 값을 생성해 내는 이터레이터를 쉽게 작성하게 해줍니다. 결과적으로 메모리 사용을 줄이고, 실제로 필요한 만큼만 계산하도록 지연 실행을 구현할 수 있습니다.
1. 기본 동작: 열거할 때마다 실행됩니다
아래 예제는 foreach가 진행될 때마다 값이 생성됩니다. 같은 시퀀스를 두 번 열거하면 생성 과정도 두 번 실행됩니다.
using System;
using System.Collections.Generic;
class Program
{
static IEnumerable<int> Numbers()
{
Console.WriteLine("Generate 1");
yield return 1;
Console.WriteLine("Generate 2");
yield return 2;
Console.WriteLine("Generate 3");
yield return 3;
}
static void Main()
{
Console.WriteLine("First foreach");
foreach (var n in Numbers())
Console.WriteLine($"Use {n}");
Console.WriteLine("Second foreach");
foreach (var n in Numbers())
Console.WriteLine($"Use {n}");
}
}
포인트: IEnumerable<T>는 열거를 시작할 때마다 소스가 다시 실행됩니다. 부작용이 있거나 비용이 큰 생성 로직이라면 주의가 필요합니다.
2. 필요한 만큼만 계산: LINQ와 함께 쓰기
지연 실행은 Take, Where 같은 LINQ 연산자와 결합될 때 강력합니다. 아래는 무한 시퀀스에서 앞의 3개만 소비합니다.
using System;
using System.Collections.Generic;
using System.Linq;
static IEnumerable<int> Natural()
{
int i = 0;
while (true)
yield return ++i;
}
static void Main()
{
foreach (var n in Natural().Take(3))
Console.WriteLine(n); // 1,2,3만 생성/소비됨
}
포인트: Take(3)이 더 이상 필요하지 않을 때 열거를 중단하므로, 뒤의 값은 생성되지 않습니다.
3. 비용 큰 작업의 중복 실행 피하기
여러 번 열거하면 매번 로직이 재실행됩니다. 한 번만 실행하고 재사용하려면 물리화(예: ToList)하세요.
using System;
using System.Collections.Generic;
using System.Linq;
static IEnumerable<int> GetData()
{
Console.WriteLine("Query...");
for (int i = 1; i <= 3; i++)
{
Console.WriteLine($"Fetch {i}");
yield return i;
}
}
static void Main()
{
var seq = GetData();
Console.WriteLine(seq.Count()); // 첫 번째 열거(쿼리+페치 발생)
Console.WriteLine(seq.Count()); // 두 번째 열거(다시 발생)
var cached = seq.ToList(); // 한 번만 실행 후 캐시
Console.WriteLine(cached.Count); // 이후 재사용 시 추가 실행 없음
Console.WriteLine(cached.Count);
}
포인트: UI 바인딩, 반복 계산, 로그 출력 등 부작용이 있거나 비용이 큰 경우 ToList/ToArray로 결과를 캐시합니다.
4. 리소스와 함께 쓰기: 파일을 줄 단위로 읽기
using과 yield를 함께 사용하면 열거 완료 시 안전하게 리소스를 해제할 수 있습니다.
using System.Collections.Generic;
using System.IO;
using System.Linq;
static IEnumerable<string> ReadLines(string path)
{
using var sr = new StreamReader(path);
string? line;
while ((line = sr.ReadLine()) != null)
yield return line;
}
static void Main()
{
foreach (var line in ReadLines("log.txt").Take(5))
System.Console.WriteLine(line);
}
포인트: 컴파일러가 상태 머신을 생성하여 열거가 끝나거나 중단될 때 StreamReader를 해제합니다.
5. 실전 가이드
- 시퀀스가 크거나 끝을 모를 때: yield return으로 스트리밍하세요.
- 앞부분만 필요할 때: Take/FirstOrDefault 등과 결합해 불필요한 계산을 막습니다.
- 동일 결과를 여러 번 사용할 때: ToList/ToArray로 한 번만 실행합니다.
- 부작용 로직(로그, 외부 호출)이 섞여 있을 때: 여러 번 열거되지 않도록 주의합니다.
정리: yield return은 값을 "나중에, 필요한 만큼" 생성하게 해 주어 성능과 메모리 사용을 최적화합니다. 다만 열거마다 실행된다는 특성을 이해하고, 필요 시 물리화로 제어하는 것이 핵심입니다.
'C#' 카테고리의 다른 글
| C# AttributeTargets로 애트리뷰트 적용 범위 제어 (0) | 2026.05.29 |
|---|---|
| C# BinarySearch와 검색 알고리즘 성능 비교 (0) | 2026.05.29 |
| C# ReadOnlyMemory<T>와 메모리 안전성 확보 (0) | 2026.05.29 |
| C# IComparable과 IComparer를 이용한 정렬 로직 최적화 (0) | 2026.05.28 |
| C# ThreadStatic vs AsyncLocal 차이와 활용 (0) | 2026.05.28 |