Dictionary
Dictionary<TKey, TValue>
는 .NET에서 제공하는 제네릭 컬렉션 클래스로, 키-값 쌍을 효율적으로 저장하고 관리할 수 있는 자료 구조입니다. HashTable을 기반으로 하여 키를 사용한 빠른 검색, 삽입, 삭제 작업을 지원합니다. System.Collections.Generic
네임스페이스에 포함되어 있으며, 키를 통해 값에 접근하는 방식으로 데이터를 관리합니다.
Dictionary<TKey, TValue> 개요
Dictionary란 무엇인가?
- Dictionary는 키Key와 값Value 쌍으로 데이터를 저장하는 자료 구조입니다.
- 해시 테이블Hash Table을 기반으로 하여, 키를 사용한 빠른 데이터 접근이 가능합니다.
- 각 키는 고유해야 하며, 하나의 키는 하나의 값에 대응합니다.
Dictionary를 언제 사용해야 하는가?
- 빠른 검색이 필요한 경우: 키를 사용하여 값에 빠르게 접근해야 할 때.
- 키-값 매핑이 필요한 경우: 특정 키에 대한 값을 저장하고 조회해야 할 때.
- 데이터의 순서가 중요하지 않은 경우: 요소의 순서가 중요하지 않고, 키를 통해 데이터에 접근하면 되는 경우.
Dictionary의 기본 사용법
Dictionary<TKey, TValue>
를 사용하면 키와 값을 명시적으로 지정하여 타입 안전성을 보장받을 수 있습니다.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Dictionary<int, string> people = new Dictionary<int, string>();
people.Add(1, "Alice");
people.Add(2, "Bob");
people.Add(3, "Charlie");
Console.WriteLine($"FirstMember: {people[1]}");
// 요소 출력
foreach (KeyValuePair<int, string> person in people)
{
Console.WriteLine($"ID: {person.Key}, Name: {person.Value}");
}
// 출력:
// FirstMember: Alice
// ID: 1, Name: Alice
// ID: 2, Name: Bob
// ID: 3, Name: Charlie
}
}
Dictionary<TKey, TValue>
의 특징
- 빠른 검색 성능: 해시 테이블을 사용하므로, 평균적으로 O(1)의 시간 복잡도로 요소를 검색, 삽입, 삭제할 수 있습니다.
- 유일한 키: 모든 키는 고유해야 하며, 중복된 키를 사용할 수 없습니다.
- 타입 안전성: 제네릭 타입을 사용하여 키와 값의 타입을 명시적으로 지정할 수 있습니다.
장점
- 고성능: 대량의 데이터를 관리할 때도 빠른 성능을 제공합니다.
- 유연성: 다양한 타입의 키와 값을 사용할 수 있습니다.
- LINQ 지원: LINQ를 통해 데이터 쿼리와 조작이 가능합니다.
단점
- 메모리 사용량: 해시 테이블을 유지하기 위해 추가 메모리를 사용합니다.
- 순서 관리 불가: 입력한 순서를 유지해야 한다면 다른 컬렉션이 더 적합합니다.
- 스레드 안전하지 않음: 기본적으로 스레드 안전성을 제공하지 않으므로, 다중 스레드 환경에서 주의가 필요합니다.
주요 메서드와 속성
생성자와 초기화
기본 생성자
빈 딕셔너리를 생성합니다.
Dictionary<int, string> people = new Dictionary<int, string>();
초기 용량 지정
딕셔너리의 초기 용량을 설정하여 성능을 최적화할 수 있습니다.
Dictionary<int, string> people = new Dictionary<int, string>(capacity: 100);
- 초기 용량 설정을 통한 최적화를 참조하세요.
컬렉션 초기화 사용
Dictionary<int, string> people = new Dictionary<int, string>
{
{ 1, "Alice" },
{ 2, "Bob" },
{ 3, "Charlie" }
};
요소 추가
Add
키-값 쌍을 딕셔너리에 추가합니다.
people.Add(4, "David");
- 이미 존재하는 키를 추가하려고 하면
ArgumentException
이 발생합니다.
인덱서를 사용한 추가
people[5] = "Eve";
- 키가 존재하지 않으면 새로운 키-값 쌍이 추가됩니다.
요소 접근 및 수정
인덱서를 통한 접근
특정 키에 해당하는 값을 가져오거나 설정합니다.
string name = people[1];
people[2] = "Bob Updated";
- 존재하지 않는 키에 접근하려고 하면
KeyNotFoundException
이 발생합니다.
ContainsKey
딕셔너리에 특정 키가 존재하는지 확인합니다.
bool hasKey = people.ContainsKey(3);
TryGetValue
특정 키에 해당하는 값을 안전하게 가져옵니다.
if (people.TryGetValue(3, out string value))
{
Console.WriteLine(value);
}
else
{
Console.WriteLine("키가 존재하지 않습니다.");
}
- 키가 존재하지 않을 경우에도 예외가 발생하지 않으므로 안전한 접근 방법입니다.
요소 제거
Remove
특정 키에 해당하는 키-값 쌍을 제거합니다.
people.Remove(2);
- 제거하려는 키가 존재하지 않아도 예외가 발생하지 않습니다.
Clear
딕셔너리의 모든 요소를 제거합니다.
people.Clear();
기타 유용한 메서드와 속성
Keys 및 Values 활용
Keys
및 Values
속성을 사용하여 딕셔너리의 모든 키 또는 값을 가져올 수 있습니다. 이를 활용해 특정 작업을 수행하거나 조건에 맞는 데이터를 추출할 수 있습니다.
Dictionary<int, string> people = new Dictionary<int, string>
{
{ 1, "Alice" },
{ 2, "Bob" },
{ 3, "Charlie" }
};
// 모든 키 출력
foreach (int key in people.Keys)
{
Console.WriteLine($"Key: {key}");
}
// 모든 값 출력
foreach (string value in people.Values)
{
Console.WriteLine($"Value: {value}");
}
// 특정 조건에 맞는 값 추출
var filteredValues = people.Values.Where(v => v.StartsWith("A"));
foreach (var value in filteredValues)
{
Console.WriteLine($"Filtered Value: {value}");
}
Keys
와 Values
컬렉션은 각각 키와 값에 대한 반복 작업을 쉽게 수행할 수 있게 해주며, 조건에 맞는 데이터 필터링 등의 작업에 유용합니다.
Count
딕셔너리에 포함된 요소의 개수를 반환합니다.
int count = people.Count;
일반적인 사용 사례
데이터 매핑 및 조회
Dictionary
는 데이터 매핑을 위한 도구로 매우 유용합니다. 예를 들어, 학생 ID와 이름을 매핑하거나, 제품 코드와 가격을 매핑하는 데 사용할 수 있습니다.
Dictionary<string, decimal> productPrices = new Dictionary<string, decimal>
{
{ "ProductA", 10.99m },
{ "ProductB", 5.49m },
{ "ProductC", 20.00m }
};
if (productPrices.TryGetValue("ProductB", out decimal price))
{
Console.WriteLine($"ProductB의 가격: {price}");
}
캐싱
Dictionary
는 데이터에 대한 캐시를 구현하는 데 자주 사용됩니다. 예를 들어, 계산 결과를 저장하여 동일한 계산을 반복하지 않도록 하는 방식으로 성능을 개선할 수 있습니다.
Dictionary<int, int> fibonacciCache = new Dictionary<int, int>();
int Fibonacci(int n)
{
if (n <= 1)
return n;
if (fibonacciCache.TryGetValue(n, out int cachedValue))
return cachedValue;
int value = Fibonacci(n - 1) + Fibonacci(n - 2);
fibonacciCache[n] = value;
return value;
}
반복 호출이 빈번한 경우에도, 캐싱을 통해 비용을 줄일 수 있습니다.
private static readonly int _myStructSize = Marshal.SizeOf<MyStruct>();
public void ProcessData()
{
for (int i = 0; i < dataList.Count; i++)
{
int size = _myStructSize; // 캐시된 값을 사용하여 성능 최적화
// 데이터 처리 로직...
}
}
Dictionary<TKey, TValue>
활용
ContainsKey 와 TryGetValue
코드의 작동 방식
if (dictionary.ContainsKey(1))
Console.WriteLine(dictionary[1]);
- 이 코드는 먼저
dictionary.ContainsKey(1)
로 키가 존재하는지 확인한 후, 존재하면dictionary[1]
을 사용하여 값을 가져옵니다. - 이 경우 딕셔너리는 두 번 검색됩니다:
ContainsKey
로 키가 존재하는지 확인할 때 한 번.- 인덱서를 통해 값을 가져올 때 한 번.
if (dictionary.TryGetValue(1, out string str))
Console.WriteLine(str);
- 이 코드는
TryGetValue
를 사용하여 키가 존재하면 값을 반환하고, 키가 존재하지 않으면 기본값(null
또는default(TValue)
)을 반환합니다. - 이 방식은 한 번의 검색만 수행되며, 키가 존재하면
str
변수에 값을 할당하고, 그 값을 사용합니다.
성능 차이
ContainsKey
+ 인덱서 접근 방식은 딕셔너리를 두 번 검색하므로, 최악의 경우 동일한 키에 대해 두 번의 해시 계산과 해시 버킷 접근이 필요합니다. 따라서 키를 찾고 그 값을 사용하는데 평균적으로 O(2)의 비용이 발생합니다.TryGetValue
방식은 키에 대해 딕셔너리를 한 번만 검색하므로, 한 번의 해시 계산과 접근만으로 값을 가져올 수 있습니다. 따라서 평균적으로 O(1)의 비용이 발생합니다.- 따라서
TryGetValue
를 사용하는 것이 성능적으로 더 효율적입니다.
스레드 안전성
Dictionary
는 기본적으로 스레드 안전하지 않으므로, 다중 스레드 환경에서 사용 시 동기화가 필요합니다.
private static Dictionary<int, string> sharedDictionary = new Dictionary<int, string>();
private static readonly object lockObject = new object();
static void AddToSharedDictionary(int key, string value)
{
lock (lockObject)
{
sharedDictionary[key] = value;
}
}
스레드 안전한 딕셔너리가 필요하다면 ConcurrentDictionary를 사용하는 것이 좋습니다.
LINQ와의 통합
Dictionary
는 LINQ와 결합하여 데이터를 쉽게 필터링하고 검색할 수 있습니다.
var filtered = people.Where(p => p.Key > 1).Select(p => p.Value);
foreach (var name in filtered)
{
Console.WriteLine(name);
// 출력:
// Bob
// Charlie
}
Dictionary
는 키-값 쌍으로 데이터를 관리하는 데 최적화된 컬렉션으로, 빠른 검색과 효율적인 데이터 매핑을 필요로 하는 상황에서 매우 유용합니다. 다만, 스레드 안전성과 메모리 사용량을 고려하여 적절한 상황에서 사용해야 합니다.
해시 함수와 충돌 처리
- 해시함수를 참조하세요
IEqualityComparer 사용
기본적으로 Dictionary
는 키의 동등성 비교를 위해 Object.GetHashCode()
와 Object.Equals()
를 사용합니다. 그러나 커스텀 비교가 필요한 경우, IEqualityComparer<TKey>
인터페이스를 구현하여 딕셔너리의 키 비교 방식을 사용자 정의할 수 있습니다.
예를 들어, 대소문자를 구분하지 않는 문자열 키를 사용하는 딕셔너리를 만들고 싶다면 다음과 같이 할 수 있습니다:
class CaseInsensitiveComparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
return string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
}
public int GetHashCode(string obj)
{
return obj.ToLower().GetHashCode();
}
}
Dictionary<string, string> caseInsensitiveDict = new Dictionary<string, string>(new CaseInsensitiveComparer());
caseInsensitiveDict.Add("Key1", "Value1");
Console.WriteLine(caseInsensitiveDict.ContainsKey("key1")); // True
이를 통해 사용자 정의 동등성 비교를 제공하고, 키의 비교 방식을 더욱 유연하게 설정할 수 있습니다.