Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

nomad-programmer

[Programming/C#] 컬렉션 (Collection) 본문

Programming/C#

[Programming/C#] 컬렉션 (Collection)

scii 2020. 9. 14. 23:56

컬렉션이란, 같은 성격을 띄는 데이터의 모음을 담는 자료 구조를 말한다. 배열도 .NET Framework가 제공하는 다양한 컬렉션 자료 구조의 일부이다.
.NET Framework의 여타 컬렉션들이 상속하게 되어 있는 ICollection 인터페이스를 상속함으로써 System.Array 클래스 자신이 컬렉션의 일원임을 증명하고 있다.

public abstract class Array : ICloneable, IList, ICollection, IEnumerable

.NET Framework는 배열 말고도 여러 컬렉션 클래스들을 제공한다. 

  • ArrayList (자료 구조에서의 Linked List, Python에서는 list)
  • Queue (Python에서 Queue 모듈)
  • Stack (Python에서 list의 push(), pop() 함수 이용)
  • Hashtable (Python에서 dict() 자료형)

ArrayList

ArrayList는 가장 배열과 닮은 컬렉션이다. 컬렉션의 요소에 접근할 때는 [] 연산자를 이용하고, 특정 위치에 있는 요소에 데이터를 임의로 할당할 수도 있다. 허나 배열과는 달리 컬렉션을 생성할 때 용량을 미리 지정할 필요가 없이 필요에 따라 자동으로 그 용량이 늘어나거나 줄어든다. ArrayList의 가장 큰 장점이다.

ArrayList에서 가장 중요한 메소드는 Add(), RemoveAt(), Insert() 이렇게 세 개이다.
Add() 메소드는 컬렉션의 가장 마지막에 있는 요소 뒤에 새 요소를 추가하고, RemoveAt() 메소드는 특정 인덱스에 있는 요소를 제거한다. Insert() 메소드는 원하는 위치에 새 요소를 삽입한다.

Python으로 대입해보면 다음과 같다.

Add() -> append()
RemoveAt() -> remove()
Insert() -> insert()
using System;
using System.Collections;

namespace test
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            ArrayList list = new ArrayList();
            for (int i = 0; i < 5; i++)
            {
                list.Add(i);
            }
            
            foreach (object obj in list)
            {
                Console.Write($"{obj} ");
            }
            Console.WriteLine();
            
            list.RemoveAt(2);

            foreach (object obj in list)
            {
                Console.Write($"{obj} ");
            }
            Console.WriteLine();
            
            list.Insert(2, 2);

            foreach (object obj in list)
            {
                Console.Write($"{obj} ");
            }
            Console.WriteLine();

            list.Add("abc");
            list.Add("def");

            for (int i = 0; i < list.Count; i++)
            {
                Console.Write($"{list[i]} ");
            }
            Console.WriteLine();
        }
    }
}


/* 결과

0 1 2 3 4 
0 1 3 4 
0 1 2 3 4 
0 1 2 3 4 abc def 

*/

ArrayList가 다양한 형식의 객체를 담을 수 있는 이유

ArrayList가 다양한 형식의 객체를 담을 수 있는 이유는 다음의 Add(), Insert() 메소드의 선언을 보면 알 수 있다.

public virtual int Add( Object value )
public virtual void Insert( int index, Object value )

모든 형식은 object를 상속하므로 object형식으로 간주될 수 있다. 그래서 Add() 메소드에 int형식의 데이터를 넣더라도 정수 형식 그대로 입력되는 것이 아니라 object 형식으로 "박싱 (Boxing)" 되어 입력된다. string도 마찬가지다.
반대로 ArrayList의 요소에 접근해서 사용할 때는 원래의 데이터 형식으로 "언박싱 (Unboxing)"이 이루어진다. 박싱과 언박싱은 작지 않은 오버헤드를 요구하는 작업이다. ArrayList가 다루는 데이터가 많으면 많아질수록 이러한 성능의 저하는 더욱 늘어난다. 이것은 ArrayList만의 문제는 아니다. Stack, Queue, Hashtable 등의 컬렉션도 갖고 있는 문제이다.
해결 방법은 "일반화 컬렉션 (Generic Collection)" 에 있다.


Queue

Queue는 대기열, 즉 기다리는(대기) 줄(열) 이라는 뜻이다. Queue 자료 구조는 데이터나 작업을 차례대로 입력해뒀다가 입력된 순서대로 하나씩 꺼내 처리하기 위해 사용된다. 배열이나 리스트가 원하는 위치에 자유롭게 접근하는 반면에 Queue의 입력은 오직 뒤에서, 출력은 앞에서만 이루어진다. 

  • Enqueue : 데이터 입력
  • Dequeue : 데이터 끄집어 냄

Dequeue() 메소드를 실행하면 데이터를 자료 구조에서 실제로 꺼내게 된다.

  • FIFO (First In - First Out) : 먼저 들어온 데이터가 맨 먼저 나간다.
  • LILO (Last In - Last Out) : 마지막에 들어온 데이터는 마지막에 나간다.
