[객체지향 프로그래밍] 10장 배열과 컬렉션 그리고 인덱서

0

이것이 C#이다

목록 보기
20/26

10.1 배열(Array)

프로그램을 작성 하다 보면 같은 성격을 띤 다수의 데이터를 한번에 다뤄야하는 경우가 자주 생깁니다. 배열은 마치 데이터를 담는 상자와 같아서, 필요한 용량을 가진 변수를 한개만 선언해서 사용하면 됩니다.
배열의 특징을 나열해보겠습니다.

  • 같은 형식복수 인스턴스를 저장할 수 있는 형식
  • 참조 형식으로써 연속된 메모리 공간을 가리킴
  • 반복문, 특히 for, foreach문과 함께 사용하면 효율 향상
  • 배열의 인덱스는 0부터 시작합니다.

배열은 다음과 같은 형식으로 선언합니다.

데이터형식[] 배열이름 = new 데이터형식[용량];

예를들어 용량이 5개인 int 형식의 배열에 데이터를 담는 과정은 다음과 같습니다.

int[] scores = new int[5];
scores[0] = 80;
scores[1] = 74;
scores[2] = 81;
scores[3] = 90;
scores[4] = 34;

우리는 배열에 접근할때 첫번째 요소는 0, 두번째 요소는 1, 세번째 요소는 2, 마지막 요소는 (배열의 길이 - 1) 라는 사실을 알고 있습니다. 만약 배열의 요소가 몇개인지 모르는 상황에서 마지막 요소에 접근하려면 배열의 길이를 알아올수있는 Length를 사용할 수 있습니다.

scores[scores.Length-1] = 34;

C# 8.0부터는 System.Index형식과 ^ 연산자가 생겼습니다. ^연산자는 컬렉션의 마지막부터 역순으로 인덱스를 지정하는 기능을 가지고 있습니다. ^1은 컬렉션의 마지막 요소를 나타내는 인덱스,^2는 마지막에서 두 번째, ^3은 마지막에서 세 번째를 나타내는 인덱스입니다.
^를 Length와 같다고 생각하면 Length-1 = ^1 , Length-2 = ^2 로 이해하면 쉬워집니다.

^연산자의 연산 결과는 System.Index 형식의 인스턴스로 나타납니다.
앞의 예제를 Index와 ^연산자를 이용해서 수정하면 다음과 같습니다.

System.Index last = ^1;
scores[last] = 34;		//scores[scores.Length-1] = 34;와 동일

scores[^1] = 34;		//System.Index를 만들지않는 간결한 버전

int Length = scores[^0]; //scores[^0] = scores.Length

10.2 배열을 초기화하는 방법 세 가지

첫 번째 방법은 new연산자와 데이터형식을 선언하고,배열의 용량을 명시하는 방법입니다.

//첫 번째 방법
string[] array1 = new string[3] {"안녕", "Hello", "Halo"};

두 번째 방법은 new연산자와 데이터형식을 선언하고,배열의 용량을 생략하는 방법입니다.

//두 번째 방법
string[] array2 = new string[] {"안녕", "Hello", "Halo"};	

세 번째 방법은 new연산자와 데이터형식,배열의 용량을 전부 생략하는 방법입니다.

//세 번째 방법
string[] array3 = {"안녕", "Hello", "Halo"};
//초기화 데이터 목록을 보고 컴파일러가 new연산자,데이터형식,배열의 용량을 자동으로 처리해줍니다.

이렇게 세가지 방법을 보면 세 번째 방법만 사용 할것 같지만, 동료들과 공유해야할때는 상대방이 읽기 편하도록 첫번째 방법을 쓰는것이 좋습니다. 물론 온전히 취향 문제이므로 셋중 어떤것을 쓰든 성능의 차이는 없습니다.

10.3 알아두면 좋은 System.Array

C#에서는 모든것이 객체입니다. 배열도 객체이며, 당연히 기반이 되는 형식도 있습니다.
.NET의 CTS(Common Type System)에서 배열은 System.Array 클래스에 대응됩니다.
다음 예제는 int기반의 배열이 System.Array 형식에서 파생되었음을 보여줍니다.

