C# 이터레이터와 LINQ 정리

공재원
∙∙조회 3

C# 이터레이터와 LINQ

1. foreach와 열거자

1-1. foreach

foreach는 배열이나 컬렉션에 저장된 요소를 하나씩 순회하는 반복문이다.

반복 횟수나 인덱스를 직접 관리하지 않아도 되며, 컬렉션의 요소를 처음부터 끝까지 순회할 수 있다.

주요 특징

  • 컬렉션의 요소를 순서대로 하나씩 가져온다.
  • 요소의 개수를 직접 관리할 필요가 없다.
  • 순회할 수 있는 객체에서 사용할 수 있다.

1-2. IEnumerable과 IEnumerator

foreach가 동작하기 위해 필요한 순회 규약이다.

  • IEnumerable은 순회할 수 있는 대상이다.
  • IEnumerator는 실제 순회를 담당하는 객체다.

주요 멤버

멤버역할
GetEnumerator()열거자를 가져온다.
MoveNext()다음 요소로 이동한다.
Current현재 요소를 가져온다.
Reset()순회 위치를 초기화한다.

동작 순서

  1. GetEnumerator()로 열거자를 가져온다.
  2. MoveNext()로 다음 요소로 이동한다.
  3. Current로 현재 요소를 가져온다.
  4. 더 이상 요소가 없을 때까지 반복한다.

1-3. 제네릭 열거자

IEnumerable<T>와 IEnumerator<T>는 요소의 자료형을 지정하는 제네릭 인터페이스다.

기존 인터페이스와 달리 요소를 object로 변환하지 않고 지정된 자료형으로 처리할 수 있다.

실제 C#에서는 제네릭 열거자를 주로 사용한다.


2. 이터레이터

2-1. 이터레이터

이터레이터는 여러 값을 하나씩 반환하는 메서드다.

일반적인 return은 값을 반환하고 메서드를 종료하지만, 이터레이터는 yield return을 사용해 값을 반환한 뒤 실행을 잠시 멈춘다.

다음 값을 요청받으면 이전에 멈춘 위치부터 다시 실행한다.

2-2. yield return

yield return은 이터레이터에서 값을 하나씩 반환하는 키워드다.

주요 특징

  • 값을 반환한 뒤 실행을 일시 중단한다.
  • 다음 값을 요청받으면 중단된 위치부터 실행한다.
  • 모든 값을 미리 생성하지 않아도 된다.
  • 필요한 만큼만 값을 생성할 수 있다.

2-3. yield break

yield break는 이터레이터의 실행을 즉시 종료한다.

조건에 따라 더 이상 값을 반환할 필요가 없을 때 사용한다.

2-4. 컬렉션 반환과 이터레이터 비교

구분컬렉션 반환이터레이터
값 생성전체 결과를 생성요청할 때 하나씩 생성
저장컬렉션에 결과 저장전체 결과를 미리 저장하지 않음
일부 사용전체 결과가 만들어질 수 있음필요한 만큼만 생성 가능
무한 수열전체 저장이 어려움필요한 만큼 생성 가능

3. 지연 실행

3-1. 지연 실행

지연 실행은 실제로 값이 필요한 시점까지 실행을 미루는 방식이다.

이터레이터 메서드는 호출하는 순간 본문이 실행되지 않는다. foreach 등으로 실제 값을 요청할 때 실행된다.

3-2. 지연 실행의 특징

  • 메서드를 호출하는 것과 실제 실행하는 시점이 다르다.
  • 원본 데이터가 변경되면 순회 결과도 달라질 수 있다.
  • 여러 번 순회하면 이터레이터가 반복 실행된다.
  • 필요한 시점에 필요한 만큼 데이터를 처리할 수 있다.

3-3. 무한 수열

이터레이터는 무한 반복을 이용해 무한 수열을 표현할 수 있다.

값을 요청하는 쪽에서 break를 사용하면 순회를 중단할 수 있다.

3-4. 지연 실행 시 주의사항

같은 이터레이터를 여러 번 순회하면 내부 계산도 반복될 수 있다.

계산량이 많거나 동일한 결과를 여러 번 사용해야 한다면 결과를 컬렉션에 저장하는 것이 좋다.


4. LINQ

4-1. LINQ의 개념

LINQ(Language Integrated Query)는 컬렉션에 저장된 데이터를 검색하고, 변환하고, 정렬하고, 집계하는 기능이다.

반복문과 조건문을 직접 작성하지 않고도 다양한 연산자를 연결해 데이터를 처리할 수 있다.

4-2. 시퀀스

시퀀스는 순서대로 열거할 수 있는 데이터의 연속이다.

IEnumerable<T>를 구현한 배열, 리스트 등의 컬렉션과 이터레이터가 반환하는 데이터가 시퀀스에 해당한다.

4-3. 주요 LINQ 연산자

