'이것이 C#이다 개정판'의 chapter 10을 정리한 글입니다.
컬렉션 (Collection)
컬렉션이란, 같은 성격을 띈 데이터의 모음을 담는 자료구조를 말합니다.
배열도 .NET이 제공하는 다양한 컬렉션 자료구조의 일부입니다.
.NET은 배열 말고도 여러 개의 컬렉션 클래스들을 제공합니다.
해당 글에서 알아볼 클래스는 ArrayList, Queue, Stack, Hashtable입니다.
ArrayList
ArrayList는 배열과 닮은 컬렉션입니다.
컬렉션의 요소에 접근할 때는 [ ] 연산자를 이용하고, 특정 위치에 있는 요소에 데이터를 임의로 할당할 수도 있습니다.
다만, 배열과는 달리 컬렉션을 생성할 때 용량을 미리 지정할 필요 없이 필요에 따라 자동으로 그 용량이 늘어나거나 줄어듭니다.
[특징]
- 필요에 따라 내부적으로 용량을 늘리거나 줄입니다.
- 초기 용량 지정이 가능은 합니다. 지정한 용량을 초과할 시 자동으로 용량이 증가됩니다.
- 비일반화(Non-Generic) 컬렉션 클래스로, object 타입으로 요소를 저장합니다.
- 어떤 타입의 데이터든 저장이 가능하지만, 타입 안정성이 낮습니다.
- object 타입으로 저장하기 때문에, 한 ArrayList에 int, string, 사용자 정의 객체 등 다양한 타입의 요소를 저장할 수 있습니다.
- 컴파일 타임에 요소의 타입을 체크하지 않으므로, 잘못된 타입의 요소를 추가하거나 꺼낼 때 런타임 오류가 발생할 가능성이 높습니다.
- 요소를 꺼낼 때마다 명시적 타입 변환이 필요합니다.
- 값 타입 요소를 저장하거나 꺼낼 때 박싱(Boxing) 및 언박싱(Unboxing) 오버헤드가 발생합니다.
이로 인해 성능 저하가 일어날 수 있습니다.
[문법]
ArrayList는 System.Collections 네임스페이스에 포함되어 있습니다.
using System.Collections;
ArrayList myList = new ArrayList();
// ArrayList myList = new ArrayList(capacity : 10);
[주요 메서드]
| 메서드 이름 | 설명 | 예시 |
| Add(object value) | ArrayList의 끝에 요소를 추가합니다. | myList.Add(10); |
| AddRange(ICollection c) | 다른 컬렉션(ArrayList, 배열 등)의 모든 요소를 ArrayList의 끝에 추가합니다. |
myList.AddRange(new int[] { 1, 2, 3 }); |
| Insert(int index, object value) | 지정된 인덱스에 요소를 삽입하니다. 기존 요소들은 뒤로 밀립니다. |
myList.Insert(0, "Start"); |
| InsertRange(int index, ICollection c) | 지정된 인덱스부터 다른 컬렉션의 모든 요소를 삽입합니다. | myList.InsertRange(1, anotherList); |
| Remove(object obj) | 지정된 객체와 동일한 첫 번째 요소를 제거합니다. | myList.Remove("Hello"); |
| RemoveAt(int index) | 지정된 인덱스의 요소를 제거합니다. | myList.RemoveAt(0); |
| RemoveRange(int index, int count) | 지정된 인덱스부터 지정된 개수만큼의 요소를 제거합니다. | myList.RemoveRange(1, 2); |
| Clear() | ArrayList에 지정된 객체가 포함되어 있는지 확인합니다. | myList.Clear(); |
| Contains(object obj) | ArrayList에 지정된 객체가 포함되어 있는지 확인합니다. | bool hasTen = myList.Contains(10); |
| IndexOf(object obj) | 지정된 객체와 동일한 첫 번째 요소의 인덱스를 찾습니다. 없으면 -1을 반환합니다. | int index = myList.IndexOf("Hello"); |
| LastIndexOf(object obj) | 지정된 객체와 동일한 마지막 요소의 인덱스를 찾습니다. 없으면 -1을 반환합니다. | int index = myList.LastIndexOf("World"); |
| Sort( ) | ArrayList의 요소를 정렬합니다. (요소들이 IComparable을 구현하거나 IComparer 제공 시) |
myList.Sort(); |
| Reverse( ) | ArrayList의 요소 순서를 뒤집습니다. | myList.Reverse(); |
| ToArray( ) | ArrayList의 모든 요소를 새로운 object 배열로 복사합니다. | object[] array = myList.ToArray(); |
| GetRange(int index, int count) | ArrayList의 지정된 범위에 있는 요소를 포함하는 새 ArrayList를 반환합니다. | ArrayList subList = myList.GetRange |
| TrimToSize() | 현재 요소 수에 맞게 용량을 줄입니다. | myList.TrimToSize(); |
| Count (프로퍼티) | ArrayList에 포함된 현재 요소의 개수를 가져옵니다. | int count = myList.Count; |
| Capacity (프로퍼티) | ArrayList에 저장할 수 있는 요소의 개수를 가져오거나 설정합니다. | int capacity = myList.Capacity; myList.Capacity = 20; |
| this[int index] (인덱서) |
지정된 인덱스의 요소를 가져오거나 설정합니다. | object element = myList[0]; myList[1] = "World"; |
※ 비일반화 컬렉션과 일반화 컬렉션
- 비일반화 컬렉션
- 저장하는 요소의 타입을 object타입으로 처리합니다.
- 어떤 타입의 데이터든 담을 수 있지만, 컴파일 타임에는 요소의 실제 타입을 알지 못합니다.
- 비일반화 컬렉션 클래스
- ArrayList : 크기가 동적으로 변하는 컬렉션
- Hashtable : key - value 쌍을 저장하는 해시테이블, 키와 값 모두 object로 저장합니다.
- Queue : FIFO(First - In, First - Out) 방식의 컬렉션
- Stack : LIFO(Last - In, First - Out) 방식의 컬렉션
- SortedList : key - value 쌍을 저장하고, 키를 기준으로 정렬하는 컬렉션
- 일반화 컬렉션
- 저장하는 요소를 컴파일시 지정하여 T 타입으로 처리합니다.
- 타입 안정성이 높으며, 박싱/언박싱이 불필요하기에 오버헤드가 거의 없습니다.
- 일반화 컬렉션 클래스
- List<T> : 비일반화 컬렉션 ArrayList의 일반화 버전입니다.
특정 타입 T의 요소를 저장하는 동적 배열입니다. - Dictionary<TKey, TValue> : 비일반화 컬렉션 Hashtable의 일반화버전입니다.
특정 타입의 키와 값을 쌍으로 저장합니다. - Queue<T> : 비일반화 컬렉션 Queue의 일반화 버전입니다.
특정 타입 T의 요소를 FIFO 방식으로 저장합니다. - Stack<T> : 비일반화 컬렉션 Stack의 일반화 버전입니다.
특정 타입 T의 요소를 LIFO방식으로 저장합니다. - SortedList<Tkey, TValue> : 비일반화 컬렉션 SortedList의 일반화 버전입니다.
특정 타입의 키와 값을 쌍으로 저장하고, 키를 기준으로 정렬합니다. - HashSet<T> : 중복되지 않는 T 타입 요소의 집합을 저장하는 컬렉션입니다.
- LinkedList<T> : T 타입 요소를 저장하는 연결 리스트입니다.
- List<T> : 비일반화 컬렉션 ArrayList의 일반화 버전입니다.
[예제]
using System;
using System.Collections;
namespace UsingList
{
class MainApp
{
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();
}
}
}

