[C# Basic] 배열, 컬렉션, 인덱서

업데이트:

이번에는 데이터를 다루기 편하게 만들어주는 C#의 배열과 컬렉션, 인덱서에 대해 알아보자.


* 배열의 선언
데이터형식[] 배열이름 = new 데이터형식[용량];  // 선언방법

string[] array = new string[5];  // 선언예제
array[0] = "0번";
Console.WriteLine(array[0]);  // 0번 출력
  • 같은 데이터 타입의 여러 변수를 한 곳에 담기 위해 배열을 사용할 수 있다.
  • for 문이나 foreach 문을 이용해 더욱 간결한 소스코드를 만들 수 있다.
* 배열의 초기화
string[] array = new string[3]{"Bono", "Porori", "Hello"};  // 1. 컬렉션초기자를 이용한 초기화
string[] array2 = new string[]{"Bono", "Porori", "Hello"};  // 2. 배열의 용량을 생략
string[] array3 = {"Bono", "Porori", "Hello"};  // 3. 할당할 데이터만 입력
  • 위와 같이 초기화 방법은 세가지로 볼 수 있다.
  • 세번째 방법이 가장 간단해 보이지만 첫번째 방법이 가독성이 제일 좋다. 코드는 남이 읽기 쉽게 쓰는것이 좋다.
* System.Array
  • 배열은 System.Array 형식에서 파생된다. 따라서 System.Array의 특성과 메소드를 파악하면 배열을 이용한 많은 것을 할 수 있다.
  • 예를 들어 몇가지만 살펴보자면,
    • Sort() : 배열을 정렬한다
    • IndexOf() : 배열에서 찾고자 하는 특정 데이터의 인덱스를 반환한다.
    • Length : 배열의 길이를 반환한다.
* 2차원배열, 다차원배열
int[ , ] array = new int [1, 2];  // 2차원배열
array[0,0] = 1;
array[0,1] = 2;
int[ , ] array2 = new int[1, 2]1  // 선언과 동시에 초기화
  • 2차원 배열은 위와 같이 사용할 수 있다. 표 형태로 생각하면 이해하기 쉽다.
  • 2차원 배열 외에도 3차원, 4차원 등의 다차원 배열이 있다. 2차원배열도 읽기 쉬운편은 아니다. 꼭 사용해야 하는 경우라면 2차원배열까지만 사용하고 그 이상의 배열은 모르는편이 낫다.
* 가변배열
int[][] array = new int[3][];
array[0] = new int[5]{1, 2, 3, 4, 5};
array[1] = new int[3]{1, 2, 3};
array[2] = new int[1]{1};
  • 2차원 배열과 비슷하지만 가변배열은 길이가 서로 다를 수 있다는점이 다르다.
  • 위 예제에서는 0번 요소에는 길이가 5인 배열, 1번 요소에는 길이가 3인 배열, 2번 요소에는 길이가 1인 배열이 할당되었다.
* Collection
  • 컬렉션이란 같은 성격의 데이터를 담는 자료 구조를 말한다.
  • 배열도 컬렉션의 일부에 해당된다.
  • .NET 프레임워크의 대표적인 컬렉션 클래스는 ArrayList, Queue, Stack, Hashtable 이 있다.
* ArrayList
ArrayList list = new ArrayList();
list.Add(10);  // 리스트에 요소 추가.  서로 다른 데이터타입이 올 수 있다.
list.Add("Bono");
list.Add(true);

list.RemoveAt(1);  // "Bono" 삭제

list.Insert(1, "Porori");  // 1번 인덱스에 "Porori" 삽입.
  • ArrayList는 배열과 가장 유사한 컬렉션이다.
  • ArrayList는 배열과 달리 용량을 지정할 필요 없이 필요에 따라 자동으로 용량이 늘어나거나 줄어든다.
  • Add() 메소드는 컬렉션의 가장 마지막 요소 뒤에 새 요소를 추가한다
  • RemoveAt() 메소드는 특정 인덱스의 요소를 삭제한다
  • Insert() 메소드는 특정 인덱스에 새로운 요소를 삽입한다.
  • 각 요소는 데이터타입이 모두 달라도 된다.
  • ArrayList의 요소의 데이터타입이 달라도 되는 이유는 각 메소드들이 object 형식의 매개변수를 받고 있기 때문이다. 하지만 이로 인해 불필요한 박싱/언박싱이 일어나면서 성능에는 좋지 않은 영향을 준다. ArrayList가 다루는 데이터가 많아지면 성능 저하는 더 많이 생길 것이다. 이는 바로 뒤에서 살펴볼 Queue, Stack, Hashtable에서도 동일하게 발생하는 문제이다. 이러한 문제를 해결하기 위해 일반화 컬렉션이 존재한다. 이는 다음 장에서 살펴보자.
* Queue
Queue que = new Queue();
que.Enqueue(1);
que.Enqueue("2");

object a = que.Dequeue();  // 가장 앞 요소 1을 꺼낸다
  • 선입선출법에 해당하는 자료구조이다. 입력은 가장 뒤에서, 출력은 가장 앞에서만 이루어진다.
  • Enqueue() 메소드는 가장 뒤에 데이터를 입력한다
  • Dequeue() 메소드는 가장 앞 요소를 반환한다
* Stack
Stack stack = new Stack();
stack.Push(1);
stack.Push(2);

int a = stack.Pop();  // 가장 뒤 요소 2를 꺼낸다
  • Queue와는 반대로 먼저 들어온 데이터가 나중에 나가는 구조이다. 후입선출 방식에 해당된다.
  • Push() 메소드는 데이터를 가장 뒤에 입력한다
  • Pop() 메소드는 가장 뒤에 있는 데이터를 반환한다
* Hashtable
Hashtable hash = new Hashtable();
hash["Name"] = "Bono";  // Name 이라는 Key 에 Bono 라는 Value 할당
hash["Age"] = 15;

Console.WriteLine(hash["Age"]);
  • Hashtable은 키와 값 형태로 이주어진 자료구조이다.
  • 데이터 출력시 키를 가지고 검색하므로 속도가 빠르다는 장점이 있다. 배열에서 요소를 찾는 경우 컬렉션을 정렬해서 이진탐색을 하거나 순차적으로 리스트를 탐색하지만 Hashtable은 키를 이용해 데이터가 저장된 컬렉션 내의 주소를 바로 계산해낸다. 이 과정을 Hashing이라 한다.
* 컬렉션 초기화 방법
int[] array = {1, 2, 3};

ArrayList list = new ArrayList(arr);  // 배열을 이용한 초기화
Queue queue = new Queue(arr);
Stack stack = new Stack(arr);

ArrayList list2 = new ArrayList(){1, 2, 3};  // 컬렉션 초기자를 이용한 초기화

Hashtable hash = new Hashtable(){  // 딕셔너리 초기자를 사용한 초기화
    [1] = 1,  // ; 이 아닌 , 로 항목을 구분
    [2] = 2
}

Hashtable hash2 = new Hashtable(){  // 컬렉션 초기자를 이용한 초기화
    {1, 1},
    {2, 2}
}
  • ArrayList, Queue, Stack 은 배열을 이용한 초기화가 가능하다.
  • ArrayList 는 추가적으로 컬렉션 초기자를 이용해 초기화하는 것이 가능하다. Queue, Stack은 불가능하다. 컬렉션 초기자는 IEnumerable 인터페이스와 Add() 메소드를 구현하는 컬렉션만 지원하는데 두 컬렉션은 IEnumerable은 상속하지만 Add() 메소드를 구현하고 있지 않다.
  • Hashtable은 딕셔너리 초기자를 이용할 수도 있고 컬렉션 초기자를 이용할 수도 있다.
* 인덱서
class Bono{
    private string[] say;  // private 필드 선언
    
    public Bono(){
        say = new String[3];  // private 필드 초기화
    }
    
    // **인덱서**
    public string this[int index]{  
        get{
            return say[index];  // 해당 인덱스 값을 반환
        }
        set{
            if(index >= say.Length){  // 인덱스가 배열의 길이보다 큰 경우
                Array.Resize<string>(ref say, index+1); // 배열 길이 추가
            }
            say[index] = value;  // 해당 인덱스에 값 할당
        }
    }
}

...
// 인덱서 사용시
Bono bono = new Bono();
bono[0] = "안녕";
bono[1] = "Hello";
  • 인덱서는 인덱스를 이용해 객체 내의 데이터에 접근할 수 있도록 해주는 프로퍼티라고 생각할 수 있다. 객체를 배열처럼 사용할 수 있게 해준다.

  • 인덱서와 프로퍼티는 비슷해 보이는데 이 둘이 다른 점은 인덱서에서는 인덱스를 이용해 데이터를 읽고 쓴다는 점이다.

  • 위 예제에서 Bono 객체를 생성하면 해당 객체를 배열처럼 쓸 수 있게 된다.

* foreach가 가능한 객체 만들기
  • foreach 구문은 IEnumerable과 IEnumerator을 상속하는 형식만 지원한다. 즉 배열이나 리스트 같은 컬렉션에서만 사용이 가능하다.
  • 새로 만든 객체를 foreach가 가능하도록 하려면 해당 인터페이스를 상속받고 구현해야한다.

1) IEnumerable 인터페이스

  • IEnumerable 인터페이스가 가지고 있는 메소드는 IEnumerator GetEnumerator() 하나 뿐이다.
  • GetEnumerator() 메소드를 구현할때는 yield return문을 사용해야 한다.
  • yield return문은 메소드의 실행을 일시정지시키고 호출자에게 결과를 반환한다
  • 메소드가 다시 호출되면 yield return이나 yield break(완전 중지)를 만날때까지 나머지 작업을 실행한다.

