Search

컬렉션

class
구조
상태
완료
날짜
목차

컬렉션

컬렉션(Collection)이란 여러 개의 데이터를 그룹화하여 관리하는 데이터 구조를 말합니다.
이 부분에서는 제너릭 컬렉션에 대해 설명하겠습니다. 개인적으로, 비제너릭 컬렉션을 사용하는 모든 경우는 제너릭 컬렉션으로 대체할 수 있으며, 이는 프로그램의 안정성 및 유지보수 측면에서 더욱 유리하다고 생각합니다. 그래서, 비제너릭 컬렉션은 사용하지 않는 것이 좋다고 생각합니다.

배열과 컬렉션의 차이점

배열의 특성 및 장단점

특성

고정 크기 : 배열은 생성 시 지정된 크기를 가지며, 크기를 동적으로 변경할 수 없습니다.
인덱스 접근 : 배열 요소에는 인덱스를 통해 빠르게 접근할 수 있습니다.

장점

성능 : 인덱스를 사용한 직접적인 접근 방식으로 인해 빠른 데이터 접근 속도를 제공합니다.
개인적인 의견이지만, 사실상 데이터 접근 속도의 차이점은 없다고 봐도 무방합니다.
간단한 사용법 : 기본적인 데이터 구조로서 사용법이 단순하고 직관적입니다.
이 부분에서 간단한 사용법은 유지보수에 큰 장점을 가지고 있습니다. 바로 C#에서 배열을 사용하면 GC가 발생하는 경우를 간접적으로 막을 수 있다는 점입니다.
배열의 특성상 크기를 미리 지정하면 개발자에게 새로운 메모리 할당 및 해제가 어렵게됩니다. 즉, 이는 GC.Alloc이 발생하기 어렵게 만듭니다. (가변 배열은 제외!)
개인적인 견해로, '단순하다', '직관적이다'는 말이 '유지보수가 용이하다'는 의미로 해석됩니다. 그래서 배열이 직관적이고 컬렉션을 사용할 필요가 없는 경우, 배열을 사용하는 것이 유지보수에 더 편리하다고 생각합니다.

단점

유연성 부족 : 크기가 고정되어 있어서, 데이터 집합의 크기가 변경될 필요가 있는 경우에는 부적합할 수 있습니다.

컬렉션의 특성 및 장단점

특성

동적 크기 : 대부분의 컬렉션은 데이터를 추가하거나 제거함에 따라 크기가 동적으로 변경됩니다.
풍부한 기능 : 요소 추가, 삭제, 검색 등 다양한 작업을 수행할 수 있는 메서드를 제공합니다.

장점

유연성 : 데이터 항목의 추가와 제거가 자유롭게 이루어질 수 있어, 동적 데이터 관리에 적합합니다.
기능성 : 다양한 유형의 컬렉션이 제공되며, 각각의 컬렉션은 특정 사용 사례에 최적화된 기능을 제공합니다.

단점

성능 : 일반적으로 배열보다 데이터 접근 및 관리에 있어서 추가적인 오버헤드가 발생할 수 있습니다.
이 부분은 개인적인 의견이지만, 사실상 배열과 차이가 없습니다.

컬렉션의 공통 인터페이스

C#에서의 컬렉션들은 다양한 인터페이스를 상속받고 있지만, 가장 중요한 두 가지 인터페이스를 공통적으로 상속받고 있습니다.

ICollection<T>

컬렉션 클래스들이 공통으로 구현해야 하는 기본적인 메서드와 속성을 정의합니다.

Add(T item)

설명 : 컬렉션에 요소를 추가합니다. 컬렉션이 특정 용량에 도달했을 경우, 내부적으로 용량을 자동으로 확장할 수 있습니다.

Clear()

설명 : 컬렉션 내부의 모든 요소를 삭제하여 컬렉션을 비웁니다. 이 작업 후, 컬렉션의 Count는 0이 됩니다.

Contains(T item)

설명 : 지정된 요소가 컬렉션 내에 존재하는지를 검사합니다. 이 메서드는 요소의 존재 유무를 반환하며, 컬렉션 내부의 요소와 지정된 요소 간의 동등성 비교를 수행합니다.

CopyTo(T[] array, int arrayIndex)

설명 : 컬렉션 내의 모든 요소를 지정된 배열의 특정 인덱스부터 시작하여 복사합니다.

Remove(T item)

설명 : 컬렉션에서 첫 번째로 발견되는 지정된 요소를 삭제합니다. 요소가 성공적으로 제거되면 true를 반환하고, 그렇지 않으면 false를 반환합니다. 컬렉션에서 해당 요소를 찾지 못한 경우에는 아무런 작업도 수행하지 않습니다.

Count

설명 : 컬렉션 내의 요소 개수를 나타냅니다.

IsReadOnly

설명: 이 속성이 true인 경우, 컬렉션은 수정할 수 없으며 Add, Remove, Clear와 같은 메서드를 사용하여 컬렉션을 변경할 수 없습니다. 읽기 전용 컬렉션은 주로 데이터의 불변성을 유지하고자 할 때 사용됩니다.