Queue
Queue 자료구조는 데이터나 작업을 차례대로 입력해뒀다가 입력된 순서대로 하나씩 꺼내 처리하기 위해 사용됩니다.
배열이나 리스트가 원하는 위치에 자유롭게 접근하는 반면에 Queue는 입력은 오직 뒤에서, 출력은 앞에서만 이루어집니다.
즉, FIFO (First - In, First - Out) 방식으로 이루어져 있습니다.
[특징]
- FIFO 방식으로 이루어져 있습니다.
- FIFO 방식이라는 것이 가장 기본적인 특징이면서 핵심인 특징입니다.
- 요소를 추가한 순서대로 꺼낼 수 있습니다.
- 큐(Queue)의 뒤(Enqueue)와 앞(Dequeue, Peek)에서의 요소 추가 및 제거는 일반적으로 효율적입니다.
이 때 시간복잡도는 평균적으로 O(1)을 갖습니다.
- 이 외의 특징은 비일반화 컬렉션 클래스가 가지는 공통된 특징입니다.
- 다양한 타입 저장 가능
- 가변 크기 : 요소를 추가하거나 제거할 때 필요에 따라 내부적으로 용량이 자동으로 조절됩니다.
- 박싱 / 언박싱 발생
- 타입 안정성 낮음
[문법]
Queue는 System.Collections 네임스페이스에 포함되어 있습니다.
using System.Collections;
Queue myQueue = new Queue();
[주요 메서드]
| 메서드 이름 | 설명 | 예시 |
| Enqueue(object obj) | 큐의 뒤(끝)에 요소를 추가합니다. | myQueue.Enqueue(data); |
| Dequeue() | 큐의 앞(시작)에서 요소를 제거하고, 해당 요소를 object 타입으로 반환합니다. 큐가 비어 있으면 'InvalidOperationException' 발생합니다. |
object item = myQueue.Dequeue(); |
| Peek() | 큐의 앞(시작)에 있는 요소를 제거하지 않고 object 타입으로 반환합니다. 큐가 비어 있으면 'InvalidOperationException' 발생합니다. |
object item = myQueue.Peek(); |
| Contains(object obj) | 큐에 지정된 객체(obj)와 동일한 요소가 포함되어 있는지 확인합니다. |
bool hasItem = myQueue.Contains(data); |
| Clear() | 큐의 모든 요소를 제거합니다. | myQueue.Clear(); |
| ToArray() | 큐의 요소를 새로운 object 배열로 복사하여 반환합니다. (큐의 순서는 유지됩니다.) | object[] array = myQueue.ToArray(); |
| TrimToSize() | 현재 요소 수에 맞게 내부 용량을 줄입니다. | myQueue.TrimToSize(); |
| Count (프로퍼티) | 큐에 포함된 현재 요소의 개수를 가져옵니다. | int count = myQueue.Count; |
[예제]
using System;
using System.Collections;
namespace UsingQueue
{
class MainApp
{
static void Main(string[] args)
{
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());
}
}
}
}

