인덱서(Indexer)와 반복기(Iterator)
인덱서(indexer)는 클래스의 인스턴스를 배열처럼 사용할 수 있도록 해주는 구문으로, 속성의 확장 모양이며 배열 형식으로 속성들을 초기화하거나 값을 가져갈 수 있는 기능을 제공합니다. 속성 여러 개로 사용할 만한 부분은 인덱서 하나로 처리할 수도 있습니다. 반복기(iterator)는 컬렉션의 항목을 단계별로 실행하는 데 사용되는 구문으로, 클래스의 특정 메서드 반환값을 단계별로 하나씩 가져가는 개념입니다. 이번 강의에서는 C#의 편리한 기능들인 인덱서와 반복기 개념을 학습하겠습니다.
> // 인덱서: 클래스의 인스턴스를 배열처럼 사용할 수 있도록 해주는 구문
> // 반복기: 컬렉션의 항목을 단계별로 실행하는 데 사용되는 구문
인덱서(Indexer)
C#에서 인덱서(indexer)는 속성 여러 개를 하나로 표현하거나 개체를 배열 형식으로 표현하고자 할 때 사용합니다. 배열의 인덱스 접근 방식인 개체이름[0]
, 개체이름[1]
식으로 개체의 속성 또는 멤버에 접근할 수 있게 합니다. 자동차 개체를 예로 든다면 자동차 카탈로그(광고지)와 같이 자동차에 대한 인덱스(목차)를 표현하는 방법으로 볼 수 있습니다.
인덱서 코드 조각
인덱서를 만들어내는 코드 조각은 특정 클래스 내에서 indexer
를 입력한 후 탭을 두 번 누르면 됩니다. 그러면 자동으로 다음 코드와 같은 인덱서에 대한 기본 뼈대 코드를 생성해 줍니다.
public object this[int index]
{
get { /* return the specified index here */ }
set { /* set the specified index to value here */ }
}
정수형 인덱서 만들기
우선 get
키워드만 사용하는 정수형 인덱서를 만들어보겠습니다.
코드: IndexerNote.cs
using System;
class Catalog
{
//[1] 정수형 인덱서: this[int index] 형태로 정의되어 개체명[0], 개체명[1] 형태로 호출됨
public string this[int index]
{
get
{
return (index % 2 == 0) ? $"{index}: 짝수 반환" : $"{index}: 홀수 반환";
}
}
}
class IndexerNote
{
static void Main()
{
Catalog catalog = new Catalog();
Console.WriteLine(catalog[0]); //[2] 개체명[인덱스] 형태로 호출 가능
Console.WriteLine(catalog[1]);
Console.WriteLine(catalog[2]);
}
}
0: 짝수 반환
1: 홀수 반환
2: 짝수 반환
인덱서는 속성과 달리 이름을 따로 지정하지 않고 this
키워드를 사용합니다. 그리고 매개 변수로 배열 형식을 받습니다. [1]
번 코드 영역에서 넘어온 매개 변수의 값이 짝수 또는 홀수일 때마다 매개 변수의 값과 함께 짝수 또는 홀수 값을 반환합니다. 따로 set
키워드를 사용하여 값을 설정하지 않았기에 읽기 전용 인덱서입니다.
[2]
번 코드 영역을 보면 알 수 있듯이 클래스에 인덱서를 만들어놓으면 개체명[인덱스]
형태로 값을 호출할 수 있습니다.
인덱서를 사용하여 여러 값을 주고 받기
속성처럼 값을 입력 받고 출력해주는 인덱서를 만들어보겠습니다.
코드: Indexer.cs
> //[?] 인덱서를 사용하여 여러 값을 주고 받기
> class Developer
. {
. private string name;
. // 인덱서
. public string this[int index]
. {
. get { return name; } // [index]로 요청시 특정 필드의 값을 반환한다.
. set { name = value; } // 넘어온 값은 value 키워드로 읽어올 수 있다.
. }
. }
>
> var developers = new Developer();
> developers[0] = "홍길동"; // 인덱스와 상관없이 name 필드에 문자열이 저장된다.
> Console.WriteLine(developers[0]); // 홍길동
홍길동
> developers[1] = "백두산";
> Console.WriteLine(developers[1]); // 백두산
백두산
인덱서는 속성과 동일하게 세터로 값을 입력 받고 게터로 값을 반환합니다. 위 예제에서는 단일 변수인 name
필드만 사용하였지만 일반적으로 인덱서는 배열 또는 컬렉션과 함께 사용됩니다.
인덱서를 사용하여 배열 형식의 개체 만들기
개체를 사용함에 있어서 배열 형식의 필드에 접근하고자 할 때 좀 더 효율적으로 접근할 수 있는 개념인 인덱서(Indexer)를 사용하여 클래스의 멤버를 인덱스(Index)를 사용해 접근해 보는 과정을 살펴보겠습니다.
코드: IndexerDemo.cs
> public class Car
. {
. //[1] 필드: 배열 형식 필드
. private string[] names;
. //[2] 생성자: 생성자 매개변수로 필드의 요소수 생성
. public Car(int length)
. {
. names = new string[length]; // 넘겨온 길이만큼 문자열 배열 생성
. }
. //[3] 속성: 읽기전용 속성(get 접근자만 사용하여 읽기전용 속성 구현)
. public int Length
. {
. get
. {
. return names.Length;
. }
. }
. //[4] 인덱서: this[] 키워드를 사용한 속성의 확장(배열) 형식
. public string this[int index]
. {
. get
. {
. return names[index];
. }
. set
. {
. names[index] = value;
. }
. }
. }
>
> // 자동차 클래스의 인스턴스 생성시 생성자의 매개변수로 배열의 크기 전달
> Car car = new Car(3);
>
> // Car 클래스에는 인덱서가 구현되어 있기에 개체를 배열형으로 접근 가능
> car[0] = "CLA";
> car[1] = "CLS";
> car[2] = "AMG";
>
> // 자동차 목록 출력: for문을 통해서 개체의 값을 출력 가능
> for (int i = 0; i < car.Length; i++)
. {
. Console.WriteLine("{0}", car[i]);
. }
CLA
CLS
AMG
인덱서는 속성의 확장형입니다. 즉, 속성은 하나의 값을 저장하는 반면 인덱서는 속성에 배열형을 적용하여 여러 형태로 보여지는 C#의 문법입니다.
배열 형식의 필드를 사용하는 인덱서
요일 정보를 담아 놓을 수 있는 형태의 개체를 만들어보겠습니다.
코드: WeekDemo.cs
> /// <summary>
. /// 요일 이름을 저장해 놓는 클래스
. /// </summary>
. public class Week
. {
. //[1] 필드: 요일 문자열을 담아 놓을 수 있는 문자열 배열
. private string[] _week;
. //[2][1] 생성자: 매개 변수가 없는 생성자
. public Week()
. {
. Length = 7; // 기본값 초기화
. _week = new string[Length]; // 7개의 요소를 갖는 배열 생성
. }
. //[2][2] 생성자: 매개 변수가 있는 생성자
. public Week(int length)
. {
. Length = length;
. _week = new string[Length];
. }
. //[3] 속성
. public int Length { get; }
. //[4] 인덱서: 개체를 배열 형태로 사용하도록 this[] 형태의 인덱서 생성
. public string this[int index]
. {
. get { return _week[index]; }
. set { _week[index] = value; }
. }
. }
>
> //[A] 배열 형식 생성
> Week week = new Week(3);
>
> //[B] 인덱서로 문자열 값을 초기화
> week[0] = "일요일";
> week[1] = "월요일";
> week[2] = "화요일";
>
> //[C] 출력: 인덱서로 배열 형식의 필드값 출력
> for (int i = 0; i < week.Length; i++)
. {
. Console.WriteLine($"{week[i]}");
. }
일요일
월요일
화요일
[4]
번 코드처럼 public string this[int index]
형태로 정수 형식의 매개 변수를 받고 그에 해당하는 문자열 값을 반환해주는 인덱서를 생성할 수 있습니다. 이렇게 생성된 인덱서는 [B]
번 코드에서 값을 대입하면 인덱서의 setter가 실행되고 [C]
번 코드에서 출력할 때 사용되면 인덱서의 getter가 실행됩니다.
문자열 매개 변수를 받는 인덱서 사용하기
키와 값의 쌍으로 데이터를 저장해 놓는 기능을 인덱서로 만들어 보겠습니다. 프로젝트에 클래스 파일은 NickName.cs와 NickNameDemo.cs 파일 두 개를 생성합니다.
코드: NickName.cs
using System.Collections;
namespace NickNameDemo
{
public class NickName
{
//[1] 필드: 해시테이블 형식의 필드 생성
private Hashtable _names = new Hashtable();
//[2] 인덱서: 문자열 매개 변수를 받고 문자열 값을 반환
public string this[string key]
{
get { return _names[key].ToString(); }
set { _names[key] = value; }
}
}
}
코드: NickNameDemo.cs
using System;
namespace NickNameDemo
{
class NickNameDemo
{
static void Main()
{
//[A] NickName 클래스의 인스턴스(개체) 생성
var nick = new NickName();
//[B] 문자열 인덱서 사용
nick["박용준"] = "RedPlus"; // Key와 Value 형태로 저장
nick["김태영"] = "Taeyo";
//[C] 문자열 인덱서 값 출력
Console.WriteLine($"{nick["박용준"]}, {nick["김태영"]}");
}
}
}
RedPlus, Taeyo
[B]
번 코드처럼 문자열 키와 값의 쌍으로 데이터를 저장할 때에는 Hashtable
클래스 또는 Dictionary
클래스를 사용하면 편합니다.
문자열 매개 변수를 받는 인덱서는 [2]
번 코드처럼 public string this[string key]
형태로 문자열 key와 문자열 반환 값을 받을 수 있습니다.
반복기와 yield
키워드
반복기(Iterator; 이터레이터)는 배열과 컬렉션 형태의 데이터를 단계별로 실행하는 데 사용할 수 있습니다. 반복기를 구현할 때에는 IEnumerable
인터페이스(또는 IEnumerable<T>
인터페이스)와 yield
키워드를 사용합니다.
참고로, 이 강의에서는 반복기(Iterator) 용어는 이터레이터와 혼용해서 사용하겠습니다.
yield return
을 사용하여 이터레이터 구현하기
우선 yield
키워드로 반복해서 값을 반환해주는 이터레이터를 만들어 보겠습니다. 이번 예제는 반드시 디버거의 F11번을 여러 번 눌러가면서 디버깅 모드로 테스트를 권장합니다.
코드: YieldReturn.cs
//[?] yield return을 사용하여 이터레이터 구현하기
using System;
using System.Collections;
class YieldReturn
{
//[1] 반복기(이터레이터) 구현: MultiData() 메서드는 3번 반복해서 문자열이 반환됨
static IEnumerable MultiData()
{
yield return "Hello";
yield return "World";
yield return "C#";
}
static void Main()
{
//[2] 반복기를 `foreach` 문으로 호출해서 사용
foreach (var item in MultiData())
{
Console.WriteLine(item);
}
}
}
Hello
World
C#
[1]
번 코드의 MultiData()
메서드는 yield return
구문으로 3번 문자열을 반환합니다. 이러한 yield return
구문은 IEnumerable
인터페이스 형식으로 반환이됩니다. 일반적으로 반복기를 만드는 공식과 같은 코드 모양입니다. 위 예제에서는 yield return
문을 3번 사용했지만 반복기를 만들 때에는 반복문으로 yield return
문을 감싸서 만드는게 일반적입니다.
반복기가 정의되었다면 [2]
번 코드에서처럼 foreach
문을 통해서 반복해서 반복기를 호출해서 반환된 값을 사용할 수 있습니다. 여기서는 3번의 반복으로 문자열이 반환됩니다.
반복기 코드에 for
루프 사용해보기
반복기 구현에 for
문을 사용한 예제를 살펴보겠습니다.
코드: YieldDemo.cs
using System;
using System.Collections;
class YieldDemo
{
//[1] yield 키워드를 사용하여 데이터를 단계별로 반환: 1부터 5까지 반복해서 반환
static IEnumerable GetNumbers()
{
yield return 1; // 각각 따로따로 호출 가능
yield return 2;
for (int i = 3; i <= 5; i++)
{
yield return i; // 반복해서 호출 가능
}
}
static void Main()
{
//[2] IEnumerable 반환값을 갖는 반복기는 `foreach` 문으로 호출해서 반복 사용
foreach (int num in GetNumbers())
{
Console.Write($"{num}\t");
}
Console.WriteLine();
}
}
1 2 3 4 5
[1]
번 코드에서 GetNumbers()
메서드 구현시 yield return
코드를 직접 여러 번 사용하거나 for
문에서 반복해서 호출할 수 있습니다. GetNumbers()
메서드는 1부터 5까지 반복해서 출력하는 기능을 가집니다.
[2]
번 코드에서처럼 IEnumerable
반환값을 가지는 반복기는 foreach
문으로 반복 호출하여 사용이 가능합니다.
반복기는(Iterator)는 내가 만들어 놓은 클래스 및 개체의 멤버를 호출할 때 foreach
문을 사용하여 반복 출력되도록 설정해 놓는 구문을 말합니다.
클래스의 인스턴스, 즉 개체의 메서드를 foreach
문을 사용하여 배열 형식의 필드에 접근 가능하도록 해주는 기능을 추가하고자 할 때 사용되는 메서드가 바로 반복기(이터레이터)입니다.
노트: 느긋한 계산법(Lazy Evaluation)
이터레이터를 사용하면 스트림 형태의 데이터에 대한 지연된 계산법을 제공합니다. 지연된 계산법은 요청이 필요할 때만 실행하는 것을 의미합니다. 코드에서는 foreach
문의 각 요청에 따라서 하나의 yield
문이 실행됨을 의미합니다.
이터레이터를 사용하여 배열의 값을 foreach
문을 사용하여 출력하기
반복기(이터레이터)를 사용하여 특정 개체의 데이터를 foreach
문을 사용하여 편리하게 출력해 보겠습니다.
코드: FavoriteLanguage.cs
> //[?] 이터레이터(반복기; Iterator): 개체의 값들을 `foreach` 문으로 반복해서 사용
> using System.Collections;
>
> public class Language
. {
. //[1] 필드
. private string[] languages;
. //[2] 생성자
. public Language(int length)
. {
. languages = new string[length];
. }
. //[3] 인덱서
. public string this[int index]
. {
. get { return languages[index]; }
. set { languages[index] = value; }
. }
. //[4] 반복기(이터레이터)
. public IEnumerator GetEnumerator()
. {
. for (int i = 0; i < languages.Length; i++)
. {
. yield return languages[i];
. }
. }
. }
>
> //[A] 클래스의 인스턴스 생성
> var language = new Language(2);
>
> //[B] 정수 형식의 인덱서로 문자열 값 저장
> language[0] = "C#";
> language[1] = "TypeScript";
>
> //[C] `foreach` 문을 사용하여 배열의 값을 출력
> foreach (var lang in language)
. {
. Console.WriteLine(lang);
. }
C#
TypeScript
[4]
번 코드와 같이 System.Collections
네임스페이스의 IEnumerator
인터페이스를 사용하여 GetEnumerator()
메서드를 구현하면 [C]
번 코드와 같이 해당 클래스의 인스턴스를 foreach
문으로 접근하여 반복해서 사용할 수 있는 편리함을 제공할 수 있습니다.
IEnumerable<T>
형태로 컬렉션 형태의 데이터 반환받기
IEnumerable<int>
, IEnumerable<string>
형태의 IEnumerable<T>
제네릭 인터페이스를 사용하면 메서드 구현시 컬렉션 형태의 반환값을 받고 이를 foreach
문을 통해 접근해서 사용할 수 있습니다.
배열에 저장된 값 중에서 특정 값보다 큰 데이터만 가져올 때 yield return
을 사용한 것과 그렇지 않은 것과의 코드를 비교해 보겠습니다. 어느 것이 더 좋다고 할 수 없지만 yield return
을 사용하면 추가적인 List<T>
형태의 컬렉션 클래스가 없이도 구현이 가능한 장점이 있습니다.
코드: YieldDescription.cs
> using System.Collections.Generic;
>
> //[1] yield 사용 전: List<T> 형태의 컬렉션 클래스를 임시로 사용하여 결괏값 저장 후 반환
> static IEnumerable<int> Greater1(int[] numbers, int greater)
. {
. List<int> temp = new List<int>();
. foreach (var n in numbers)
. {
. if (n > greater)
. {
. temp.Add(n);
. }
. }
. return temp;
. }
>
> //[2] yield 사용 후: 추가적인 클래스 사용없이 여러 데이터를 yield return으로 반환
> static IEnumerable<int> Greater2(int[] numbers, int greater)
. {
. foreach (var n in numbers)
. {
. if (n > greater)
. {
. yield return n;
. }
. }
. }
>
> int[] numbers = { 1, 2, 3, 4, 5 };
>
> foreach (var n in Greater1(numbers, 3))
. {
. Console.WriteLine(n);
. }
4
5
>
> foreach (var n in Greater2(numbers, 3))
. {
. Console.WriteLine(n);
. }
4
5
[1]
번 코드는 List<T>
제네릭 클래스를 사용하여 여러 개의 데이터를 담아서 한번에 반환하는 방식이고 [2]
번 코드는 [1]
번과 동일한 기능을 하지만 추가적인 제네릭 클래스를 사용하지 않고 바로 yield return
코드로 여러 데이터를 반환하는 예를 보여줍니다. 이처럼 yield return
을 사용하면 반복해서 처리해야 하는 데이터를 반환할 때 유용하게 사용될 수 있습니다.
IEnumerable<T>
의 MoveNext()
메서드와 Current 속성
IEnumerable<T>
형태는 내부적으로 MoveNext()
메서드와 Current
속성이 함께 사용됩니다. C# 인터렉티브에서 다음 코드를 순서대로 실행해보세요.
(1) 3개의 정수 데이터를 반환하는 GetNumbers()
함수를 생성합니다.
> IEnumerable<int> GetNumbers()
. {
. yield return 1;
. yield return 3;
. yield return 5;
. }
(2) GetEnumerator()
메서드의 결괏값을 nums 변수에 담고 전체를 출력해보면 3개의 데이터가 표시됩니다.
> var nums = GetNumbers().GetEnumerator();
> nums
GetNumbers { 1, 3, 5 }
(3) MoveNext()
메서드를 호출하면 데이터를 하나씩 선택한 후 Current
속성으로 현재 값을 가져옵니다.
> nums.MoveNext();
> nums.Current
1
(4) MoveNext()
메서드의 결과는 다음 값이 있으면 true
이고 없으면 false
를 반환합니다.
> nums.MoveNext()
true
> nums.Current
3
(5) MoveNext()
에 의해서 다음 값이 없으면 false
를 반환하고 Current
속성은 마지막 데이터를 나타냅니다.
> nums.MoveNext();
> nums.Current
5
> nums.MoveNext()
false
> nums.Current
5
참고: 이터레이터를 사용한 피보나치수열 출력하기
아래 샘플 코드는 이터레이터를 사용하여 피보나치수열을 7개 정도 출력하는 내용입니다.
코드: IteratorFibonacci.cs
> static IEnumerable<int> GetFibonacci()
. {
. int current = 1;
. int next = 1;
. yield return current;
. while (true)
. {
. int temp = current + next;
. current = next;
. next = temp;
. yield return current;
. }
. }
> int count = 7;
> int i = 0;
> foreach (var f in GetFibonacci())
. {
. Console.WriteLine(f);
. if (++i > count)
. {
. break;
. }
. }
1
1
2
3
5
8
13
21
장 요약
인덱서와 반복기는 클래스의 인스턴스인 개체에 컬렉션의 개념을 도입했습니다. 개체에 배열처럼 인덱스를 사용하여 값을 저장하고 개체의 값을 foreach
문으로 하나씩 가져다 사용할 수 있는 편리한 기능을 제공해줍니다.