인덱서(Indexer)와 반복기(Iterator)

  • 22 minutes to read

인덱서(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.csNickNameDemo.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 문으로 하나씩 가져다 사용할 수 있는 편리한 기능을 제공해줍니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com