Stack
Stack는 Queue와 반대로 먼저 들어온 데이터가 나중에 나가고(First In - Last Out), 나중에 들어온 데이터는 먼저 나가는(Last In - First Out) 구조의 컬렉션입니다.
[특징]
- LIFO 방식으로 이루어져 있습니다.
- LIFO 방식이라는 것이 가장 기본적인 특징이면서 핵심인 특징입니다.
- 가장 최근에 추가된 요소가 가장 먼저 꺼내집니다.
- 스택의 맨 위(Push, Pop, Peek)에서의 요소 추가 및 제거는 일반적으로 효율적입니다.
평균적으로 시간복잡도는 O(1)을 갖습니다.
- 이 외의 특징은 비일반화 컬렉션 클래스가 가지는 공통된 특징입니다.
- 다양한 타입 저장 가능
- 가변 크기 : 요소를 추가하거나 제거할 때 필요에 따라 내부적으로 용량이 자동으로 조절됩니다.
- 박싱 / 언박싱 발생
- 타입 안정성 낮음
[문법]
Stack은 System.Collections 네임스페이스에 포함되어 있습니다.
using System.Collections;
Stack myStack = new Stack();
[주요 메서드]
| 메서드 이름 | 설명 | 예시 |
| Push(object obj) | 스택의 맨 위에 요소를 추가합니다. 매개변수 타입은 object입니다. |
myStack.Push(data); |
| Pop() | 스택의 맨 위에서 요소를 제거하고 해당 요소를 object 타입으로 반환합니다. 스택이 비어 있으면 'InvalidOperationException' 발생합니다. |
object item = myStack.Pop() |
| Peek() | 스택의 맨 위에서 요소를 제거하지 않고, object 타입으로 반환합니다. 스택이 비어 있으면 'InvalidOperationException' 발생합니다. |
object item = myStack.Peek(); |
| Contains(object obj) | 스택에 지정된 객체(obj)와 동일한 요소가 포함되어 있는지 확인합니다. |
bool hasItem = myStack.Contains(data); |
| Clear() | 스택의 모든 요소를 제거합니다. | myStack.Clear(); |
| ToArray() | 스택의 요소를 새로운 object 배열로 복사하여 반환합니다. (순서는 스택에서 꺼내는 순서와 동일합니다. - LIFO) |
object[] array = myStack.ToArray(); |
| Count (프로퍼티) | 스택에 포함된 현재 요소의 개수를 가져옵니다. | int count = myStack.Count; |
[예제]
using System;
using System.Collections;
namespace UsingStack
{
class MainApp
{
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());
}
}
}