분류연산자역할
필터링Where조건에 맞는 요소 선택
변환Select요소를 다른 형태로 변환
정렬OrderBy오름차순 정렬
정렬OrderByDescending내림차순 정렬
추가 정렬ThenBy추가 정렬 기준 적용
집계Count요소 개수 계산
집계Sum합계 계산
집계Average평균 계산
집계Min, Max최솟값과 최댓값 계산

4-4. 요소 선택 연산자

연산자역할
First첫 번째 요소 선택
Last마지막 요소 선택
Single요소가 정확히 하나일 때 선택
ElementAt지정한 인덱스의 요소 선택

요소가 없거나 조건을 만족하는 요소가 없을 때 예외가 발생할 수 있다.

FirstOrDefault, LastOrDefault, SingleOrDefault 등은 요소가 없을 때 기본값을 반환한다.

4-5. 범위 선택 연산자

연산자역할
Take앞에서부터 지정한 개수만큼 선택
Skip앞에서부터 지정한 개수만큼 건너뜀
TakeWhile조건이 참인 동안 선택
SkipWhile조건이 참인 동안 건너뜀

TakeWhile과 SkipWhile은 조건이 처음으로 거짓이 되는 지점에서 동작이 달라진다.

4-6. 조건 확인 연산자

연산자역할
Any하나라도 존재하는지 확인
All모든 요소가 조건을 만족하는지 확인
Contains특정 값이 포함되어 있는지 확인

Any()는 요소가 하나라도 존재하는지 확인할 때 사용한다.

Count() > 0과 달리 하나의 요소만 확인해도 결과를 알 수 있으므로 불필요한 순회를 줄일 수 있다.

빈 시퀀스에 All()을 사용하면 true를 반환한다.

4-7. 집합 연산자

연산자역할
Distinct중복 제거
Concat두 시퀀스를 연결
Union합집합
Intersect교집합
Except차집합

Concat은 중복을 유지하지만 나머지 집합 연산자는 중복을 제거한다.


5. LINQ 쿼리 구문

5-1. 쿼리 구문

쿼리 구문은 LINQ를 SQL과 비슷한 형태로 작성하는 방식이다.

주요 구성 요소

구문역할
from데이터 원본 지정
where조건 지정
orderby정렬 기준 지정
select반환할 결과 지정

기본적으로 from으로 시작하고 select로 끝난다.

5-2. 메서드 구문과 쿼리 구문

LINQ는 메서드 구문과 쿼리 구문 두 가지 방식으로 작성할 수 있다.

  • 메서드 구문은 LINQ 연산자를 연결하는 방식이다.
  • 쿼리 구문은 from, where, orderby, select 등을 사용하는 방식이다.

두 방식은 같은 결과를 표현할 수 있으며, 컴파일러가 쿼리 구문을 메서드 구문으로 변환한다.


6. LINQ의 실행 방식

6-1. 지연 실행

대부분의 LINQ 쿼리는 지연 실행 방식으로 동작한다.

쿼리를 작성하는 순간에는 실제 데이터 처리가 이루어지지 않고, foreach 등으로 결과를 요청할 때 실행된다.

6-2. 즉시 실행

즉시 실행은 연산자를 호출하는 시점에 실제 계산을 수행하는 방식이다.

주요 즉시 실행 연산자

  • 집계: Count, Sum, Average, Min, Max
  • 요소 선택: First, Last, Single, ElementAt
  • 조건 확인: Any, All, Contains
  • 컬렉션 변환: ToList, ToArray, ToDictionary

6-3. ToList와 ToArray

ToList()와 ToArray()는 지연 실행 쿼리를 즉시 실행하고 결과를 새로운 컬렉션에 저장한다.

메서드반환 자료형
ToList()List<T>
ToArray()T[]

결과를 저장하면 이후 원본 컬렉션의 요소가 추가되거나 삭제되어도 저장된 컬렉션의 요소 구성은 유지된다.

단, 참조 형식의 객체를 저장하는 경우 객체 내부의 변경까지 방지하지는 않는다.

6-4. ToList와 ToArray를 사용하는 경우

  • 동일한 쿼리를 여러 번 순회할 때
  • 계산 결과를 반복해서 사용해야 할 때
  • 특정 시점의 결과를 유지해야 할 때

한 번만 순회하고 원본이 변경되지 않는다면 지연 실행을 그대로 사용하는 것도 적절하다.


7. 전체 개념의 연결

이터레이터와 LINQ는 서로 독립적인 기능이 아니라 연결된 개념이다.

  1. IEnumerable<T>는 데이터를 순회할 수 있도록 한다.
  2. IEnumerator<T>는 실제 순회 과정을 담당한다.
  3. foreach는 열거 규약을 이용해 데이터를 순회한다.
  4. yield return은 이터레이터를 통해 데이터를 하나씩 제공한다.
  5. LINQ는 시퀀스를 대상으로 데이터를 검색하고 변환한다.
  6. LINQ의 여러 연산자는 지연 실행을 통해 필요한 시점에 데이터를 처리한다.
  7. ToList()와 ToArray()는 결과를 즉시 계산해 컬렉션에 저장한다.