Menu

C# Dictionary: Add, TryGetValue, 순회, 정렬 사용법

Dictionary<TKey, TValue>는 키를 값에 대응시키고 빠르게 조회합니다. 항목을 추가하고 갱신하는 방법, TryGetValue로 안전하게 읽기, KeyValuePair 항목 순회하기, 개수 세기, 키의 대소문자 무시하기, SortedDictionary로 키를 정렬된 상태로 유지하기를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

Dictionary<TKey, TValue>는 값을 고유한 키 아래에 저장하고, 항목이 몇 개든 키로 값을 거의 상수 시간에 찾습니다. C#의 해시 맵에 해당합니다. 이름에서 번호로 가는 전화번호부, ID에서 레코드로 가는 캐시, 단어별 개수가 그 예입니다.

딕셔너리 만들기와 값 읽기

출력:

12
2.50
2
True
False

두 이니셜라이저 형태는 같은 일을 합니다. ["key"] = value 형태(C# 6)는 인덱서를 쓰므로 반복된 키가 덮어쓰고, { key, value } 형태는 Add를 호출하므로 반복된 키가 있으면 그 줄이 실행될 때 예외를 던집니다.

ContainsKey는 해시 조회라서 빠릅니다. ContainsValue는 값이 인덱싱되어 있지 않으므로 모든 항목을 훑어야 합니다.

Add와 인덱서와 TryAdd

항목을 넣는 방법은 세 가지이며, 키가 이미 있을 때 무슨 일이 일어나는지만 다릅니다:

출력:

26
Caught ArgumentException
True
False
31

Add가 중복에 예외를 던지는 것은 기능입니다. 고유해야 했는데 고유하지 않은 데이터를 드러내 줍니다. "넣거나 갱신"이 의도라면 인덱서를, 첫 값이 이겨야 한다면 TryAdd(.NET Core 2.0 이상)를 쓰세요.

키는 null일 수 없습니다. Add(null, ...)나 dict[null]은 ArgumentNullException을 던집니다. 값은 값 타입이 허용하면 null일 수 있습니다.

KeyNotFoundException과 TryGetValue

없는 키를 인덱서로 읽으면 KeyNotFoundException이 발생합니다. 가장 흔한 딕셔너리 오류이며, 해결책은 거의 항상 TryGetValue입니다.

출력:

Caught KeyNotFoundException
Found ana@example.com
Missing, value is null: True
no email

TryGetValue는 해시 조회를 한 번 하고 성공 여부를 bool로 알려 줍니다. if (dict.ContainsKey(k)) { var v = dict[k]; } 패턴도 동작하지만 키를 두 번 조회합니다. 키가 없으면 out 변수는 타입의 기본값(null, 0, false)을 받습니다.

.NET Core 2.0 이상에는 키가 없을 때 대체값을 반환하는 GetValueOrDefault(key, fallback)도 있습니다: emails.GetValueOrDefault(103, "no email").

항목 갱신과 제거

출력:

2
True
False
1
0

cart["milk"] += 1은 쓰기 전에 읽으므로 딕셔너리에 아직 milk가 없으면 KeyNotFoundException을 던집니다. Remove는 없는 키에 대해 예외 대신 false를 반환하므로 먼저 ContainsKey를 확인할 필요가 없습니다.

순회하기: KeyValuePair, Keys, Values

딕셔너리에 대한 foreach는 각각 Key와 Value를 가진 KeyValuePair<TKey, TValue> 항목을 만들어 냅니다.

출력:

Ana: 88
Ben: 72
Chloe: 95
Ana Ben Chloe 
Total 255
77

pair.Value는 읽기 전용이므로 값을 갱신하려면 인덱서로 써야 합니다. 마지막 루프는 키의 List<string> 복사본을 순회하며, 이는 언제나 안전합니다. 기존 값을 덮어쓰면서 scores.Keys를 직접 순회하는 것은 .NET Core 3.0 이상에서는 허용되지만 .NET Framework에서는 InvalidOperationException을 던집니다.

같은 딕셔너리에 대한 foreach 안에서 새 키를 추가하면 모든 버전에서 InvalidOperationException이 발생합니다. 열거 중 제거는 .NET Framework에서는 예외를 던지고 .NET Core 3.0부터는 허용됩니다. 어디서나 동작해야 하는 코드는 제거할 키를 먼저 모은 뒤 루프가 끝나고 제거합니다.

C# 7과 .NET Core 2.0 이상에서는 루프 헤더에서 KeyValuePair를 분해할 수 있습니다:

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

딕셔너리로 개수 세기

개수 세기가 교과서적인 용도입니다. TryGetValue로 현재 개수를 읽고(없는 키는 0), 다시 씁니다.

출력:

the 3
cat 1
and 2
dog 1
bird 1

같은 모양으로 항목을 묶을 수도 있습니다. Dictionary<string, List<Order>>에서 TryGetValue로 리스트를 가져오고, 없으면 만들어 저장한 뒤, 거기에 Add합니다. 일회성 세기와 묶기라면 LINQ가 식 하나로 해 줍니다: words.GroupBy(w => w).ToDictionary(g => g.Key, g => g.Count()). LINQ를 참고하세요.

비교자로 대소문자 무시하는 키

문자열 키는 기본적으로 정확히 비교합니다. "Apple"과 "apple"은 두 개의 키입니다. 이를 바꾸려면 생성자에 IEqualityComparer<string>을 넘기세요.

출력:

False
text/html
1

HTTP 헤더, 파일 확장자, 사용자 이름 같은 식별자에는 StringComparer.OrdinalIgnoreCase가 맞는 선택입니다. 저장하기 전에 모든 키에 .ToLower()를 호출해도 되지만, 한 곳에서 빠뜨리기 쉽습니다.

직접 만든 클래스를 키로 쓰면 딕셔너리는 키의 GetHashCode와 Equals를 호출합니다. 이를 재정의하지 않은 클래스는 참조로 비교하므로, 필드가 같은 두 별개의 객체는 서로 다른 키입니다. 이 쌍을 작성하는 방법은 HashSet을 참고하세요.

순서, 정렬, SortedDictionary

Dictionary는 열거 순서에 대해 아무것도 약속하지 않습니다. 실제로 항목이 추가되기만 한 딕셔너리는 삽입 순서대로 열거되지만, Remove 뒤에 오는 Add는 비워진 칸을 재사용해 더 앞에 나타날 수 있습니다. 코드는 절대 이에 의존하면 안 됩니다.

순서가 필요하면 쓰는 지점에서 정렬하거나 정렬된 컬렉션을 쓰세요:

출력:

Cairo 210
Lima 340
Oslo 520
By value, highest first:
Oslo 520
Lima 340
Cairo 210
Berlin, Cairo, Lima, Oslo

SortedDictionary<TKey, TValue>는 키를 항상 정렬된 상태로 유지하므로(균형 트리입니다) 조회와 삽입이 O(1)이 아니라 O(log n)입니다. 키 순서로 자주 열거한다면 이것을 쓰고, 순서가 한 번만 필요하다면 일반 딕셔너리를 LINQ로 정렬하세요. SortedList<TKey, TValue>는 메모리를 덜 쓰지만 커지면 삽입이 느린 세 번째 선택지입니다.

빠른 참조

작업코드
만들기new Dictionary<string, int>()
넣거나 덮어쓰기d[k] = v
넣기, 중복이면 예외d.Add(k, v)
새것일 때만 넣기d.TryAdd(k, v)
읽기, 없으면 예외d[k]
안전하게 읽기d.TryGetValue(k, out var v)
키 존재 여부d.ContainsKey(k)
제거d.Remove(k)(bool 반환)
크기d.Count
키, 값d.Keys, d.Values
키로 정렬d.OrderBy(p => p.Key) 또는 SortedDictionary
대소문자 무시new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase)

흔한 실수

  • 없는 키를 d[k]로 읽기. KeyNotFoundException이 발생합니다. TryGetValue를 쓰세요.
  • 이미 있을 수 있는 키에 Add 호출하기. ArgumentException이 발생합니다. 인덱서나 TryAdd를 쓰세요.
  • 딕셔너리에 대한 foreach 안에서 키 추가하기. InvalidOperationException이 발생합니다. 변경 사항을 모았다가 뒤에 적용하세요.
  • 열거 순서에 의존하기. 정렬하거나 SortedDictionary를 쓰세요.
  • 넣은 뒤 키 객체의 필드 바꾸기. 해시 코드가 바뀌어 항목을 더 이상 찾을 수 없습니다.
  • ContainsKey 뒤에 인덱서. 조회가 두 번입니다. TryGetValue는 한 번입니다.

자주 묻는 질문

C#에서 Dictionary.Add와 인덱서의 차이는 무엇인가요?

dict.Add(key, value)는 새 항목을 넣고, 키가 이미 있으면 ArgumentException을 던집니다. dict[key] = value는 키가 새것이면 항목을 넣고 이미 있으면 값을 덮어쓰며, 중복에 대해 절대 예외를 던지지 않습니다. TryAdd(key, value)는 키가 새것일 때만 넣고 그렇지 않으면 false를 반환합니다.

C#에서 TryGetValue는 어떻게 동작하나요?

dict.TryGetValue(key, out var value)는 키가 있으면 true를 반환하고 value를 설정하며, 없으면 false를 반환하고 value를 타입의 기본값으로 설정합니다. ContainsKey 뒤에 dict[key]를 쓰면 조회가 두 번이지만 이것은 한 번이며, KeyNotFoundException을 절대 던지지 않습니다.

C#에서 Dictionary는 어떻게 순회하나요?

foreach (KeyValuePair<string, int> pair in dict)는 각 항목을 pair.Key와 pair.Value로 줍니다. 키만 또는 값만 순회하려면 dict.Keys나 dict.Values를 쓰세요. 그 루프 안에서 딕셔너리에 키를 추가하지 마세요. InvalidOperationException이 발생합니다.

C#의 Dictionary는 순서가 있나요?

어떤 순서도 보장되지 않습니다. 항목이 추가되기만 한 딕셔너리는 보통 삽입 순서대로 열거되지만, Remove 뒤에는 새 항목이 비워진 칸을 채울 수 있어 순서가 바뀝니다. 순서가 필요하면 정렬하세요: dict.OrderBy(p => p.Key), 또는 항상 키 순서로 열거하는 SortedDictionary<TKey, TValue>를 쓰세요.

Dictionary 키가 대소문자를 구분하지 않게 하려면 어떻게 하나요?

생성자에 비교자를 넘기세요: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase). 그러면 조회, Add, ContainsKey에서 "Apple"과 "apple"이 같은 키가 됩니다. 비교자는 딕셔너리를 만들 때 고정됩니다.

C#의 KeyValuePair란 무엇인가요?

KeyValuePair<TKey, TValue>는 딕셔너리를 열거할 때 항목마다 건네주는 구조체입니다. 읽기 전용 Key와 Value 속성을 가지므로 이를 통해 항목을 바꿀 수 없습니다. 대신 dict[pair.Key] = newValue라고 쓰세요(루프 뒤에서, 또는 키의 복사본을 순회하면서).

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기