5. Collection 사용
Collection은 일반적으로 다수의 값을 저장하기 위한 용도로 사용되며 .NET에서는 Collection과 관련한 여러 Type을 정의하고 있습니다.
Namespace | Type | 설명 |
System.Collections | IEnumerable IEnumerable<T> |
Collection에 의해 사용되는 인터페이스와 기반 |
System.Collections.Generic | List<T> Dictionary<T> Queue<T> Stack<T> |
generic type 매개변수를 통해 Collection에서 다룰 형식을 지정 |
System.Collections.Concurrent | BlockingCollection ConcurrentDictionary ConcurrentQueue |
멀티스레드에서 사용 |
System.Collections.Immutable | ImmutableArray ImmutableDictionary ImmutableList ImmutableQueue |
원본값이 바뀌지 않는 불변전용, 단 새로운 인스턴스를 생성하는 과정에서는 값이 바뀔 수 있음 |
● Collection의 구현
모든 Collection은 ICollection인터페이스를 구현하고 있으므로 Count속성을 통해 얼마나 많은 객체를 Collection이 가지고 있는지를 확인할 수 있습니다.
List<int> li = new();
li.Add(1);
li.Add(2);
li.Add(3);
int count = li.Count;
Console.WriteLine(count);
//3
또한 모든 Collection은 IEnumerable인터페이스를 구현하고 있으므로 foreach구문을 통해 요소를 나열할 수 있습니다. 이를 위해 Collection은 IEnumerator를 구현하는 GetEnumerator() 메서드를 가지고 있고 반환되는 객체는 Collection 탐색을 위한 MoveNext와 Reset메서드와 Collection에서 현재 Item을 의미하는 Current속성을 가지게 됩니다.
List<int> li = new();
li.Add(1);
li.Add(2);
li.Add(3);
foreach(var item in li)
{
Console.WriteLine(item);
}
//1
//2
//3
● Collection의 가용성 명시
.NET 1.1 이후로 StringBuilder는 EnsureCapacity()라는 메서드를 제공하여 내부에 저장되는 배열의 크기를 미리 지정할 수 있도록 하였습니다. 이는 최종적인 문자열의 크기를 명시함으로서 통상 나중에 추가되는 문자로인해 배열의 크기가 수시로 달라지는 동작을 유발하지 않도록 하기 위함입니다. 다시 말해 문자열의 크기만 사전에 가늠할 수 있다면 성능적으로 좀더 유리한 방향으로 사용할 수 있는 것입니다.
.NET Core 2.1 이후부터는 Dictionary<T>나 HashSet<T>등에도 EnsureCapacity()메서드를 제공하였고 .NET 6에 와서는 List<T>, Queue<T>, Stack<T>, Set<T>에도 EnsureCapacity() 메서드를 지원하고 있습니다. 따라서 다음과 같이 Collection이 가질 수 있는 크기를 지정할 수 있게 되었습니다.
List<int> li = new();
li.EnsureCapacity(100);
//Collection의 크기를 100개로 한정함
● Collection의 선택
Collection에서는 선택 가능한 List, Dictionary, Stack, Queue등 다양한 형식의 Collection이 존재하며 이들 Collection은 각각의 특징에 따라 목적에 맞게 사용되어야 합니다.
List는 IList<T>를 구현한 형식이며
namespace System.Collections.Generic
{
[DefaultMember("Item")] //indexer
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable
{
T this[int index] { get; set; }
int IndexOf(T item);
void Insert(int index, T item);
void RemoveAt(int index);
}
}
IList<T>는 ICollection<T>로 부터 상속되었으므로 Count속성과 Item을 collection의 끝에 놓는 Add(), Item을 원하는 위치에 넣는 Insert(), 원하는 위치의 Item을 제거하는 RemoveAt() 메서드 등을 가지고 있습니다.
List는 Collection에서 Iitem의 정렬 상태를 직접 제어하고자 하는 경우에 유용하게 사용될 수 있으며 각각의 Item은 자동으로 할당되는 고유한 index (혹은 position)값을 가집니다. 여기서 index는 중요한데 index에 따라 각각의 item을 구분할 수 있으므로 서로 중복되는 item을 담을 수 있고 이 index에 따라 item이 정렬될 수 있습니다. (참고로 index는 0부터 시작합니다.)
예를 들어 아래와 같이 List가 구성된 상태에서
Index | Item |
0 | 홍길동 |
1 | 홍길석 |
2 | 홍길영 |
'홍길석'과 '홍길영'사이에 '홍길윤'이라는 새로운 Item을 추가하게 되면
Index | Item |
0 | 홍길동 |
1 | 홍길석 |
2 | 홍길윤 |
3 | 홍길영 |
뒤이어 오는 Index값은 그만큼을 증가된 값으로 가지게 됩니다. 따라서 새로운 Item을 넣거나 제거하는 동작은 Index의 변화에 영향을 줄 수 있음을 알고 있어야 합니다.
List<string> facebook = new();
facebook.Add("홍길동");
facebook.Add("홍길순");
facebook.Add("홍길석");
Console.WriteLine("현재 값");
foreach(var item in facebook)
{
Console.WriteLine(item);
}
facebook.RemoveAt(1); //2번째 삭제
facebook.Insert(1, "홍길영"); //2번째 새로운 Item 추가
Console.WriteLine("변경 값");
foreach(var item in facebook)
{
Console.WriteLine(item);
}
// 현재 값
// 홍길동
// 홍길순
// 홍길석
// 변경 값
// 홍길동
// 홍길영
// 홍길석
Dictionary는 각각의 Item값이 고유한 sub value값을 가지는 경우 유용하며 이는 Collection에서 필요한 값을 빨리 찾기 위한 Key로서 사용됩니다. 예를 들어 특정 연락처 목록을 가지는 경우라면 이름(중복되지만 않는다면)같은 값을 Key로서 사용할 수 있는 것입니다. 여기서 Key는 사전에서 Index로 생각해 볼 수 있는데 Key는 항상 정렬된 상태이므로 Key를 사용하면 원하는 것을 빠르게 찾을 수 있습니다. '홍길동'을 찾기 위해 '김삿갓'처럼 'ㄱ'으로 시작하는 순서부터 검색할 필요가 없는 것과 마찬가지입니다.
Dictionary는 IDictionary<TKey, TValue> Interface를 구현하고 있으며
namespace System.Collections.Generic
{
[DefaultMember("Item")] //Indexer
public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable
{
TValue this[TKey key] { get; set; }
ICollection<TKey> Keys { get; }
ICollection<TValue> Values { get; }
void Add(TKey key, TValue value);
bool ContainsKey(TKey key);
bool Remove(TKey key);
bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value);
}
}
Dictionary의 Item은 KeyValuePair<TKey, TValue>라는 값 형식의 구조체이고 여기서 TKey는 key, TValue는 value의 Type에 해당합니다.
namespace System.Collections.Generic
{
public readonly struct KeyValuePair<TKey, TValue>
{
public KeyValuePair(TKey key, TValue value);
public TKey Key { get; }
public TValue Value { get; }
[EditorBrowsable(EditorBrowsableState.Never)]
public void Deconstruct(out TKey key, out TValue value);
public override string ToString();
}
}
Dictionary<int, string> facebook = new();
facebook.Add(0, "홍길동");
facebook.Add(1, "홍길영");
facebook.Add(2, "홍길석");
//현재값
foreach(var item in facebook)
{
Console.WriteLine($"{item.Key}: {item.Value}");
}
// 0: 홍길동
// 1: 홍길영
// 2: 홍길석
//Key 검색
Console.WriteLine($"{facebook[1]}");
//홍길영
Stack은 LIFO(Last-In, Firat-Out)구조의 동작을 구현하는데 알맞은 Collection입니다. Stack에서는 그저 가장 상위에 있는 하나의 Item에 접근하고 삭제할 수 있는데, 전체 Item을 순회하는 것도 가능하지만 최상위 Item외에 Stack에 있는 다른 순서의 Item에는 접근할 수 없습니다.
LIFO동작은 문서편집기와 같은 동작으로 설명될 수 있습니다. 편집기에서 수행한 사용자의 동작을 Stack에 쌓고 사용자가 'Ctrl + Z'키를 누르면 Stack에 있는 가장 최근의 동작을 가져와 현재 상태에 반영시키게 되는 구조인 것입니다.
Queue는 FIFO(First-In, Firt-Out)동작을 구현하는데 적합한 구조이며 가장 먼저 들어온 Item에만 접근하거나 해당 Item을 삭제할 수 있습니다. 물론 Queue 또한 전체 Item을 열거할 수 있지만 가장 먼저 들어온 Item이 아닌 다른 순서의 Item에는 임의로 접근할 수 없다는 차이가 존재합니다.
Queue<string> facebook = new();
facebook.Enqueue("홍길동");
facebook.Enqueue("홍길영");
facebook.Enqueue("홍길석");
//현재 값
foreach(var item in facebook)
{
Console.WriteLine(item);
}
//값 가져오기 & Item제거
string name = facebook.Dequeue();
Console.WriteLine(name);
//현재 값
foreach(var item in facebook)
{
Console.WriteLine(item);
}
//값 가져오기 (삭제x)
name = facebook.Peek();
Console.WriteLine(name);
참고로 .NET 6에서는 PriorityQueue라는 것이 추가되었는데 각 Item의 위치에 더해 우선순위 값을 추가로 가지는 구조입니다.
PriorityQueue<string, int> facebook = new();
facebook.Enqueue("홍길동", 3);
facebook.Enqueue("홍길영", 1);
facebook.Enqueue("홍길석", 2);
//현재 값 확인
foreach(var item in facebook.UnorderedItems)
{
Console.WriteLine(item);
}
//(홍길영, 1)
//(홍길동, 3)
//(홍길석, 2)
string name = facebook.Dequeue(); //우선 순위에 따라 홍길영이 먼저 추출됨
Console.WriteLine(name);
//홍길영
//현재 값 확인
foreach(var item in facebook.UnorderedItems)
{
Console.WriteLine(item);
}
//(홍길석, 2)
//(홍길동, 3)
마지막으로 Sets은 2개의 Collection을 다루는 데 사용되며, 이를 위해 HashSet이나 SortedSet이 마련되어 있습니다. 이를 이용해 HashSet에서 처음 Collection에서 중복된 Item을 걸러내고 다른 Collection을 통해 그 결과를 반영합니다.
string[] arr1 = { "홍길동", "홍길순", "홍길영", "홍길동", "홍길석" };
Console.WriteLine(string.Join(",", arr1)); //홍길동만 중복됨
//걸러내기
var h = new HashSet<string>(arr1);
//홍길동을 제외한 다른 이름만 출력
string[] arr2 = h.ToArray();
Console.WriteLine(string.Join(",", arr2));
위에서 설명한 각각의 Collection은 새로운 Item을 추가하거나 제거하기 위한 다음과 같은 이름을 가지고 있습니다. 저마다 메서드 이름은 다르지만 용도는 비슷하게 사용됩니다.
Collection | 추가 메서드 | 삭제 메서드 | 설명 |
List | Add, Insert | Remove, RemoveAt | List의 Item은 integer형식의 Index값을 가지고 있고 이를 기준으로 Item이 정렬되어 있습니다. Add는 Item을 List의 마지막에 추가하며 Insert는 임의의 위치에 Item을 추가할 수 있도록 합니다. |
Dictionary | Add | Remove | Dictionary의 Item은 Index값을 가지고 있지 않으며 따라서 별도의 정렬과정을 거치지 않습니다. 검색을 위한 Key는 ContainsKey 메서드를 통해 사용여부를 확인할 수 있습니다. |
Stack | Push | Pop | Push는 Stack의 가장 상위에 Item을 추가합니다. 따라서 가장 처음 추가된 Item은 Stack의 가장 아래에 위치하게 됩니다. 또한 Pop은 가장 상위의 Item을 끄집어내서 제거하게 됩니다. 제거없이 Value값을 확인하려면 Pop대신 Peek()메서드를 사용합니다. |
Queue | Enqueue | Dequeue | Enqueue는 Item을 Queue의 마지막에 추가합니다. 따라서 가장 처음 추가된 item은 Queue의 가장 처음에 위치하게 됩니다. Dequeue는 가장 처음(상위)의 Item을 끄집어내 제거하게 되는데 제거과정 없이 Value값을 확인하려면 Dequeue대신 Peek()메서드를 사용합니다. |
● Collection 정렬
List<T> Class는 기본적으로 Index를 기준으로 한 정렬을 수행하지만 Sort()메서드를 통해 임의의 정렬을 수행할 수 있습니다.(다만 각 Item의 Index값은 바뀔 수 있습니다.) List의 String값이나 다른 내장된 Type의 정렬은 별도의 노력이 없이도 정렬이 가능하지만 직접 생성한 Type으로 Collection이 만들어진 경우 정렬을 위해서는 IComparable라는 이름의 Interface를 구현해야 합니다. 이와 관련한 내용은 아래 글에서 확인할 수 있습니다.
[.NET/C#] - [C#] 인터페이스(Interface)와 상속(Inheriting)
컬렉션 중에는 자동으로 Item을 정렬하는 기능을 갖는 컬렉션이 몇가지 있고 정렬된다는 특징은 서로 비슷하지만 어떤 것을 선택하느냐에 따라 메모리와 성능에 영향을 주는 경우가 있으므로 해결하고자 하는 상황에 맞는 가장 적절한 컬렉션을 사용할 수 있도록 해야 합니다.
SortedDictionary<TKey, TValue> | Key에 의해 정렬되며 Key와 Value을 쌍으로 한 Collection을 표현합니다. |
SortedList<TKey, TValue> | Key에 의해 정렬되며 Key와 Value을 쌍으로 한 Collection을 표현합니다. |
SortedSet<T> | 정렬된 상태를 유지하는 중복되지 않는 객체의 Collection을 표현합니다. |
당연한 이야기지만 Stack<T>과 Queue<T>는 정렬될 수 없습니다.
● 그 외 컬렉션(Collection)
bit값을 사용하는 작은 Collection으로 System.Collections.BitArray가 있습니다. 이것은 Booleans을 표현하는데 만약 true면 bit가 1이 됨을 의미하며 false라면 bit가 0임을 의미합니다.
System.Collections.Generics.LinkedList<T>는 이중으로 연결된 List를 표현하는데 List의 모든 Item은 이전과 다음 Item의 참조를 가집니다. 이러한 특징으로 인해 값이 추가되거나 삭제되는 경우 참조하는 포인터만 변경함으로써 List<T>보다는 더 나은 삽입/제거의 성능을 제공해 줄 수 있습니다. 다만 임의 접근이 안되므로 검색에서는 더 느릴 수 있습니다.
● 불변 컬렉션(Immutable Collection)
필요하다면 Collection을 불변으로 만들 수 있으며 이는 Collection의 Item을 변경할 수 없다는 것을 의미합니다.
불변 컬렉션은 우선 System.Collections.Immutable 네임스페이스를 Import 하는 것으로 시작합니다. 그러면 IEnumerable<T>를 구현하는 모든 Collection은 불변 List나 dictionary 등으로 변경할 수 있는 6개의 확장 메서드가 추가로 주어지게 됩니다.
List<string> facebook = new();
facebook.Add("홍길동");
facebook.Add("홍길영");
facebook.Add("홍길석");
ImmutableList<string> ImmutableFacebook = facebook.ToImmutableList<string>(); //Immutable List변환
ImmutableFacebook.Add("홍길순");
//상기 추가가 반영되지 않음
foreach(var item in ImmutableFacebook)
{
Console.WriteLine(item);
}
//홍길동
//홍길영
//홍길석
위 예제는 새롭게 추가된 '홍길순'이라는 Item을 반영하지 않고 있지만 아래와 같이 새로운 ImmutableList를 만들어내는 방법을 통해서는 필요한 Item을 추가할 수 있습니다.
ImmutableList<string> newFacebook = ImmutableFacebook.Add("홍길순");
● IEnumerable<T>사용시 성능 문제
다양한 Collection Type을 다루기 위해 아래와 같이 IEnumerable<T>를 직접 사용하는 경우
void MyCollection<T>(IEnumerable<T> collection)
{
//어떤 처리
}
Parameter를 통해 List나 queue, stack등 IEnumerable<T>를 구현하는 모든 Collection을 전달할 수 있게 됨으로써 위와 같은 메서드는 상당한 유연성을 발휘하게 됩니다. 그러나 동시에 이와 같은 구현은 성능적으로 문제가 될 수 있습니다.
IEnumerable<T>에서 발생할 수 있는 성능 문제는 내부에서 Item을 열거할 때 객체를 힙(Heap)에 할당해야 한다는 것에서 발생합니다. 이를 피하기 위해서라도 다음과 같이 구체적인 Type을 명시하는 것이 좋습니다.
void MyCollection<T>(List<T> collection)
{
//어떤 처리
}
List<T>는 자신의 열거 메서드인 GetEnumerator()메서드를 사용하는데 IEnumerable<T>에서의 GetEnumerator()메서드가 참조 타입을 반환하는 대신 List<T>의 GetEnumerator()메서드는 구조체를 반환합니다.
'.NET' 카테고리의 다른 글
[.NET] 닷넷 Type 사용하기 - 6. 네트워크 리소스 활용 (0) | 2022.06.26 |
---|---|
[.NET] 닷넷 Type 사용하기 - 5. index와 range 그리고 Span (0) | 2022.06.26 |
[.NET] 닷넷 Type 사용하기 - 3. 정규식(regular expressions) (0) | 2022.06.26 |
[.NET] 닷넷 Type 사용하기 - 2. 날짜와 시간 (0) | 2022.06.26 |
[.NET] 닷넷 Type 사용하기 - 1. 숫자, 문자열 (0) | 2022.06.26 |