Search
moon
sun

컬렉션

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와 같은 메서드를 사용하여 컬렉션을 변경할 수 없습니다. 읽기 전용 컬렉션은 주로 데이터의 불변성을 유지하고자 할 때 사용됩니다.

IEnumerable<T>

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

GetEnumerator 반환 값 IEnumerator<T>

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

IEnumerator<T>

•
Current : 열거자의 현재 위치에 있는 요소를 반환합니다.
•
MoveNext : 열거자를 컬렉션의 다음 요소로 이동시킵니다. 더 이상 이동할 요소가 없으면 false를 반환합니다.
•
Reset : 열거자를 초기 위치(컬렉션의 첫 번째 요소 앞)으로 되돌립니다.
// List<int> 생성 및 초기화 List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; // List<int>의 GetEnumerator() 메서드를 사용하여 IEnumerator<int> 얻기 // List<int>.Enumerator는 struct라, 아래처럼 인터페이스로 받으면 박싱이 발생합니다. 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를 통해 다시 한 번 같은 버킷에서 내부 처리 로직이 실행됩니다. 그러므로, 동일한 해시 코드를 최대한 반환하지 않는 것이 중요합니다.
◦
Equals와 GetHashCode 재정의 : 구조체를 Key로 쓰면서 이 둘을 재정의하지 않으면, 기본 ValueType.Equals가 리플렉션 기반으로 비교하기 때문에 매우 느리고 박싱까지 발생합니다. 구조체 Key를 쓸 거라면 IEquatable<T>까지 구현해주는 것이 좋습니다.
•
해결법 : 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#
복사
💡
.NET은 런타임 소스가 공개되어 있어서 이 부분은 확인이 가능합니다. SortedSet은 레드-블랙 트리로 구현되어 있고, SortedDictionary도 내부적으로 같은 트리를 사용합니다. 그래서 추가, 삭제, 검색이 모두 O(log N)이며, 정렬이 필요 없다면 Dictionary나 HashSet이 더 빠릅니다.
•
SortedDictionary, SortedSet 사용시 공통 주의 사항
◦
요소 타입은 IComparable 인터페이스를 구현해야 하며, 요소 간의 비교가 가능해야 합니다.
▪
IComparable 인터페이스를 구현했는지를 컴파일 시점에 체크하지 않습니다. 대신, 해당 타입의 인스턴스들을 정렬하려고 시도할 때, 런타임에 InvalidOperationException 또는 비슷한 예외가 발생할 수 있습니다.
public class MyData { public int Value { get; set; } // IComparable 인터페이스를 구현하지 않음 } var myDataSet = new SortedSet<MyData>(); // 실행 시점에 오류 발생
C#
복사