int[] array = new int[] { 10,30,20,7,1 };
Debug.Log($"Type Of Array : {array.GetType()}");
Debug.Log($"Base type Of Array : {array.GetType().BaseType}");

출력
Type Of Array : System.Int32[]
Base type Of Array : System.Array

따라서 System.Array의 특성과 메소드를 파악하면 배열의 특성과 메소드를 알게되는 셈이며, 보너스로 배열을 이용하여 재미있는 일들도 할 수 있습니다.
다음표는 Array클래스의 주요 메소드와 프로퍼티를 나타냅니다.

< T >는 형식 매개변수(Type Parameter)라고 하는데, 이들 메소드를 호출할 때는 T대신 배열의 기반 자료형을 인수로 입력하면 컴파일러가 해당 형식에 맞춰 동작하도록 메소드를 컴파일합니다. 자세한 내용은 일반화 프로그래밍에서 다룹니다. 다음은 예제입니다.

using System;

namespace studyC_Program
{
    internal class Program
    {
        private static bool CheckPassed(int score)
        {
            return score >= 60;
        }

        private static void Print(int value)
        {
            Console.Write($"{value} ");
        }

        static void Main(string[] args)
        {
            int[] scores = new int[] { 80, 74, 81, 90, 34 };

            foreach (int s in scores)
                Console.Write($"{s} ");
            Console.WriteLine();

            Array.Sort(scores);
            Array.ForEach<int>(scores, new Action<int>(Print));
            Console.WriteLine();

            Console.WriteLine($"Number of dimensions : {scores.Rank}");

            Console.WriteLine($"Binary Search : 81 is at " + $"{Array.BinarySearch<int>(scores, 81)}");

            Console.WriteLine($"Linear Search : 90 is at " + $"{Array.IndexOf(scores, 90)}");

            //TrueForAll 메소드는 배열과 함께 조건을 검사하는 메소드를 매개변수로 받습니다.(여기서는 CheckPassed)
            Console.WriteLine($"Everyone passed ? : " + $"{Array.TrueForAll<int>(scores, CheckPassed)}");

            //FindIndex 메소드는 특정 조건에 부합하는 메소드를 매개변수로 받습니다. 여기서는 람다식으로 구현해봤습니다.(람다식은 14장에서 소개합니다)
            int index = Array.FindIndex<int>(scores, (score => score < 60));
            scores[index] = 61;
            
            Console.WriteLine($"Everyone passed ? : " + $"{Array.TrueForAll<int>(scores, CheckPassed)}");

            //5였던 배열 용량을 10으로 재조정 합니다.
            Array.Resize<int>(ref scores, 10);
            Console.WriteLine($"New Length of scores : {scores.Length}");

            //Action대리자는 14장에서 소개합니다.
            Array.ForEach<int>(scores, new Action<int>(Print));
            Console.WriteLine();

            Array.Clear(scores, 3, 7);
            Array.ForEach<int>(scores, new Action<int>(Print));
            Console.WriteLine();

            //이 코드는 scores배열의 0번째 부터 3개 요소를 sliced 배열의 0번째~2번빼 요소에 차례대로 복사합니다.
            int[] sliced = new int[3];
            Array.Copy(scores, 0, sliced, 0, 3);
            Array.ForEach<int>(sliced, new Action<int>(Print));
            Console.WriteLine();
        }
    }
}

출력
80 74 81 90 34
34 74 80 81 90
Number of dimensions : 1
Binary Search : 81 is at 3
Linear Search : 90 is at 4
Everyone passed ? : False
Everyone passed ? : True
New Length of scores : 10
61 74 80 81 90 0 0 0 0 0
61 74 80 0 0 0 0 0 0 0
61 74 80

10.4 배열 분할하기

배열 분할에 앞서 C#8.0에서 System.Index와 같이 추가된 System.Range를 알아 보겠습니다.
System.Range는 시작 인덱스와 마지막 인덱스를 이용해서 범위를 나타냅니다. System.Range객체를 생성할 때는 다음과 같이 ..연산자를 이용합니다.
..연산자는 다음 예제 코드처럼 왼쪽에는 시작인덱스, 오른쪽에는 마지막 인덱스가 옵니다.
주의 할 점은 마지막 인덱스는 배열 분할 결과에서 제외됩니다.(시작인덱스는 포함됩니다.)