2) IEnumerator 인터페이스

  • GetEnumerator() 메소드는 IEnumerator 인터페이스를 상속하는 클래스의 객체를 반환하면 된다.
  • IEnumerator 인터페이스는 총 3개의 메소드를 가지고 있다
  • boolean MoveNext() : 다음 요소로 이동. 컬렉션의 끝을 지나면 false, 이동 성공시 true 반환
  • void Reset() : 컬렉션의 첫 위치의 앞으로 이동. 0번이 첫 요소라면 -1로 이동한다.
  • Object Current { get; } : 컬렉션의 현재 요소를 반환

예제를 한 번 만들어보자.

using System;
using System.Collections;

namespace EnumerableTest{
    class Bono : IEnumerable, IEnumerator{  // 두 인터페이스 상속
        private int[] array;
        private int position = -1;  // 컬렉션의 현재 위치 초기값.  0이 아닌 -1로 시작.  movenext() 시 0으로 이동하게 됨
        
        public Bono(){  // 생성자에서 배열 초기화
            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);  // 배열 리사이즈
                }
                array[index] = value;  // 해당 인덱스에 값 할당
            }
        }
        
        // IEnumerator 멤버
        public object Current{
            get{
                return array[position];  // 현재 위치의 값을 반환
            }
        }
        
        // IEnumerator 멤버
        public bool MoveNext(){
            if (position == array.Length -1){  // 더이상 이동이 불가능한 경우 초기화 후 false 반환
                Reset();
                return false;
            }
            position++;  // 위치 1 이동
            return true;
        }
        
        // IEnumerator 멤버
        public void Reset(){
            position = -1;  // 위치값 초기화
        }
        
        // IEnumerable 멤버
        public IEnumerator GetEnumerator(){
            for (int i = 0; i < array.Length; i++){
                yield return array[i];  // 각 요소를 하나씩 반환
            }
        }
    }
    
    class MainApp{
        static void Main(){
            Bono bono = new Bono();
            
            for(int i=0; i < 5; i++){
                bono[i] = i;  // 인덱서를 이용한 변수 세팅
            }
            
            foreach(var temp in bono){  // IEnumerator GetEnumerator() 이 실행되면서 각 요소를 하나씩 반환
                Console.WriteLine(temp);
            }
        }
    }
}

카테고리:

업데이트: