멈추지 않고 끈질기게
[C#] 딕셔너리(Dictionary) 본문
※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.
※ 해당 포스팅은 .Net 5.0 버전을 기준으로 작성되었습니다.
자료구조 중에는 인덱스 대신 키(Key) 값을 사용하여 데이터(Value)에 접근하는 '연관 배열'이 있습니다.
이번 포스팅에서는 C#에서 제공하는 연관 배열, 딕셔너리(Dictionary)에 대해 알아보겠습니다.
1. Dictionary의 특징
Dictionary는 Key와 Value를 한 쌍으로 다루는 배열입니다. 일반적인 배열과 동일해 보일 수 있으나,
Dictionary의 Key 값은 인덱스와 달리 선언시에 자료형을 지정합니다. 따라서 정수 뿐만 아니라 소수, 문자열 등
다양한 타입을 검색 기준으로 사용할 수 있습니다.
2. Dictionary의 선언 및 초기화
Dictionary는 System.Collelctions.Generic 네임스페이스에 정의되어 있으며 이를 참조하여 사용할 수 있습니다.
Dictionary의 선언 및 초기화는 다음과 같이 실행할 수 있습니다.
//선언 및 초기화
Dictionary<int, float> dic1;
dic1 = new Dictionary<int, float>();
//선언과 동시에 초기화
Dictionary<string, string> dic2 = new Dictionary<string, string>();
//선언과 동시에 초기화2
Dictionary<int, string> dic3 = new Dictionary<int, string>
{
{ 2023, "년" }, { 1, "월" }, { 27, "일"}
};
Dictionary는 Key와 Value의 타입을 각각 지정해주어야 합니다. 초기화 시에 초기 원소를 직접 삽입해 줄 수도 있습니다.
다만 이 경우, Key가 중복되지 않게 해야 합니다. Dictionary는 Key 중복을 허용하지 않기 때문에, 초기화 시에 중복되는 Key를 넣어주면 ArgumentException이 발생합니다.
3. Dictionary의 속성
Dictionary의 속성 값들은 다음과 같습니다.
Dictionary<int, string> dic = new Dictionary<int, string>{
{ 2023, "년" }, { 1, "월" }, { 27, "일" }
};
//원소의 갯수
Console.WriteLine($"원소의 갯수: {dic.Count}");
//키와 밸류 모음
List<int> keys = new List<int>(dic.Keys);
List<string> values = new List<string>(dic.Values);
for (int i = 0; i < dic.Count; i++)
Console.Write(keys[i] + values[i] + " ");
Console.WriteLine();
//IEqualityComparer
List<int> list = new List<int> { 1, 2, 2, 3, 3, 3, 4, 5, 5, 5 };
int[] arr = list.Distinct(dic.Comparer).ToArray();
PrintElements(arr, "배열의 원소: ");
(PrintElemetns는 모든 원소 출력용으로 개인적으로 작성한 함수입니다.)
Count는 Dictionary에 원소(Key와 Value 쌍)가 몇개 존재하는지 나타내는 값으로, 리스트나 큐, 스택의 그것과 동일합니다.
Keys, Values 속성은 각각 Dictionary의 Key와 Value의 모음을 반환합니다. 반환 타입이 Dictionary<T, T>.KeyCollection,
Dictionary<T, T>.ValueCollection 으로 다소 생소한데, 이 클래스들도 IEnumerable 인터페이스를 상속받기 때문에 리스트의 생성자로 사용할 수 있습니다. 상기 코드에서는 리스트의 생성자에 매개변수로 전달하여 리스트를 생성하였고, System.Linq 네임스페이스를 참조했다면 간단하게 뒤에 ToList(), ToArray() 함수를 이용하여 변환할 수도 있습니다.
Comparer 속성 값은 Dictionary가 사용하는 비교자를 반환합니다. Dictionary는 Key값의 중복을 허용하지 않기 때문에 IEqualityComparer 인터페이스를 상속받고 있습니다. 해당 비교자는 System.Linq에 정의된 중복 제거 함수 Distinct()에
매개변수로 전달하는 등으로 사용할 수 있습니다.
(다만 Distinct() 함수는 매개변수를 전달하지 않아도 해당 자료구조의 타입에 맞는 기본 비교자를 사용하여 중복 제거를 실행하고, 상기 코드에서도 dir.Comparer를 생략해도 같은 결과를 얻을 수 있습니다. Dictionary의 IEqualityComparer 를
받아서 어디에 사용해야 좋을지는 조금 더 공부가 필요할 듯 합니다.)
4. Dictionary 관련 함수
Dictionary와 관련된 함수들 중 자주 사용되는 것들을 알아보겠습니다.
Dictionary<int, string> dic = new Dictionary<int, string> {
{ 2020, "경자년" }, { 2021, "신축년" }
};
PrintElements(dic, "Dictionary의 원소: ");
//원소 추가
dic.Add(2022, "임인년");
PrintElements(dic, "Dictionary의 원소: ");
//원소 추가(성공)
int id = 2023;
if (dic.TryAdd(id, "계묘년"))
PrintElements(dic, "Dictionary의 원소: ");
else
Console.WriteLine($"{id}년에 대한 데이터가 이미 존재합니다.");
//원소 추가(실패)
id = 2023;
if (dic.TryAdd(2023, "새해"))
PrintElements(dic, "Dictionary의 원소: ");
else
Console.WriteLine($"{id}년에 대한 데이터가 이미 존재합니다.");
Console.WriteLine();
//Value 가져오기(성공)
if (dic.TryGetValue(id, out string res1))
Console.WriteLine($"{id}년 검색 결과: {res1}");
else
Console.WriteLine("{id}년에 대한 데이터가 존재하지 않습니다. ");
//Value 가져오기(실패)
id = 2024;
if (dic.TryGetValue(id, out string res2))
Console.WriteLine($"{id}년 검색 결과: {res2}");
else
Console.WriteLine($"{id}년에 대한 데이터가 존재하지 않습니다. ");
Console.WriteLine();
//원소 검색(Key)
id = 2023;
Console.WriteLine($"해당 Key를 가지고 있는가({id})? : {dic.ContainsKey(id)}");
id = 2025;
Console.WriteLine($"해당 Key를 가지고 있는가({id})? : {dic.ContainsKey(id)}");
//원소 검색(Value)
string val = "계묘년";
Console.WriteLine($"해당 Value를 가지고 있는가({val})? : {dic.ContainsValue(val)}");
val = "갑진년";
Console.WriteLine($"해당 Value를 가지고 있는가({val})? : {dic.ContainsValue(val)}");
Console.WriteLine();
//원소 제거
dic.Remove(2023);
PrintElements(dic, "Dictionary의 원소: ");
//원소 제거2
id = 2022;
if(dic.Remove(id, out string res3))
Console.WriteLine($"{id}년에 대한 데이터를 삭제하였습니다(Value: {res3})");
else
Console.WriteLine($"{id}년에 대한 데이터가 존재하지 않습니다.");
//원소 전부 제거
dic.Clear();
PrintElements(dic, "Dictionary의 원소: ");
Add() 함수를 통해 Dictionary에 원소를 추가할 수 있습니다. 매개변수로 Key와 Value를 모두 넘겨야 하며, 이 때 Key가
이미 Dictionary에 존재하는 값이라면 ArgumentException을 발생시키므로 주의해야 합니다.
TryAdd() 함수를 사용하여 좀 더 안전하게 원소를 추가할 수도 있습니다. 매개변수는 Add()와 동일하게 전달하며, 이 때 원소 추가에 성공하면 true를, 실패하면(Key 중복) false를 반환합니다. 상기 코드처럼 조건식으로 사용하면 중복 시의 예외처리까지 설정해줄 수 있습니다.
Dictionary의 Value는 배열이나 리스트에서 인덱스를 통해 원소에 접근하듯이 가져올 수 있습니다. 상기 코드에서 2023년에 대한 데이터를 가져오려면, dic[2023] 과 같이 간단하게 가져올 수 있습니다. 다만 배열이나 리스트에서 인덱스 범위를 벗어나는 값을 입력시 IndexOutOfRangeException을 발생시키는 것과 마찬가지로, Dictionary에 존재하지 않는 Key값을
넣을 경우 KeyNotFoundException을 발생시킵니다.
TryGetValue() 함수를 이용하면 이러한 문제를 예방할 수 있습니다. Key 값을 매개변수로 전달하고, 해당 Key가 Dictionary에 존재할 경우 out 키워드를 통해 Value 값을 받을 수 있습니다. 함수는 해당 Key가 존재할 경우 true를, 존재하지 않을 경우 false를 반환합니다. 상기 코드와 같이 조건식 안에 넣어서 Key가 존재하지 않을 경우에 대한 예외처리까지 설정해줄 수 있습니다.
ContainsKey(), ContainsValue() 함수를 통해 특정 값이 Dictionary의 Key 또는 Value에 존재하는지 알아볼 수 있습니다. 각각 Key, Value값을 전달 시 해당 Key, Value가 존재한다면 true, 아니라면 false를 반환합니다.
Remove() 함수를 통해 Dictionary의 원소를 제거할 수 있습니다. Key 값을 매개변수로 넘기면 해당 Key와 Value 값을 함께 삭제합니다. Key 값만 전달할 경우, 해당 Key가 존재하지 않는다면 아무일도 일어나지 않습니다.
두번째 매개변수로 out 키워드를 사용하면 좀 더 확실하게 결과를 확인할 수 있습니다. 이 버전의 Remove() 함수는 Key 값이 존재할 경우 true를 반환하고 out 변수를 통해 삭제되는 Value값을 전달하며, 아니라면 false를 반환합니다. 상기 코드에서는 조건식 안에 넣어 Key 값이 존재한다면 삭제되는 Value값을 함께 출력하고, 없을 경우 출력할 문구를 따로 설정하였습니다.
Clear() 함수를 통해 모든 원소(Key 및 Value)를 제거할 수 있습니다.
추가로, Dictionary에서 모든 원소에 접근할때는 다른 배열이나 리스트와 조금 다른 점이 있습니다.
static void PrintElements<TKey, TValue>(Dictionary<TKey, TValue> dic, string ment)
{
Console.Write(ment);
if (dic.Count == 0)
{
Console.WriteLine("없음");
return;
}
foreach (KeyValuePair<TKey, TValue> item in dic)
Console.Write($"({item.Key}, {item.Value}) ");
Console.WriteLine();
}
원소 출력을 위해 작성한 PrintElements 함수입니다. foreach 구문을 보시면 KeyValuePair<TKey, TValue> 구조체 타입을
받고 있음을 알 수 있습니다. Dictionary의 원소는 단일 자료형이 아닌 Key와 Value의 쌍이기 때문에, KeyValuePair<TKey, TValue> 타입을 원소로 받은 뒤 여기에서 각각 Key, Value 값에 접근해야 합니다.
5. Dictionary의 장단점
Dictionary의 가장 큰 장점은 검색을 빠르게 수행할 수 있다는 점입니다. 배열이나 리스트에서도 인덱스를 통해 접근 시 O(1)의 속도로 접근할 수 있으나, 인덱스 값을 검색하는 데에 최대 O(n)의 시간을 소모하기 때문에 데이터를 자주 검색하는 데에 적합하다고 보기는 어렵습니다. 하지만 Dictionary는 Key값을 이용하여 접근하기 때문에, 해당 Key값만 전달받는다면 항상 O(1)의 시간으로 접근할 수 있습니다. 아래는 Dictionary를 사용하는 예시입니다.
class Item
{
public readonly int id;
public readonly string name;
public readonly float value;
public readonly int price;
public Item(int id, string name, float value, int price)
{
this.id = id;
this.name = name;
this.value = value;
this.price = price;
}
}
static void Main(string[] args)
{
Item hpPotion = new Item(1001, "체력 포션", 50.0f, 100);
Item mpPotion = new Item(1002, "마나 포션", 50.0f, 150);
Item spPotion = new Item(1003, "SP 포션", 5.0f, 200);
Dictionary<int, Item> itemDic = new Dictionary<int, Item>();
itemDic.Add(hpPotion.id, hpPotion);
itemDic.Add(mpPotion.id, mpPotion);
itemDic.Add(spPotion.id, spPotion);
}
상기 코드와 같이 Item 클래스를 정의하고, 각 아이템을 id를 통해 구분할 수 있게 해두면 아이템에 관한 로직을 짜기 매우 수월해집니다. 게임 실행 시 전반적인 로직을 담당하는 GameManager(가칭) 같은 곳에서 로딩시에 csv파일을 읽어들여 아이템에 대한 정보를 모두 Dictionary에 저장해 둔 뒤, 아이템을 습득, 사용 또는 판매할 때마다 해당 아이템에서 id만 전달받으면 Dictionary에서 필요한 정보를 찾아 사용할 수 있습니다.
다만 Dictionary는 잦은 검색에 최적화된 대신, 단순 데이터 저장용으로 사용하기엔 적합하지 않습니다. 데이터를 항상 Key와 Value의 쌍으로 저장하기 때문에, 단순히 데이터들의 합계나 평균을 구하는데 사용하기는 부적절합니다. 예를 들어 학교에서 학생들의 성적 및 신상 정보를 학번을 기준으로 저장하고 관리하려고 한다면 Dictionary가 최적의 선택지가 될 수 있지만, 단순히 전교생의 수학 점수 평균을 구한다고 한다면 오히려 Dictionary보다는 배열이 더 적절할 것입니다. 또한 몇몇 알고리즘에서 데이터들을 저장한 뒤 정렬하여 사용하는 방식을 쓰는데, Dictionary는 기본적으로 정렬 기능을 지원하지 않기 때문에 이러한 용도로 사용할 수 없습니다.
'C#' 카테고리의 다른 글
[C#] 대리자(Delegate) (0) | 2023.02.09 |
---|---|
[C#] IComparable 인터페이스, Comparison<T> 대리자 (0) | 2023.01.31 |
[C#] 큐(Queue)와 스택(Stack) (0) | 2023.01.26 |
[C#] 리스트(List) (0) | 2023.01.25 |
[C#] 배열(Array) (0) | 2023.01.20 |