IEnumrable<T>

IEnumerable<T>는 C#에서 제네릭 컬렉션을 순회하기 위한 인터페이스입니다.
이 내용은 유니티의 코루틴과 연관되어 있습니다. 이를 이해한 후에는 아래에 있는 코루틴 관련 페이지를 읽어보시면, 많은 도움이 될 것입니다.

GetEnumerator 반환 값 IEnumerator<T>

설명 : 컬렉션을 순회하기 위한 열거자(Enumerator) 객체를 반환하는 메서드입니다. 이 메서드는 IEnumerator<T> 인터페이스를 구현하는 객체를 반환하며, 이 객체를 통해 컬렉션의 요소를 순차적으로 접근할 수 있습니다.

IEnumrator<T>

Current : 열거자의 현재 위치에 있는 요소를 반환합니다.
MoveNext : 열거자를 컬렉션의 다음 요소로 이동시킵니다. 더 이상 이동할 요소가 없으면 false를 반환합니다.
Reset : 열거자를 초기 위치(컬렉션의 첫 번째 요소 앞)으로 되돌립니다.
// List<int> 생성 및 초기화 List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; // List<int>의 GetEnumerator() 메서드를 사용하여 IEnumerator<int> 얻기 IEnumerator<int> enumerator = numbers.GetEnumerator(); // MoveNext()를 사용하여 컬렉션을 순회 while (enumerator.MoveNext()) { // 현재 요소에 접근하기 위해 Current 프로퍼티 사용 int currentElement = enumerator.Current; // 현재 요소 출력 Console.WriteLine(currentElement); }
C#
복사

C# 컬렉션의 다양한 종류

리스트 (List)

설명 : List<T>는 크기를 동적으로 변경할 수 있는 배열입니다. 요소의 추가, 삭제 등이 자주 발생할 때 유용합니다.
List<Item> inventory = new List<Item>(); inventory.Add(new Item("Sword")); inventory.Add(new Item("Shield"));
C#
복사
사용시 주의 사항 : 리스트를 사용할 때, 신입 개발자들이 가장 자주 범하는 실수 중 하나는 가비지 컬렉션(GC)을 고려하지 않고 코드를 작성하는 것입니다.
메모리 재할당 및 복사 : 크기 조정이 일어날 때마다 발생하는 메모리 재할당과 요소 복사는 시간과 자원을 소모하는 작업입니다.
이 부분은 작아 보일 수 있지만, LoadManager와 관련된 Load 요소 중 하나를 구현할 때, 수백 또는 수천 개의 메모리 재할당이 발생할 수 있습니다.
가비지 컬렉션 오버헤드 : 빈번한 크기 조정으로 인해 많은 임시 배열이 생성되고, 이는 GC가 더 자주 발생하게 만들 수 있습니다.
해결법
초기 용량 지정: 가능하다면, List<T>를 생성할 때 예상되는 최대 요소 수를 기반으로 초기 용량을 지정합니다.
이 외에도 초기 용량을 지정함으로써 빈번한 메모리 할당을 줄일 수 있습니다. 그러나, 용량을 과도하게 지정하면 더 나쁜 결과를 초래할 수 있으므로 적절한 크기의 초기 용량을 설정해야 합니다.
List<int> numbers = new List<int>(100);
C#
복사
용량 확장 로직 이해 : List<T>의 용량이 자동으로 증가하는 방식을 이해하고, 대량의 데이터를 다룰 때는 이를 고려하여 적절한 시점에 Capacity 속성을 조정할 수 있습니다.

LinkedList<T>

설명 : LinkedList<T> 이중 연결 리스트를 구현한 것입니다. 이중 연결 리스트는 각 요소가 앞뒤 요소에 대한 참조를 가지고 있어, 리스트의 양방향 탐색이 가능합니다. LinkedList<T>는 요소의 동적 추가 및 제거가 자주 발생하고, 순차적 접근이 주로 이루어질 때 유용하게 사용될 수 있습니다. 배열이나 List<T>와 달리, LinkedList<T>에서는 요소의 인덱스 접근이 제공되지 않아, 특정 요소에 접근하기 위해서는 처음부터 노드를 순회해야 합니다.
LinkedList<string> commands = new LinkedList<string>(); // 특정 조건에서 명령어 제거 commands.Remove("Jump"); // 해당 명령어를 찾는 과정 최악의 경우 O(N), 삭제 O(1) // 명령어를 뒤로 되돌리거나 재실행 commands.RemoveLast(); // 마지막 명령어 제거 O(1) commands.AddFirst("명령어"); // 제거된 명령어를 다시 실행 목록에 추가 O(1).
C#
복사
사용시 주의 사항 : LinkedList<T>는 순차적 접근이 주로 필요하거나, 리스트의 중간에서 자주 삽입 및 삭제가 발생하는 경우 유용합니다. 하지만, 빈번한 인덱스 접근이 필요한 경우나 메모리 사용량이 중요한 경우 List<T>가 더 효율적일 수 있습니다.
LinkedList의 특성상 앞, 뒤 노드를 기억해야 하므로, List보다 약간 더 많은 메모리를 사용합니다.

