숏컷 코드
var scores = new Dictionary<string, int>
{
["Mina"] = 1200,
["Jin"] = 980,
};
// 안전한 읽기
if (scores.TryGetValue("Mina", out int score))
Console.WriteLine(score);
// 기본값 반환 (키 없으면 0)
int rating = scores.GetValueOrDefault("Sora", 0);문법
어떤 작업에 Dictionary를 먼저 쓰나
| 상황 | 먼저 떠올릴 것 |
|---|---|
| 키로 빠르게 조회 | Dictionary<TKey, TValue> |
| 순서보다 조회 속도가 중요 | Dictionary<TKey, TValue> |
| 멀티스레드 읽기/쓰기 | ConcurrentDictionary<TKey, TValue> |
| 키 정렬이 중요 | SortedDictionary<TKey, TValue> |
왜 O(1) 인가 — 해시 테이블 원리
Dictionary는 내부적으로 해시 테이블을 씁니다. 키의 GetHashCode()로 버킷 위치를 계산하기 때문에 항목 수와 무관하게 거의 일정한 시간에 조회됩니다. 반면 List<T>의 Contains()는 O(n)으로 항목이 늘수록 느려집니다.
Dictionary 조회 시간: O(1) 평균
List Contains: O(n)따라서 "키로 값을 찾는" 패턴이라면 List를 순회하는 것보다 Dictionary가 훨씬 효율적입니다.
읽기 패턴 비교
// ❌ 키가 없으면 KeyNotFoundException
int val = scores["Unknown"];
// ✅ 존재 여부 확인 후 읽기
if (scores.ContainsKey("Unknown"))
int val = scores["Unknown"];
// ✅ 더 나은 방법: TryGetValue (조회 1회)
if (scores.TryGetValue("Unknown", out int val))
Console.WriteLine(val);
// ✅ 키 없을 때 기본값 반환
int val = scores.GetValueOrDefault("Unknown", -1);ContainsKey + 인덱서는 해시를 두 번 계산합니다. TryGetValue는 한 번에 처리하므로 더 효율적입니다.
// ❌ 두 단계 조회 + 경쟁 상태에 취약
if (scores.ContainsKey(name))
{
Console.WriteLine(scores[name]);
}
// ✅ 한 번에 읽기
if (scores.TryGetValue(name, out int currentScore))
{
Console.WriteLine(currentScore);
}추가·수정·삭제
scores["NewPlayer"] = 500; // 추가 또는 덮어쓰기
scores.TryAdd("NewPlayer", 500); // 키가 없을 때만 추가
scores.Remove("Jin");
scores.Clear();순회 패턴
// KeyValuePair 순회
foreach (var (name, s) in scores)
Console.WriteLine($"{name}: {s}");
// 키만
foreach (string key in scores.Keys) { }
// 값만
foreach (int val in scores.Values) { }
// LINQ 적용
var topPlayers = scores
.Where(kv => kv.Value >= 1000)
.OrderByDescending(kv => kv.Value)
.Select(kv => kv.Key)
.ToList();ConcurrentDictionary — 멀티스레드 환경
ConcurrentDictionary<K,V>는 내부적으로 락을 분할(striped locking)해 여러 스레드가 동시에 다른 버킷에 접근할 수 있습니다. TryAdd, TryRemove, TryUpdate, AddOrUpdate, GetOrAdd가 핵심 API입니다.
var concurrent = new ConcurrentDictionary<string, int>();
// 없으면 추가, 있으면 무시
concurrent.TryAdd("Mina", 1200);
// 없으면 추가, 있으면 팩토리 결과로 갱신
int newVal = concurrent.AddOrUpdate(
key: "Mina",
addValue: 1,
updateValueFactory: (key, old) => old + 1); // 카운터 패턴
// 있을 때만 변경 (낙관적 동시성 — expected 값과 다르면 false 반환)
bool updated = concurrent.TryUpdate("Mina", newValue: 1300, comparisonValue: 1200);
// 없으면 계산해서 추가 (GetOrAdd는 원자적이지 않으므로 팩토리가 여러 번 불릴 수 있음)
int score = concurrent.GetOrAdd("Sora", key => LoadScoreFromDb(key));
concurrent.TryRemove("Jin", out int removed);체크포인트
| 작업 | 권장 방법 |
|---|---|
| 안전한 읽기 | TryGetValue() |
| 기본값 반환 | GetValueOrDefault() |
| 추가 (덮어쓰기) | dict[key] = value |
| 추가 (중복 방지) | TryAdd() |
| 삭제 | Remove() |
| 스레드 안전 필요 | ConcurrentDictionary<K,V> |
| 없으면 추가, 있으면 갱신 | AddOrUpdate(key, add, (k,old) => ...) |
| 낙관적 갱신 | TryUpdate(key, newVal, expected) |
| 없으면 계산해서 추가 | GetOrAdd(key, factory) |
주의할 점
Dictionary는 삽입 순서를 보장하지 않습니다. 순서가 중요하다면 SortedDictionary<K,V>(키 정렬) 또는 LinkedList + Dictionary 조합을 검토하세요.
멀티스레드 환경에서 Dictionary를 동시에 읽고 쓰면 예외나 무한 루프가 발생할 수 있습니다. 스레드 안전이 필요하면 ConcurrentDictionary<K,V>를 사용하고, 전체 연산의 원자성이 필요하면 lock을 함께 써야 합니다.
순회 중에 같은 Dictionary를 수정하면 InvalidOperationException이 납니다. 삭제나 추가가 필요하면 키 목록을 먼저 ToList()로 복사한 뒤 바꾸는 편이 안전합니다.
참고 링크
2 sources