Quick Reference
Dictionary<TKey, TValue>는 고유한 키로 값을 찾을 때 씁니다. 키가 없을 수 있는 읽기에는 TryGetValue, 없으면 추가하고 있으면 바꿀 때는 인덱서를 사용합니다. 조회와 추가의 평균 비용은 O(1)이지만, 순서와 스레드 안전성은 제공하지 않습니다.
var scores = new Dictionary<string, int>
{
["Mina"] = 1200,
};
if (scores.TryGetValue("Mina", out int score))
Console.WriteLine(score);
scores["Mina"] = 1250; // 있으면 갱신, 없으면 추가
bool added = scores.TryAdd("Jin", 980); // 없을 때만 추가
bool removed = scores.Remove("Mina");- 읽기 키가 반드시 있다고 확신할 때만
scores[key]를 사용합니다. 없으면KeyNotFoundException이 발생합니다. - 값의 기본값과 “키 없음”을 구분해야 하면
GetValueOrDefault대신TryGetValue를 사용합니다. - 대소문자를 무시하는 키는 생성할 때
StringComparer.OrdinalIgnoreCase를 전달합니다.
읽기와 쓰기
Add는 중복 키를 오류로 보고, TryAdd는 실패 여부를 false로 돌려줍니다. 인덱서 대입은 추가와 갱신을 모두 수행하므로 설정값을 덮어써도 되는 경우에 맞습니다.
var ports = new Dictionary<string, int>();
ports.Add("http", 80); // 중복 키면 ArgumentException
ports.TryAdd("https", 443); // 중복 키면 false
ports["http"] = 8080; // 기존 값을 8080으로 갱신
ports["admin"] = 9000; // 새 키를 추가
if (ports.TryGetValue("https", out int port))
Connect(port);
int fallback = ports.GetValueOrDefault("metrics", -1);ContainsKey 다음에 인덱서를 읽으면 탐색을 두 번 하고, 두 호출 사이에 컬렉션이 바뀔 여지도 생깁니다. “있으면 값도 필요하다”는 요청은 TryGetValue 하나로 처리합니다. 반대로 GetValueOrDefault("x")가 0, false, null을 반환했을 때는 키가 없었던 것인지 실제 저장값인지 알 수 없습니다.
순회할 때는 KeyValuePair<TKey, TValue> 또는 분해를 사용합니다. 같은 딕셔너리를 foreach로 순회하는 동안 항목을 추가하거나 제거하지 않습니다. 변경할 키가 필요하면 먼저 Keys.ToList()로 복사합니다.
foreach (var (name, value) in scores)
Console.WriteLine($"{name}: {value}");
foreach (string key in scores.Keys.ToList())
{
if (scores[key] < 0)
scores.Remove(key);
}키 비교와 성능
키의 동일성은 생성자에 넘긴 IEqualityComparer<TKey> 또는 EqualityComparer<TKey>.Default가 결정합니다. 해시 기반 컬렉션에서는 Equals가 같은 두 키가 반드시 같은 해시 코드를 반환해야 합니다.
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["Content-Type"] = "application/json",
};
bool found = headers.TryGetValue("content-type", out string? contentType);키로 넣은 객체의 비교 기준이 되는 필드를 나중에 바꾸면 그 객체를 다시 찾지 못할 수 있습니다. record처럼 값 비교가 분명한 불변 키, string, 숫자형은 적합합니다. 변경 가능한 Name을 GetHashCode에 쓰는 클래스는 키로 쓰지 않는 편이 안전합니다.
많은 항목 수를 미리 안다면 생성자에 예상 용량을 주거나 EnsureCapacity를 호출해 재할당을 줄일 수 있습니다. 다만 O(1)은 해시 분포가 정상적이라는 전제의 평균 성능이며, 순회 순서는 API 계약이 아닙니다.
var cache = new Dictionary<int, string>(capacity: 10_000);
cache.EnsureCapacity(20_000);동시성에서의 선택
일반 Dictionary는 여러 스레드가 동시에 읽고 쓰는 용도가 아닙니다. 개별 키 연산을 원자적으로 처리해야 하면 ConcurrentDictionary<TKey, TValue>를 선택합니다.
var counters = new ConcurrentDictionary<string, int>();
int count = counters.AddOrUpdate(
"downloads",
addValue: 1,
updateValueFactory: (_, oldValue) => oldValue + 1);
int value = counters.GetOrAdd("visits", _ => LoadInitialCount());GetOrAdd와 AddOrUpdate의 값 팩터리는 경합 중 여러 번 호출될 수 있으므로, 데이터베이스 쓰기처럼 한 번만 실행되어야 하는 부작용을 넣으면 안 됩니다. 여러 키를 함께 검사하고 갱신하는 하나의 업무 규칙은 ConcurrentDictionary만으로 원자적이지 않으므로 별도의 동기화나 저장소 트랜잭션이 필요합니다.
자주 틀리는 부분
dict[key]읽기는 “없으면 기본값”이 아니라 예외입니다. 선택적 값은TryGetValue로 읽습니다.dict[key] = value는 중복 키를 막지 않습니다. 최초 등록만 허용하면TryAdd또는Add를 씁니다.- 사용자 입력의 문자열 키에 문화권 비교를 기본으로 두지 않습니다. 프로토콜 키, 식별자는 보통
Ordinal또는OrdinalIgnoreCase가 맞습니다. - 삽입 순서에 의존하지 않습니다. 순서가 의미라면 별도 순서 목록을 두거나 요구에 맞는 정렬 컬렉션을 선택합니다.
참고 링크
2 sources