Hashtable
Hashtable은 키(Key)와 값(Value)의 쌍으로 이루어진 데이터를 다룰 때 사용합니다.
데이터를 저장하고 검색할 때 해싱(Hashing) 기법을 사용하여 매우 빠른 평균 성능을 제공하는 것이 특징입니다.
[특징]
- 키 - 값이 쌍을 이루고 있습니다.
- 각 데이터 항목이 고유한 키와 그에 연결된 값으로 구성됩니다.
- 키 - 값 쌍이 저장된 순서를 유지하지 않습니다. 해시 값에 따라 내부적으로 저장 위치가 결정됩니다.
- 키는 반드시 고유해야 합니다. 이미 존재하는 키로 요소를 추가하려 하면 ArgumentException이 발생합니다.
키는 null이 될 수 없습니다. 다만, 중복은 가능합니다.
- 해싱(Hashing) 기법
- 해싱은 어떤 데이터(키)를 해시 함수(Hash Function)라는 수학적 연산을 통해 고정된 크기의 값(해시 값 또는 해시 코드)으로 매핑하는 과정입니다.
- 매핑하는 과정을 통해 얻은 해시 값을 이요하여 데이터가 저장될 위치(해시 주소 또는 인덱스)를 직접 계산하고, 계산된 위치에 데이터를 저장하거나 저장된 데이터를 검색하는 기법입니다.
- 구성 요소
- 키 (Key) : 해시 함수에 입력되는 데이터입니다. 이 키를 통해 원하는 데이터 항목을 식별합니다.
- 해시 함수 : 키를 입력으로 받아 해시 값(Hash Code)을 생성하는 함수입니다.
좋은 해시 함수는 키들을 해시 테이블 주소 공간에 균등하게 분포시켜야 합니다. - 해시 주소 / 해시 인덱스 : 해시 값을 해시 테이블의 실제 물리적 주소나 배열의 인덱스로 변환한 값입니다.
- 해시 테이블 / 버킷(Bucket) : 데이터 항목(키와 값 쌍)이 실제로 저장되는 공간입니다.
보통은 배열 형태로 구성되며, 각 배열 칸을 버킷이라고 부릅니다.
- 기본적으로 다중 스레드에서 읽기는 안전하지만, 쓰기 작업 시에는 외부에서 명시적으로 동기화 처리가 필요합니다.
- 이 외의 특징은 비일반화 컬렉션 클래스가 가지는 공통된 특징입니다.
- 다양한 타입 저장 가능
- 가변 크기 : 필요에 따라 내부적으로 용량이 자동으로 조절됩니다.
- 박싱 / 언박싱 발생
- 타입 안정성 낮음
[문법]
Hashtable은 는 System.Collections 네임스페이스에 포함되어 있습니다.
using System.Collections;
Hashtable myDictionary = new Hashtable();
[주요 메서드]
| 메서드 이름 | 설명 | 예시 |
| Add(object key, object value) | 지정된 키와 값을 Hashtable에 추가합니다. 키가 이미 존재하거나 null이면 예외를 발생합니다. |
myDictionary.Add("newkey", 123); |
| Remove(object key) | 지정된 키를 가진 요소를 제거합니다. 키가 없으면 아무 작업도 수행하지 않습니다. |
myDictionary.Remove("apple"); |
| Contains(object key) | 지정된 키를 가진 요소가 Hashtable에 포함되어 있는지 확인합니다. (ContainsKey와 동일 |
bool hasKey = myDictionary.Contains("price"); |
| ContainsKey(object key) | 지정된 키를 가진 요소가 Hashtable에 포함되어 있는지 확인합니다. |
bool hasItem = myDictionary.ContainsKey("banana"); |
| ContainsValue(object value) | 지정된 값을 가진 요소가 Hashtable에 포함되어 있는지 확인합니다. 값은 중복될 수 있으므로 여러 키에 연결된 같은 값을 찾을 수 있습니다. |
bool hasValue = myDictionary.ContainsValue(1); |
| Clear() | Hashtable의 모든 요소를 제거합니다. | myDictionary.Clear(); |
| Clone() |
Hashtable의 얕은 복사본을 만듭니다. (키와 값은 복사되지 않고, 참조만 복사됩니다.) |
Hashtable clonedDict = (Hashtable)myDictionary.Clone(); |
| Synchronized(Hashtable table) | Hashtable의 스레드로부터 안전하게 보호되는 래퍼(wrapper)를 반환합니다. |
int count = myQueue.Count; |
| Keys | Hashtable의 모든 키를 포함하는 ICollection을 가져옵니다. | ICollection keys = myDictionary.Keys; |
| Values | Hashtable의 모든 값을 포함하는 ICollection을 가져옵니다. | ICollection value = myDictionary.Values; |
| Count (프로퍼티) | Hashtable에 포함된 현재 키 - 값 쌍의 개수를 가져옵니다. | int count = myDictionary.Count; |
| this[object key] (인덱서) | 지정된 키를 가진 요소의 값을 가져오거나 설정합니다. 가져올 때 키가 없으면, null을 반환하고, 설정할 때 키가 없으면 추가하고, 있으면 값을 업데이트합니다. 반환/설정 타입은 object입니다. |
object val = myDictionary["key"]; myDictionary["key"] = "newValue"; // 추가됨 myDictionar["newKey"] = "someValue"; |
[예제]
using System;
using System.Collections;
using static System.Console;
namespace UsingHashtable
{
class MainApp
{
static void Main(string[] args)
{
Hashtable ht = new Hashtable();
ht["하나"] = "one";
ht["둘"] = "two";
ht["셋"] = "three";
ht["넷"] = "four";
ht["다섯"] = "five";
WriteLine(ht["하나"]);
WriteLine(ht["둘"]);
WriteLine(ht["셋"]);
WriteLine(ht["넷"]);
WriteLine(ht["다섯"]);
}
}
}

컬렉션을 초기화하는 방법
ArrayList, Queue, Stack의 초기화
ArrayList, Queue, Stack은 배열의 도움을 받아 간단하게 초기화를 수행할 수 있습니다.
이들 컬렉션의 생성자를 호출할 때 배열 객체를 매개변수로 넘기면 컬렉션 객체는 해당 배열을 바탕으로 내부 데이터를 채웁니다.
세 컬렉션 중 ArrayList는 배열의 도움 없이도 직접 컬렉션 초기자를 이용해서 초기화할 수 있습니다.
컬렉션 초기자는 생성자를 호출할 때, 생성자 뒤에 중괄호 사이에 컬렉션 요소의 목록을 입력하여 사용합니다.
// 배열을 이용하여 초기화
int[] arr = { 123, 456, 789 };
ArrayList list = new ArrayList(arr); // 123, 456, 789
Stack stack = new Stack(arr); // 789, 456, 123
Queue queue = new Queue(arr); // 123, 456, 789
// 컬렉션 초기자 이용하여 초기화
ArrayList list2 = new ArrayList() { 11, 22, 33 };
※ Stack과 Queue는 컬렉션 초기자를 이용할 수 없습니다.
컬렉션 초기자는 IEnumerable 인터페이스와 Add() 메서드를 구현하는 컬렉션만 지원하는데,
이 두 컬렉션은 IEnumerable은 상속하지만 Add() 메서드는 구현하지 않기 때문에 컬렉션 초기자를 이용할 수 없습니다.
[예제]
using System;
using System.Collections;
using static System.Console;
namespace InitializingCollections
{
class MainApp
{
static void Main(string[] args)
{
int[] arr = { 123, 456, 789 };
ArrayList list = new ArrayList(arr);
foreach(object item in list)
{
WriteLine($"ArrayList : {item}");
}
Console.WriteLine();
Stack stack = new Stack(arr);
foreach (object item in stack)
{
WriteLine($"Stack : {item}");
}
Console.WriteLine();
Queue queue = new Queue(arr);
foreach (object item in queue)
{
WriteLine($"Queue : {item}");
}
Console.WriteLine();
ArrayList list2 = new ArrayList() { 11, 22, 33 };
foreach (object item in list2)
{
WriteLine($"ArrayList2 : {item}");
}
Console.WriteLine();
}
}
}

Hashtable의 초기화
Hashtable을 초기화할 때는 딕셔너리 초기자(Dictionary Initializer)를 이용합니다.
권장하지는 않지만 컬렉션 초기자를 사용할 수도 있습니다.
// 딕셔너리 초기자 이용하여 초기화
Hashtable ht1 = new Hashtable()
{
["하나"] = 1, // ;가 아니라 ,를 이용하여 항목을 구분합니다.
["둘"] = 2,
["셋"] = 3
};
// 컬렉션 초기자 이용하여 초기화
Hashtable ht2 = new Hashtable()
{
{"하나", 1},
{"둘", 2},
{"셋", 3}
};
[딕셔너리 초기자를 이용한 초기화 권장]
위에서도 언급한 것 처럼 컬렉션 초기자를 이용하여 Hashtable을 초기화하는 것보다, 딕셔너리 초기자를 이용하여 초기화하는 것을 더 권장합니다.
- [ ] 인덱서를 통해 Key - Value를 바로 지정할 수 있습니다.
- 가독성이 좋고 더 직관적입니다.
- 컬렉션 초기자를 사용하면, 내부적으로 Add() 메서드를 호출합니다. 그러기에 비교했을 때 비효율적입니다.
인덱서 (Indexer)
인덱서는 클래스, 구조체, 또는 인터페이스의 인스턴스에 대해,
배열처럼 [ ] 연산자를 사용하여 접근할 수 있게 해주는 특별한 멤버입니다.
[문법]
class 크래스이름
{
한정자 인덱서형식 this[형식 index] // 인덱스의 식별자가 꼭 index일 필요는 없습니다.
{
get
{
// index를 이용하여 내부 데이터 반환
}
set
{
// index를 이용하여 내부 데이터 저장
}
}
}
위의 선언 문법을 보면, 인덱서는 프로퍼티처럼 식별자를 따로 가지지 않습니다.
프로퍼티가 이름을 통해 객체 내의 데이터에 접근하게 해준다면, 인덱서는 인덱스를 통해 객체 내의 데이터에 접근하게 해줍니다.
[특징]
- 배열과 유사한 접근 구문
- 객체 인스턴스에 [ ] (대괄호)를 사용하여 인덱스나 키를 통해 접근할 수 있습니다.
- 이를 통해 사용 코드의 가독성과 직관성을 향상시킬 수 있습니다.
- this 키워드 사용
- 인덱서 선언 시 자신의 인스턴스를 나타내는 this 키워드를 사용합니다.
- 참고로 정적(static) 인덱서는 정의할 수 없습니다.
- 정적 멤버는 클래스나 구조체의 타입 자체에 속하며, 특정 인스턴스에 종속되지 않습니다.
그래서 정적 멤버에 접근할 때는 객체 인스턴스 없이 타입 이름을 사용합니다.
따라서 this는 특정 인스턴스를 가리키는데, 정적 멤버는 인스턴스가 없기 때문에,
정적 인덱서를 정의하려고 하면 '정적 멤버 컨텍스트에서는 인스턴스 멤버인 this를 사용할 수 없다'라는 컴파일 오류가 발생하게 됩니다.
- 정적 멤버는 클래스나 구조체의 타입 자체에 속하며, 특정 인스턴스에 종속되지 않습니다.
- get 및 set 접근자
- 프로퍼티(Property)와 유사하게, 값을 가져오는 get과 값을 설정하는 set 접근자를 가집니다.
- 인덱서 또한 읽기 전용(only get), 쓰기 전용(only set)으로도 선언이 가능합니다.
- 여러 매개변수 사용 가능
- 대괄호 안에 하나 이상의 매개변수를 가질 수 있으며, 매개변수의 타입도 정수형 외에 문자열 등 다양하게 지정할 수 있습니다.
- 이런 특징 덕분에, 다차원 배열 인덱싱이나 키 기반 인덱싱 구현에 유용합니다.
- 오버로딩
- 매개변수의 개수나 타입이 다르면 동일한 이름의 인덱서를 여러 개 정의할 수 있습니다.
- 데이터 저장 역할 X
- 인덱서 자체는 데이터를 저장하지 않고, 주로 클래스 내부에 있는 다른 컬렉션(배열, List, Dictionary 등)에 접근하거나, 계산된 값을 반환/설정하는 데 사용됩니다.
- 인터페이스에 선언
- 인터페이스는 인덱서의 시그니처(signature)만 선언할 수 있으며, 구현은 해당 인터페이스를 상속받는 클래스/구조체가 담당합니다.
[예제]
using System;
using System.Collections;
using static System.Console;
namespace Indexer
{
class MyList
{
private int[] array;
public MyList()
{
array = new int[3];
}
public int this[int index]
{
get
{
return array[index]; ;
}
set
{
if (index >= array.Length)
{
Array.Resize(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]);
}
}
}
}

foreach가 가능한 객체 만들기
foreach 문은 for 문처럼 요소의 위치를 위한 인덱스 변수를 선언할 필요가 없습니다.
세미콜론을 넣지 않아도 되고, 조건문이나 증감식을 쓰지 않아도 됩니다.
for 문을 이용한 코드에 비해 foreach 문을 이용한 코드는 쓰기도 좋고, 읽기도 좋습니다.
하지만, foreach 문은 아무 형식의 객체에서나 사용할 수 있는 것이 아닙니다. 배열이나 리스트 같은 컬렉션에서만 사용할 수 있습니다.
이유를 알기 전, foreach 문 작동 방식에 대해 먼저 알아보고자 합니다.
[foreach 문 작동 방식]
컴파일러는 foreach 문을 만나면 내부적으로 GetEnumerator(), MoveNext(), Current를 사용하는 while 루프와 유사한 코드로 변환합니다.
[예시]
foreach(int item in myList)
{
// ... item 사용 코드
}
↓
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
// ...
IEnumerator enumerator = collection.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
// ...
}
- foreach 문은 컴파일 될 때, GetEnumerator()를 호출해서 열거자를 가져옵니다.
- IEnumerator는 세 가지 멤버를 가집니다.
- bool MoveNext() : 다음 요소로 이동
- object Current { get; } : 현재 요소 반환
- void Reset() : 열거 위치 초기화
- foreach는 내부적으로 MoveNext() → Current를 반복해서 호출하며 루프를 수행합니다.
확인해보면, foreach문은 단순해 보이지만, 실제로는 인터페이스 기반의 디자인 패턴 위에서 작동하는 것을 볼 수 있습니다.
비일반화 컬렉션도 IEnumerable만 구현하면 foreach가 잘 작동하며, 이는 '열거자 패턴(Iterator Pattern)'의 전형적인 예시입니다.
[foreach가 가능한 컬렉션의 조건]
foreach문을 사용하려면 컬렉션 타입이 다음 두 가지 조건 중 하나를 만족해야 합니다.
- (일반화 컬렉션일 때) System.Collections.Generic.IEunerable<T> 인터페이스를 구현하는 경우
- (비일반화 컬렉션일 때) System.Collections.IEnumerable 인터페이스를 구현하는 경우
그리고 이러한 인터페이스를 구현하는 컬렉션 타입은 다음과 같은 조건을 만족하는 GetEnumerator() 메서드를 제공해야 합니다.
- GetEnumerator() 메서드는 반환 타입이 System.Collections.Generic.IEnumerator<T> 또는 System.Collections.IEnumerator여야 합니다.
반환된 열거자(Enumerator) 객체는 Current 속성과 MoveNext() 메서드를 가져야 합니다.
[yield 문]
아래의 예제에서는 yield 키워드가 쓰이는데, 이는 IEnumerator를 상속하는 클래스를 따로 구현하지 않아도 컴파일러가 자동으로 해당 인터페이스를 구현한 클래스를 생성해줍니다.
yield 키워드는 반복기 블록(iterator block)을 만드는 데 사용됩니다.
반복기 블록은 메소드나 get 접근자 내에서 사용하여, 컬렉션 전체를 미리 생성하여 반환하는 대신, 요청이 있을 때마다 시퀀스의 다음 요소를 하나씩 생성하여 반환하는 기능을 구현할 수 있게 해줍니다.
- 주요 역할
- IEnumerator 인터페이스를 직접 구현하는 복잡한 과정을 yield 키워드를 사용하여 매우 간결하게 처리할 수 있도록 합니다.
- 시퀀스의 모든 요소를 메모리에 로드하지 않고, foreach 루프 등 소비자가 다음 요소를 요청할 때 비로소 해당 요소를 생성하여 제공합니다.
- yeild return 문을 만날 때마다 메소드의 현재 상태(로컬 변수 값, 실행 위치)가 저장되었다가, 다음 요청 시 저장된 상태에서부터 실행을 다시 시작합니다.
- 문법
- yeild return expression;
- expression의 값을 시퀀스의 다음 요소로 반환합니다.
- 반복기 블록의 현재 실행 상태를 저장하고, 일시 정지시킵니다.
- 다음에 호출자가 요청하면, 저장된 상태에서부터 실행을 재개합니다.
- yeild break;
- 반복의 끝을 알립니다.
- 이 문장을 만나면 반복기 블록의 실행이 즉시 중단되고, 더 이상 생성될 요소가 없음을 호출자에게 알립니다.
※ 호출자의 MoveNext() 호출이 fasle를 반환하게 됩니다.
- yeild return expression;
[예제]
using System;
using System.Collections;
using static System.Console;
namespace Yield
{
class MyEnumerator
{
int[] numbers = { 1, 2, 3, 4 };
public IEnumerator GetEnumerator()
{
yield return numbers[0];
yield return numbers[1];
yield return numbers[2];
yield break; // bield break는 GetEnumerator() 메서드를 종료시킵니다.
yield return numbers[3]; // 따라서 이 코드는 실행되지 않습니다.
}
}
class MainApp
{
static void Main(string[] args)
{
var obj = new MyEnumerator();
foreach (int i in obj)
{
Console.WriteLine(i);
}
}
}
}