int[] scores = {80,74,81,90,34};

System.Range r1 = 0..3; //첫번째(0)부터 세 번째(2) 요소까지 
int[] sliced = scores[r1]; // []에 인덱스 대신 System.Range객체를 넣으면 분할된 배열이 반환됩니다.

//[]직접 ..연산자를 넣으면 더 간결해집니다.
int[] sliced2 = scores[0..3];

//마지막 인덱스를 생략하면 마지막 요소까지 분할합니다.
int[] sliecd3 = scores[1..];	//두번째(1)부터 마지막까지

int[] sliecd4 = scores[..];		//전체


//System.Index객체를 이용할 수도 있습니다.
System.Index idx = ^1;	//^1 = Length-1이고 Length = 5이다 따라서 ^1 = 4
int[] sliecd5 = scores[..idx]; //마지막 인덱스만 제외한 배열

int[] sliced6 = scores[..^2]; //마지막에서 첫번째, 두번째 인덱스만 제외한 배열

int[] sliced7 = scores[^4..^1]; //끝에서 4번째부터 끝에서 2번째까지

10.5 2차원 배열

  • 2개의 차원(가로 + 세로)로 요소 배치

  • 차원의 길이를 뒤에서 부터 읽어서 해석
    * 예)int[2,3] -> 길이가 3인 1차원 배열을 요소로 2개 갖고 있는 2차원 배열

  • 배열의 세가지 초기화 방법 모두 사용 가능

2차원 배열을 선언하는 방법은 다음과 같습니다.

데이터 형식[,] 배열이름 = new 데이터형식[2차원길이,1차원길이];

//2*3 크기의 int형식 2차원 배열은 다음과 같이 선언할 수 있습니다.
int[,] array = new int[2,3];
array[0,0] = 1;
array[0,1] = 2;
array[0,2] = 3;
array[1,0] = 4;
array[1,1] = 5;
array[1,2] = 6;


//2차원 배열의 초기화는 배열의 세가지 초기화 방법 모두 사용 가능합니다.
int[,] arr = new int[2,3] {{1,2,3}, {4,5,6}};
int[,] arr2 = new int[,] {{1,2,3}, {4,5,6}};
int[,] arr3 = {{1,2,3}, {4,5,6}};

10.7 가변 배열

가변 배열은 용량만 정의 하고 인덱스에 배열을 넣는 배열입니다. 예를 들면 0번째 인덱스의 배열은 길이가 3, 1번째 인덱스의 배열은 길이가 4, 2번째 인덱스의 배열은 길이가 2... 이렇게 인덱스마다 배열의 길이가 다른 배열을 요소로 갖는 배열을 말합니다.

  • 배열을 요소로 갖는 배열이다.
  • 요소로 입력되는 배열의 차원과 길이는 같아야 할 필요가 없다

가변배열의 형식은 다음과 같습니다.

데이터형식[][] 배열이름 = new데이터 형식[가변 배열의 용량][];

//선언 후 초기화
int[][] jagged = new int[3][];
jagged[0] = new int[5] { 1, 2, 3, 4, 5 };
jagged[1] = new int[] { 10, 20, 30 };
jagged[2] = new int[] { 100, 200 };

//선언과 동시에 초기화
int[][] jagged2 = new int[2][] 
{ new int[] {1000,2000},
new int[4] {6,7,8,9} };

//접근법
foreach (int[] arr in jagged)
{
    foreach (int e in arr)
    {
        Console.WriteLine(e);
    }
}

10.8 컬렉션

컬렉션이란 같은 성격을 띈 데이터의 모음을 담는 자료구조를 말합니다. 사실 배열도 .NET이 제공하는 컬렉션의 자료구도의 일부입니다.
배열말고도 다양한 자료구조를 제공하는데, 그중에서도 네 가지 클래스를 다뤄볼겁니다.
ArrayList, Queue, Stack, Hashtable인데 이 네 가지 자료구조의 특징과 사용방법 정도만 간단히 알아보겠습니다.