딕셔너리 (Dictionary)

설명 : Dictionary<TKey, TValue>는 키와 값의 쌍으로 데이터를 저장합니다. 키를 통해 빠르게 값에 접근할 수 있어서 검색이 많이 필요한 경우 유용합니다.
Dictionary<string, int> playerScores = new Dictionary<string, int>(); playerScores.Add("Player1", 100); playerScores.Add("Player2", 50);
C#
복사

해시셋 (HashSet)

설명 : HashSet<T>는 유일한 요소만을 저장하는 컬렉션입니다. 요소의 중복을 허용하지 않고, 빠른 검색을 필요로 할 때 사용합니다.
HashSet<string> acquiredItems = new HashSet<string>(); acquiredItems.Add("Magic Sword");
C#
복사
딕셔너리, 해쉬 셋 사용시 공통 주의 사항
해시 셋과 딕셔너리를 사용할 때 주의할 점에 대해 설명하겠습니다. 특히, Key를 구조체로 직접 만들어 사용하는 경우입니다.
동등한 해시 코드 반환 : 같은 해시 코드를 반환하면 Equals를 통해 다시 한 번 같은 버킷에서 내부 처리 로직이 실행됩니다. 그러므로, 동일한 해시 코드를 최대한 반환하지 않는 것이 중요합니다.
해결법 : HashCode.Combine 사용
해시 값을 직접 지정하는 것보다는 해당 함수를 사용하는 것이 바람직합니다.
public override int GetHashCode() { return HashCode.Combine(Part1, Part2); }
C#
복사

큐 (Queue)

설명 : Queue<T>는 선입선출(FIFO) 구조로, 요소가 추가된 순서대로 제거됩니다. 데이터를 순차적으로 처리해야 할 때 사용합니다.
Queue<string> gameEvents = new Queue<string>(); gameEvents.Enqueue("Start Level"); gameEvents.Enqueue("Spawn Enemy");
C#
복사

스택 (Stack)

설명 : Stack<T>는 후입선출(LIFO) 구조로, 가장 마지막에 추가된 요소가 먼저 제거됩니다. 나중에 발생한 것을 먼저 처리해야 할 때 유용합니다.
// 값 형식으로 구현하는게 좋음 // 클래스의 경우 Clone과 같은 새 인터턴스 생성 과정이 필요. Stack<PlayerPosition> undoStack = new Stack<PlayerPosition>(); // 현재 위치를 스택에 푸시하여 이동 전 상태를 저장 //Move 메소드 ... undoStack.Push(CurrentPosition); ... //Undo 메소드 // 마지막 이동을 스택에서 팝하여 이전 위치로 되돌림 ... CurrentPosition = undoStack.Pop(); ...
C#
복사

SortedDictionary<TKey, TValue>

설명 : SortedDictionary<TKey, TValue>키에 따라 자동으로 정렬되는 키/값 쌍의 컬렉션입니다. 이 컬렉션은 레드-블랙 트리를 사용하여 구현되어 있으며, 요소의 추가와 검색에 있어서 효율적인 성능을 제공합니다. 키는 유일해야 하며, 자동으로 정렬됩니다.
SortedDictionary<DateTime, int> playerScoresByTime = new SortedDictionary<DateTime, int>(); playerScoresByTime.Add(DateTime.Now, 100); playerScoresByTime.Add(DateTime.Now.AddMinutes(5), 150);
C#
복사

SortedSet<T>

설명 : SortedSet<T>는 유일한 요소만을 저장하며, 모든 요소가 자동으로 정렬되는 컬렉션입니다. 이는 내부적으로 레드-블랙 트리를 사용하여 구현되어 있으며, 요소의 추가, 삭제, 검색 등의 작업을 효율적으로 수행할 수 있습니다.
SortedSet<int> uniqueItemIDs = new SortedSet<int>(); uniqueItemIDs.Add(1001); uniqueItemIDs.Add(1003);
C#
복사
현재 레드-블랙 트리를 사용하여 구현되어 있다고 언급되었지만, 실제로는 내부적으로 B트리로 구현되어 있을 수 있습니다. C#에서는 내부 구현 코드를 공개하지 않아 확실하지 않습니다. 그러나 자동 정렬과 같은 기능은 일반적으로 RB트리 또는 B트리를 이용하여 구현되므로, 이를 추론할 수 있습니다.
SortedDictionary, SortedSet 사용시 공통 주의 사항
요소 타입은 IComparable 인터페이스를 구현해야 하며, 요소 간의 비교가 가능해야 합니다.
IComparable 인터페이스를 구현했는지를 컴파일 시점에 체크하지 않습니다. 대신, 해당 타입의 인스턴스들을 정렬하려고 시도할 때, 런타임에 InvalidOperationException 또는 비슷한 예외가 발생할 수 있습니다.
public class MyData { public int Value { get; set; } // IComparable 인터페이스를 구현하지 않음 } var myDataSet = new SortedSet<MyData>(); // 실행 시점에 오류 발생
C#
복사