[IEnumerator 인터페이스의 메소드 및 프로퍼티]
위에서 간략하게 정리된 내용을 다시금 정리합니다.
| 메서드 또는 프로퍼티 | 설명 |
| boolean MoveNext( ) | 다음 요소로 이동합니다. 컬렉션의 끝을 지난 경우에는 false, 이동이 성공한 경우네는 true를 반환합니다. |
| void Reset( ) | 컬렉션의 첫 번째 위치의 "앞"으로 이동합니다. 예를 들어, 첫 번째 위치가 0번인 경우 Reset()을 호출하면 -1번으로 이동합니다. 첫 번째 위치로의 이동은 MoveNext()를 호출한 다음 이루어집니다. |
| Object Current { get; } | 컬렉션의 현재 요소를 반환합니다. |
[예제]
위의 조건들을 바탕으로 foreach 문이 가능하게끔 예제를 구현하였습니다.
using System;
using System.Collections;
using static System.Console;
namespace Enumerable
{
class MyList : IEnumerable, IEnumerator
{
private int[] array;
int position = -1; // Code A
public MyList()
{
array = new int[3];
}
public int this[int index]
{
get
{
return array[index];
}
set
{
if (index >= array.Length)
{
Array.Resize(ref array, index + 1);
Console.WriteLine($"Array Resized : {array.Length}");
}
array[index] = value;
}
}
// IEnumerator 멤버
public object Current
{
get
{
return array[position];
}
}
// IEnumerator 멤버
public bool MoveNext()
{
if (position == array.Length - 1)
{
Reset();
return false;
}
position++;
return (position < array.Length);
}
// IEnumerator 멤버
public void Reset()
{
position = -1;
}
// IEnumerable 멤버
public IEnumerator GetEnumerator()
{
return this;
}
}
class MainApp
{
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);
}
}
}
}

[Code A]
position은 컬렉션의 현재 위치를 다루는 변수입니다. 초기값은 0이 아닌 -1로 설정을 하였습니다.
0이 아니라 -1인 이유는, 0은 배열의 첫 번째 요소를 가리키는 수이기 때문입니다.
position이 0의 값을 갖고 있을 때 foreach 문이 첫 번째 반복을 수행하면 MoveNext() 메서드를 실행하고, 이 때 position이 1이 되어 두 번째 요소를 가져오는 문제가 생기게 됩니다.
그렇기에 초기값은 -1로 설정하여야 합니다.
'C#' 카테고리의 다른 글
| [C#] 예외 처리 (0) | 2025.05.06 |
|---|---|
| [C#] 일반화 프로그래밍 (0) | 2025.05.05 |
| [C#] 배열 (0) | 2025.05.02 |
| [C#] 프로퍼티, 레코드 (0) | 2025.04.25 |
| [C#] 인터페이스 (0) | 2025.04.24 |