using System;
using System.Collections;

namespace test
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            Queue queue = new Queue();
            queue.Enqueue(1);
            queue.Enqueue(2);
            queue.Enqueue(3);
            queue.Enqueue(4);
            queue.Enqueue(5);

            while (queue.Count > 0)
            {
                Console.WriteLine(queue.Dequeue());
            }
        }
    }
}


/* 결과

1
2
3
4
5

*/

Stack

Stack은 Queue와는 반대로 먼저 들어온 데이터가 마지막에 나가고, 나중에 들어온 데이터는 맨 먼저 나가는 구조의 컬렉션이다.

  • FILO (First In - Last Out) : 먼저 들어온 데이터가 맨 나중에 나간다.
  • LIFO (Last In - First Out) : 나중에 들어온 데이터가 맨 먼저 나간다.

Stack에 데이터를 넣을 때는 Push() 메소드를 이용하고, 데이터를 꺼낼 때는 Pop() 메소드를 이용한다. Push() 메소드는 데이터를 위에 "쌓고", Pop() 메소드는 제일 위에 쌓여 있는 데이터를 "꺼낸다". 
Pop()을  호출하여 데이터를 Stack에서 꺼내고 나면 그 데이터는 컬렉션에서 제거가 되고 그 아래에 있던 데이터가 제일 위로 올라온다. 그 다음에 Pop()을 호출하면 방금 올라온 데이터를 꺼내게 된다.
접시를 쌓는것이라고 생각하면 된다.

using System;
using System.Collections;

namespace test
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            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

*/

Hashtable

Hashtable은 키(Key)와 값(Value)의 쌍으로 이루어진 데이터를 다룰 때 사용한다. 사전이 가장 좋은 예이다. 그래서 Python에서는 사전형이라고 불린다.

Hashtable은 탐색 속도가 빠르고 사용하기도 편하다. Hashtable은 배열에서 index를 이용해 배열 요소에 접근하는 것에 준하는 탐색 속도를 자랑한다. 다시 말하면 탐색 속도가 거의 소요되지 않는다고 할 수 있다.
ArrayList에서 원하는 데이터를 찾으려면 컬렉션을 정렬하여 이진 탐색을 수행하거나 순차적으로 리스트를 탐색해 나가지만, Hastable은 키를 이용해서 단번에 데이터가 저장되어 있는 컬렉션 내의 주소를 계산해낸다.
이 작업을 "해싱 (Hashing)" 이라고 한다. Hashtable의 이름은 이 알고리즘에서 유래된 것이다.

using System;
using System.Collections;

namespace test
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            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["다섯"]);
            Console.WriteLine(ht["여섯"] ?? "알 수 없는 키");
        }
    }
}


/* 결과

one
two
three
four
five
알 수 없는 키

*/

컬렉션 초기화 방법

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

using System;
using System.Collections;

namespace test
{
    internal class Program
    {
        public static void Main(string[] args)
        {
            int[] arr = {123, 456, 789};

            ArrayList list = new ArrayList(arr);
            foreach (object item in list)
            {
                Console.WriteLine($"ArrayList : {item}");
            }
            Console.WriteLine();

            Stack stack = new Stack(arr);
            foreach (object item in stack)
            {
                Console.WriteLine($"Stack : {item}");
            }
            Console.WriteLine();

            Queue queue = new Queue(arr);
            foreach (object item in queue)
            {
                Console.WriteLine($"Queue : {item}");
            }
            Console.WriteLine();

            // ArrayList는 추가적으로 배열의 도움 없이 직접 컬렉션 초기자를 이용해 초기화하는 것이 가능하다.
            ArrayList list2 = new ArrayList() { 11, 22, 33 };
            foreach (object item in list2)
            {
                Console.WriteLine($"ArrayList2 : {item}");
            }
        }
    }
}


/* 결과

ArrayList : 123
ArrayList : 456
ArrayList : 789

Stack : 789
Stack : 456
Stack : 123

Queue : 123
Queue : 456
Queue : 789

ArrayList2 : 11
ArrayList2 : 22
ArrayList2 : 33

*/

Hashtable 초기화

딕셔너리 초기자 (Dictionary Initializer)를 이용한다. 딕셔너리 초기자는 컬렉션 초기자와 비슷하게 생겼다.

Hashtable ht = Hashtable()
{
    ["하나"] = 1,
    ["둘"] = 2,
    ["셋"] = 3
};

Hashtable을 초기화할 때에도 컬렉션 초기자를 사용할 수 있다.

Hashtable ht = Hashtable()
{
    {"하나", 1},
    {"둘", 2},
    {"셋", 3}
};

하지만 쓰기도 편하고 읽기도 수월한 "딕셔너리 초기자"를 사용하는 것을 권한다.

Comments