10.8.0 컬렉션에 다양한 형식의 객체를 담을 수 있는 이유

컬렉션의 매개변수로 다양한 형식의 객체를 담을 수 있습니다.
이유는 매개변수의 형식이 Object이기 때문인데요 int형식의 데이터를 넣더라도 정수형식 그대로 입력되는 것이 아니라 object형식으로 박싱되어 입력되는 것입니다. 박싱과 언박싱은 작지 않은 오버헤드를 요구하는 작업이므로 현재 살펴보는 컬렉션에 다루는 데이터가 많아질수록 이러한 성능 저하는 더욱 늘어납니다. 해결방법은 다음장에서 살펴볼 일반화 컬렉션에서 알 수 있습니다.

10.8.1 ArrayList

  • 배열과 같이 인덱스를 이용하여 요소에 접근 가능
  • 필요에 따라 동적으로 크기가 증가
  • 요소를 추가하는 Add(),삭제하는 RemoveAt(),리스트 중간에 요소를 삽입하는 Insert()메소드 등을 제공
void Print(ArrayList l)
{
    foreach (object obj in l)
        Console.Write($"{obj} ");
    Console.WriteLine();
}

ArrayList list = new ArrayList();
for (int i = 0; i < 5; i++)     //리스트에 0,1,2,3,4 추가	
{
    list.Add(i);
}

Print(list);

list.RemoveAt(0);               //첫번째 인덱스(0부터 시작) 삭제

Print(list);

list.Insert(0, 7);              //첫번째 인덱스에 요소 '7' 삽입

Print(list);

list.Add("abc");                //마지막 인덱스에 요소 'abc'추가
list.Add("def");                //마지막 인덱스에 요소 'def'추가

Print(list);

출력
0 1 2 3 4
1 2 3 4
7 1 2 3 4
7 1 2 3 4 abc def

10.8.2 Queue

Queue는 대기열이라는 뜻입니다. 작업을 차례대로 입력해뒀다가 순서대로 하나씩 꺼내 처리하기위해 사용됩니다.

  • 선입선출(First In First Out) 구조의 자료구조
  • 데이터를 입력하는 Enqueue(),출력하는 Dequeue()메소드를 제공
Queue que = new Queue();
que.Enqueue(1);				
que.Enqueue(2);
que.Enqueue(3);
que.Enqueue(4);
que.Enqueue(5);

while (que.Count > 0)
    Console.WriteLine(que.Dequeue());

출력
1
2
3
4
5

10.8.3 Stack

  • 선입후출(First In Last Out) 구조의 자료구조
  • 데이터를 입력하는 Push(),출력하는 Pop() 메소드 제공
Stack stack = new Stack();
            stack.Push(1);
            stack.Push(2);
            stack.Push(3);
            stack.Push(4);
            stack.Push(5);

            while (stack.Count > 0)
                Console.WriteLine(stack.Pop());

출력
5
4
3
2
1

10.8.4 Hashtable

  • 키(key)와 값(value)으로 이루어진 데이터를 다룰 때 사용
  • 키를 해싱(Hashing)해서 테이블 내의 주소를 계산
  • 배열처럼 다루기 간편하고 탐색속도도 빠름

Hashtable은 어떤 형식이든 키로 사용할 수 있고, 탐색 시간이 거의 소요되지않는 탐색속도를 자랑합니다.
ArrayList에서 원하는 데이터를 찾으려면 컬렉션을 정렬해서 이진 탐색을 수행하거나 순차적으로 리스트를 탐색해나가지만, Hashtable은 키를 이용해서 단번에 데이터가 저장된 컬렉션 내의 주소를 계산해냅니다. 이 작업을 해싱(Hashing)이라고 합니다.

Hashtable ht = new Hashtable();
ht["하나"] = "one";
ht["둘"] = "two";
ht["셋"] = "three";
ht["넷"] = "four";
ht["다섯"] = "five";

Console.WriteLine(ht["하나"]);
Console.WriteLine(ht["둘"]);
Console.WriteLine(ht["셋"]);
Console.WriteLine(ht["넷"]);
Console.WriteLine(ht["다섯"]);

출력
one
two
three
four
five

10.9 컬렉션을 초기화 하는 방법

ArrayList, Queue, Stack은 배열의 도움을 받아 간단하게 초기화를 수행할 수 있습니다.
컬렉션의 생성자를 호출할 때 배열 객체를 매개변수로 넘기면 컬렉션 객체는 해당 배열을 바탕으로 내부 데이터를 채웁니다.

int[] arr = { 123, 456, 789 };

Arraylisyt list = new ArrayList(arr);	//123, 456, 789

Queue que = new Queue(arr);				//123, 456, 789

Stack stack = new Stack(arr);			//789, 456, 123

ArrayList는 배열의 도움없이 직접 컬렉션 초기자를 이용가능합니다.

//생성자 호출시 {} 안에 요소 목록을 입력합니다.
ArrayList list2 = new ArrayList() { 11,22,33 };

(Stack과 Queue는 컬렉션 초기자를 이용할 수 없습니다.
컬렉션 초기자는 IEnumerable 인터페이스와 Add() 메소드를 수현하는 컬렉션만 지원하는데 이 두 컬렉션은 Add()메소드를 상속하지 않기 때문입니다.)

Hashtable은 딕셔너리 초기자를 이용합니다.
다음은 딕셔너리 초기자의 예제입니다.

//딕셔너리 초기자 사용
Hashtable ht = new Hashtable()
{
	["하나"] = 1,
    ["둘"] = 2,
    ["셋"] = 3,
};

10.10 인덱서

  • 인덱스(Index)를 이용해서 객체 내의 데이터에 접근 하게 해주는 프로퍼티

인덱서는 객체를 마치 배열처럼 사용할 수 있게 해줍니다.
인덱서를 선언하는 형식은 다음과 같습니다.

class 클래스이름
{
	한정자 인덱서형식 this[형식 index]
    {
    	get
        {
        	// index를 이용하여 내부 데이터 반환
        }
        set
        {
        	//index를 이용해서 내부데이터 저장
        }
    }
}

프로퍼티이름을 통해 객체에 접근하게 해준다면,
인덱서인덱스를 통해 객체에 접근하게 해줍니다.
둘다 같은 일을 하지만 인덱스를 이용하느냐 이름을 이용하느냐의 차이입니다.

다음예제의 MyList는 내부에 정수 형식 배열을 갖고 있고,
인덱서를 통해 이 배열에 접근합니다.
인덱서를 통해 데이터를 저장하고자 하는 시도가 이루어질때 지정한 인덱스보다 배열의 크기가 작다면 인덱스에 맞춰 배열의 크기를 재조정합니다.

class Mylist
{
	private int[] array;
    
    public MyList()
    {
    	array = new int[3]; //크기가 3인 배열 인스턴스 생성
    }
    
    public int this[int index]
    {
    	get
        {
        	return array[index];
        }
        set
        {
        	//저장하려는 인덱스보다 배열의 크기가 작을 경우
        	if(index >= array.Length)
            {
            	//배열의 크기를 재조정합니다.
            	Array.Resize<int>(ref array, index + 1);
                Console.WriteLine($"Array Resized : {array.Length}");
            }
            
            array[index] = value;
        }
    }
    
    public int Length
    {
    	get { return array.Length; }
    }
}

class MainApp
{
	static void Main(string[] args)
    {
    	MyList list = new MyList();
        for (int i = 0; i < 5; i++)
        	list[i] = i;	//배열을 다루듯 인덱스를 통해 데이터를 입력합니다.
            
        for (int i = 0; i < list.Length; i++)
        {
        	//데이터를 얻어올때도 인덱스를 이용합니다.
        	Console.WriteLine(list[i]);		
        }
        
    }
}

10.11 foreach가 가능한 객체 만들기

foreach문은 가독성이 좋지만 배열이나 리스트 같은 한정된 컬렉션에서만 사용할 수 있습니다.
foreach문이 객체 내의 요소를 순회하기 위해서는 IEnumerable 인터페이스를 상속해야 합니다.

이 말은 즉 사용자가 만든 MyList같은 클래스도 IEnumerable를 상속한다면 foreach문에 사용 가능하다는 이야기입니다.

IEnumerable를 상속하여 만들어야 하는 메소드는
IEnumerator GetEnumerator() 메소드 1개입니다.

GetEnumerator()는 IEnumerator 인터페이스를 상속하는 클래스의 객체를 반환해야 하는데요.

yield문을 사용하는 방법직접 인터페이스를 상속하는 방법이 있습니다.

10.11.1 yield문 이해하기

C#의 yield 키워드는 호출자(Caller)에게 컬렉션 데이터를 하나씩 리턴할 때 사용합니다.
흔히 Enumerator(Iterator)라고 불리우는 이러한 기능은 집합적인 데이터셋으로부터 데이터를 하나씩 호출자에게 보내주는 역할을 합니다.

yield는 yield return 또는 yield break의 2가지 방식으로 사용되는데,

  • yield return은 컬렉션 데이터를 하나씩 리턴하는데 사용합니다.
  • yield break는 리턴을 중지하고 Iteration 루프를 빠져 나올 때 사용합니다.

yield return문은 현재 메소드(예제 GetEnumerator())의 실행을 일시정지 시켜놓고 호출자에게 결과를 반환합니다.

메소드가 다시 호출되면, 일시정지된 실행을 복구하여 yiled return 또는 yeild break문을 만날때까지 나머지 작업을 실행하게 됩니다.

아래는 yiled의 예제 프로그램입니다.

using System.Collections;

namespace Yield
{
    internal class MyEunmerator
    {
        int[] numbers = { 1, 2, 3, 4 };
        public IEnumerator GetEnumerator()
        {
            yield return numbers[0];    //첫번째 루프에서 리턴하는 값
            yield return numbers[1];    //두번째 루프에서 리턴하는 값
            yield return numbers[2];    //세번째 루프에서 리턴하는 값
            yield break;                //리턴 중지, IEnumerator을 빠져나옴
            yield return numbers[3];    //이 코드는 실행되지 않습니다.
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            var obj = new MyEunmerator();
            foreach (int i in obj)
            {
                Console.WriteLine(i);
            }
        }
    }
}

이러한 특별한 리턴 방식은 다음과 같은 경우에 유용하게 사용됩니다.

(1) 만약 데이터의 양이 커서 모든 데이터를 한꺼번에 리턴하는 것하는 것 보다 조금씩 리턴하는 것이 더 효율적일 경우. 예를 들어, 어떤 검색에서 1만 개의 자료가 존재하는데, UI에서 10개씩만 On Demand로 표시해 주는게 좋을 수도 있다. 즉, 사용자가 20개를 원할 지, 1000개를 원할 지 모르기 때문에, 일종의 지연 실행(Lazy Operation)을 수행하는 것이 나을 수 있다.

(2) 어떤 메서드가 무제한의 데이터를 리턴할 경우. 예를 들어, 랜덤 숫자를 무제한 계속 리턴하는 함수는 결국 전체 리스트를 리턴할 수 없기 때문에 yield 를 사용해서 구현하게 된다.

(3) 모든 데이터를 미리 계산하면 속도가 느려서 그때 그때 On Demand로 처리하는 것이 좋은 경우. 예를 들어 소수(Prime Number)를 계속 리턴하는 함수의 경우, 소수 전체를 구하면 (물론 무제한의 데이터를 리턴하는 경우이기도 하지만) 시간상 많은 계산 시간이 소요되므로 다음 소수만 리턴하는 함수를 만들어 소요 시간을 분산하는 지연 계산(Lazy Calculation)을 구현할 수 있다.

10.11.2 yield문을 이용한 방법

C#에서 yield 가 자주 사용되는 곳은 집합적 데이터를 가지고 있는 컬렉션 클래스입니다.
일반적으로 컬렉션 클래스는 데이타 요소를 하나 하나 사용하기 위해 흔히 Enumerator(Iterator) 를 구현하는 경우가 많은데, Enumerator를 구현하는 한 방법으로 yield 를 사용할 수 있습니다.

컬렉션 타입 혹은 Enumerable 클래스에서 GetEnumerator() 메서드를 구현하는 한 방법으로 yield 를 사용할 수 있습니다.
즉, GetEnumerator() 메서드에서 yield return를 사용하여 컬렉션 데이터를 순차적으로 하나씩 넘겨주는 코드를 구현하고, 리턴타입으로 IEnumerator 인터페이스를 리턴할 수 있습니다.
C#에서 Iterator 방식으로 yield 를 사용하면, 명시적으로 별도의 Enumerator 클래스를 작성할 필요가 없습니다.

즉, yield 문을 이용하면 IEnumerator를 상속하는 클래스를 따로 구현하지 않아도 컴파일러가 자동으로 해당 인터페이스를 구현한 클래스를 생성해줍니다.

아래의 예제는 MyList라는 컬렉션 타입에 있는 데이터를 하나씩 리턴하는 GetEnumerator() 메서드의 샘플코드이다. 예제의 GetEnumerator() 메서드는 데이터를 하나씩 리턴하기 위해 yield return문을 while 루프 안에서 사용하고 있다. 클래스 안의 샘플 data는 1부터 5까지 숫자인데, 외부 호출자가 순차적으로 호출하면 yield return에서 하나씩 리턴한다. 예를 들어, 처음에는 1을 리턴하고, 다음에는 2를, 그 다음에는 3을 리턴한다

호출자(Caller)가 이 메서드를 사용하는 방법은
(1) foreach 문을 사용하여 C#에서 자동으로 Iterator 루프 처리를 하게 하는 방법과
(2) GetEnumerator()로부터 IEnumerator 인터페이스를 얻어 MoveNext() 메서드와 Current 속성을 사용하여 개발자가 직접 수동으로 요소를 하나씩 사용하는 방법이 있다. 일반적으로 그 편리성 때문에 foreach 문을 사용하는 방식을 사용한다.

using System;
using System.Collections;

public class MyList
{
    private int[] data = { 1, 2, 3, 4, 5 };
    
    public IEnumerator GetEnumerator()
    {
        int i = 0;
        while (i < data.Length)
        {
            yield return data[i];
            i++;                
        }
    }

    //...
}

class Program
{
    static void Main(string[] args)
    {
        // (1) foreach 사용하여 Iteration
        var list = new MyList();

        foreach (var item in list)  
        {
            Console.WriteLine(item);
        }

        // (2) 수동 Iteration
        IEnumerator it = list.GetEnumerator();
        it.MoveNext();
        Console.WriteLine(it.Current);  // 1
        it.MoveNext();
        Console.WriteLine(it.Current);  // 2
    }
}

10.11.3 직접 인터페이스를 상속하는 방법

using System.Collections;

namespace Enumerable
{
    class MyList : IEnumerable, IEnumerator
    {
        private int[] array;
        int position = -1;

        public MyList()
        {
            array = new int[3];
        }

        public int this[int index]
        {
            get
            {
                return array[index];
            }
            set 
            {
                if (index >= array.Length)
                {
                    Array.Resize<int>(ref array, index + 1);
                    Console.WriteLine($"Array Resized : {array.Length}");
                }
                array[index] = value;
            }
        }

        //IEnumerator 멤버
        //IEnumerator로 부터 상속받은 Current프로퍼티는
        //현재 위치의 요소를 반환합니다.
        public object Current
        {
            get { return array[position]; }
        }

        //IEnumerable 멤버
        //IEnumerable로부터 상속받은 MoveNext() 메소드는
        //다음 위치의 요소로 이동합니다.
        public bool MoveNext()
        {
            if (position == array.Length - 1)
            {
                Reset();
                return false;
            }

            position++;
            return (position < array.Length);
        }

        //IEnumerator의 멤버
        //IEnumerator로부터 상속받은 Reset()메소드는
        //요소의 위치를 첫 요소 앞으로 옮깁니다.
        public void Reset()
        {
            position = -1;
        }

        //IEnumerable의 멤버
        public IEnumerator GetEnumerator() 
        {
            return this;
        }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            MyList list = new MyList();
            for (int i = 0; i < 5; i++)
            {
                list[i] = i;
            }

            foreach (int e in list)
            {
                Console.WriteLine(e);
            }
        }
    }
}

0개의 댓글