강좌와 팁

C# Dictionary 사용법 정리 날짜:2021-7-11 10:36:33 조회수:90
작성자 : 소엔
포인트 : 1580
가입일 : 2020-02-02 00:09:14
방문횟수 : 109
글 203개, 댓글 64개
소개 : SoEn 운영자입니다.
작성글 보기
쪽지 보내기
Dictionary 키와 값의 쌍을 저장하는 컬렉션이다. 해시 알고리즘을 사용하여 해쉬 테이블이라고도 부르며 대응 관계를 표현한다고 해서 맵이라고도 부른다. 애초부터 빠른 검색을 위해 특화된 자료구조여서 검색 속도는 환상적으로 빠르다. 수억개의 키가 있어도 실시간으로 값을 찾아낸다. 생성자는 다음과 같다.

Dictionary<TKey,TValue>()
Dictionary<TKey,TValue>(Int32)
Dictionary<TKey,TValue>(IDictionary<TKey,TValue>)
Dictionary<TKey,TValue>(IEqualityComparer<TKey>)

모든 생성자는 키의 타입을 지정하는 TKey 값의 타입을 지정하는 TValue 타입 인수로 가진다. , 모두 임의의 타입을 사용할 있다. 예를 들어 키는 문자열, 값은 정수인 사전은 Dictionary<String, int> 타입으로 선언한다.
디폴트 생성자만 해도 충분하다. 용량은 자동으로 늘어나지만 생성할 미리 용량을 지정할 수도 있다. 다른 사전을 복사하는 복사 생성자도 정의되어 있다. 키의 상등성을 비교하는 비교자를 지정할 있되 생략시 대소문자를 구분하는 기본 비교자를 사용한다.
값을 추가할 때는 Add 메서드나 [] 인덱서를 사용한다. Add 메서드와 키와 값을 인수로 전달하고 인덱서는 키로부터 요소를 찾아 값을 대입한다.

Add (TKey key, TValue value);
this[key] = value

새로운 키를 삽입하는 동작은 같지만 이미 키가 있을 경우의 처리가 다르다. Add 메서드는 ArgumentException 예외를 발생시키지만 인덱서는 중복키를 새로운 값으로 대체한다. Add 추가만 하고 인덱서는 키가 없으면 추가, 있으면 갱신한다.
값은 사전 내에서 유일해야 하며 null 없다. 반면 값은 참조 타입일 경우 null이어도 상관 없으며 중복도 가능하다. 값을 읽는 별도의 메서드는 정의되어 있지 않으며 인덱서를 사용한다. [ ] 괄호안에 키를 지정하면 값을 읽어 리턴한다. 키가 없으면 KeyNotFoundException 예외가 발생한다.
 
class CSTest {
      static void Main()
      {
             Dictionary<String, String> dic = new Dictionary<String, String>();
             dic.Add("한국", "서울");
             dic["중국"] = "상하이";
             try
             {
                   dic.Add("한국", "부산");
             }
             catch (ArgumentException)
             {
                   Console.WriteLine("이미 저장한 국가입니다.");
             }
             dic["중국"] = "베이징";
             dic.Add("일본", "도쿄");

             Console.WriteLine(dic["한국"]);
             Console.WriteLine(dic["중국"]);
             Console.WriteLine(dic["일본"]);
      }
}
이미 저장한 국가입니다.
서울
베이징
도쿄

한국 키가 이미 저장되어 있는 상태에서 Add 메서드로 한국을 추가하면 예외가 발생한다. 반면 중국 키가 이미 있는 상태에서 인덱서로 다시 대입하면 기존값이 바뀐다. 사전을 안전하게 읽으려면 키를 읽기 전에 존재하는지 검사해야 한다.

bool ContainsKey (TKey key);
bool ContainsValue (TValue value);

ContainsKey 메서드는 키가 사전에 존재하는지 조사한다. 메서드가 true 리턴하면 해당 키가 이미 존재하는 것이다. 상태일 인덱서로 키를 안전하게 읽을 있으며 Add 같은 키를 삽입해서는 안된다.
ContainsValue 값이 존재하는지 조사한다. 키는 해쉬 알고리즘에 의해 빠르게 찾는데 비해 값은 선형 검색으로 일일이 찾아야 하므로 속도가 느리다.
 
class CSTest {
      static void Main()
      {
             Dictionary<String, String> dic = new Dictionary<String, String>();
             dic.Add("한국", "서울");
             dic["중국"] = "베이징";
             dic.Add("일본", "도쿄");

             if (dic.ContainsKey("한국"))
             {
                   Console.WriteLine("한국의 수도 : " + dic["한국"]);
             }
             if (dic.ContainsKey("미국"))
             {
                   Console.WriteLine("미국의 수도 : " + dic["미국"]);
             }
             if (dic.ContainsValue("베이징"))
             {
                   Console.WriteLine("베이징 있음");
             }
             if (dic.ContainsValue("런던"))
            {
                   Console.WriteLine("런던 있음");
             }
      }
}
한국의 수도 : 서울
베이징 있음

한국은 키가 있어 수도를 조사하지만 미국은 키가 없어 조사하지 않는다. 없는 키를 읽으면 예외가 발생한다. 그래서 항상 키가 있는지 점검한 읽어야 안전하다. 베이징은 값이 있지만 런던은 없다.
값이 존재하는지만 조사할 그런 값이 개나 있는지, 값을 가진 키가 무엇인지는 없다. 사전은 키로부터 값을 신속하게 찾는 자료 구조이지 반대는 아니다. 값으로부터 키를 찾으려면 순회하며 일일이 점검하는 수밖에 없다.
값을 읽기 전에 키의 존재 여부를 일일이 검사하는 것은 귀찮은 일이다. 그렇다고 없는 키를 무턱대고 검색해 버리면 예외가 발생할 있어 위험하다. 예외없이 값을 찾으려면 다음 메서드를 호출한다.

bool TryGetValue (TKey key, out TValue value);

키가 있을 때만 값을 출력 인수로 대입하고 true 리턴한다. 키가 없으면 출력 인수에 기본값을 대입하고 false 리턴한다.
 
class CSTest {
      static void Main()
      {
             Dictionary<String, String> dic = new Dictionary<String, String>();
             dic.Add("한국", "서울");
             dic["중국"] = "베이징";
             dic.Add("일본", "도쿄");

             String country;
             if (dic.TryGetValue("일본", out country))
             {
                   Console.WriteLine("일본의 수도 : " + country);
             }
             if (dic.TryGetValue("프랑스", out country))
             {
                   Console.WriteLine("프랑스의 수도 : " + country);
             }
             else
             {
                   Console.WriteLine("프랑스 정보 없음");
             }
      }
}
일본의 수도 : 도쿄
프랑스 정보 없음

TryGetValue 일본을 검색하면 country 도쿄를 대입하고 true 리턴한다. 이때는 인덱서로 다시 dic["일본"] 찾을 필요 없이 country 바로 사용하면 된다. 프랑스를 검색하면 country null 대입하고 false 리턴한다. 출력 인수의 원래값을 유지하는 것이 아니라 기본값으로 변경해 버린다.
TryGetValue 리턴값으로 성공 여부를 알려줄 어떤 경우라도 예외를 발생시키지는 않아 예외 구문을 작성하지 않아도 된다. 그러나 출력용 인수를 미리 준비하고 리턴값을 점검해 봐야 한다는 면에서 번거롭기는 매한가지이다.
키를 삭제할 때는 다음 메서드를 사용한다.

bool Remove (TKey key);
void Clear ();

키를 발견하여 지웠으면 true 리턴하고 키가 없으면 false 리턴한다. 삭제에 실패해도 예외는 발생하지 않으며 사전은 변화없이 유지된다. Clear 모든 요소를 지운다. Count 0 되지만 내부 용량은 변화 없다.
사전의 모든 요소를 순회하며 읽을 때는 foreach문이 제일 편하다. 사전의 요소는 KeyValuePair<TKey,TValue> 타입이며 타입의 변수로 요소를 받아 키와 값을 하나씩 읽는다. 다음 예제는 사전의 모든 키와 값을 덤프한다.
 
class CSTest {
      static void Main()
      {
             Dictionary<String, String> dic = new Dictionary<String, String>();
             dic.Add("한국", "서울");
             dic["중국"] = "베이징";
             dic.Add("일본", "도쿄");

             foreach (KeyValuePair<String, String> item in dic)
             {
                   Console.WriteLine(item.Key + " : " + item.Value);
             }
      }
}
한국 : 서울
중국 : 베이징
일본 : 도쿄

제어 변수 타입이 복잡해 보이면 var 받아서 사용해도 된다. 순회는 값을 읽기만 하며 요소를 변경할 수는 없다. item Key Value 모두 읽기 전용 속성이어서 변경할 없다. 그렇다면 다음 코드는 어떨까?

foreach (KeyValuePair<String, String> item in dic)
{
          dic[item.Key] = "모름";
}

인덱서로 item.Key 읽어 해당 키의 값을 일괄 변경하였다. 문법적인 이상은 없어 에러는 아니며 컴파일 실행 가능하지만 예외가 발생한다. foreach dic 사전을 순회하는 중간에 dic 사전을 바꿔 버리면 순회 루틴이 정상 실행할 없기 때문이다.
사전 뿐만 아니라 모든 컬렉션은 순회중에 읽기만 해야 한다. foreach 대신 GetEnumerator 메서드로 열거자를 구해 직접 열거할 수도 있지만 별다른 이점은 없다. 사전의 속성은 다음과 같다.
 
속성 설명
Count 요소의 개수를 조사하는 읽기 전용이다. 사전의 실제 용량은 개수보다는 많으며 내부적으로 관리할 조사하는 방법은 없다.
Keys 키의 컬렉션이며 Dictionary<TKey,TValue>.KeyCollection 타입이다.
Values 값의 컬렉션이며 Dictionary<TKey,TValue>.ValueCollection 타입이다.
Comparer 키의 값을 비교하는 IEqualityComparer<TKey> 타입의 객체이다.

사전 전체를 순회하는 방법이 있으니 Keys Values 따로 순회할 일은 별로 없다. Comparer 키의 중복성을 검사하는 비교자이며 디폴트는 EqualityComparer<T>.Default 무난하게 지정되어 있다. 웬만해서는 변경할 일이 드물지만 문자열 타입의 키를 대소문자 구분없이 저장할 가끔 변경한다. 다음 예제를 보자.
 
class CSTest {
      static void Main()
      {
             Dictionary<String, String> dic = new Dictionary<String, String>();
             dic.Add("Korea", "서울");
             dic.Add("KOREA", "서울");

             if (dic.ContainsKey("korea"))
             {
                   Console.WriteLine("한국 정보 있음");
             }
             else
             {
                   Console.WriteLine("한국 정보 없음");
             }
      }
}
한국 정보 없음

Korea KOREA 키로 저장했는데 저장된다. 디폴트 비교자가 대소문자를 구분하기 때문에 철자는 같아도 문자 구성이 다르면 키가 중복되지 않는다. 상태에서 korea 키가 있는지 찾으면 없다고 나온다.
대소문자를 정확히 구분해야 하는 경우는 이런 비교가 합당하지만 코드값이나 URL 대소문자 구분이 없는 정보를 저장할 때는 불편해진다. www.soen.kr 저장해 놓고 www.SoEn.kr 찾으면 없다고 나온다.
이럴 때는 대소문자를 무시하는 StringComparer.OrdinalIgnoreCase 비교자로 사전을 생성해야 한다. 비교자는 값을 최초 넣을 때부터 적용하는 것이어서 중간에 바꿀 수는 없고 생성할 때만 지정할 있다.
 
class CSTest {
      static void Main()
      {
             Dictionary<String, String> dic = new Dictionary<String, String>
                   (StringComparer.OrdinalIgnoreCase);
             dic.Add("Korea", "서울");
             //dic.Add("KOREA", "서울");

             if (dic.ContainsKey("korea"))
             {
                   Console.WriteLine("한국 정보 있음");
             }
             else
             {
                   Console.WriteLine("한국 정보 없음");
             }
      }
}
한국 정보 있음

비교자가 대소문자를 무시하면 Korea키를 저장한 상태에서 KOREA 중복이어서 같이 저장할 없다. korea 찾아도 있다고 보고한다. 키는 문자열이라기보다는 식별자여서 대소문자를 무시하는 것이 어울리는 경우가 많다.
비교자는 키를 비교하는 역할만 뿐이지 대소문자 구성을 강제로 바꾸지는 않는다. 또한 키만 비교할 값은 항상 기본 비교자로 비교한다.
 
class CSTest {
      static void Main()
      {
             Dictionary<String, String> dic = new Dictionary<String, String>
                   (StringComparer.OrdinalIgnoreCase);
             dic.Add("Korea", "Seoul");
             dic.Add("CHINA", "BEIJING");
             dic.Add("japan", "Tokyo");

             foreach (KeyValuePair<String, String> item in dic)
             {
                   Console.WriteLine(item.Key + " : " + item.Value);
             }
             Console.WriteLine(dic.ContainsValue("seoul") ? "있다." : "없다.");
      }
}
Korea : Seoul
CHINA : BEIJING
japan : Tokyo
없다.

사전의 키와 값에 입력한 대소문자는 그대로 유지된다. 비교자가 대소문자를 무시해도 값을 찾을 때는 항상 대소문자를 구분한다. seoul 없고 Seoul 있다. 만약 값을 대소문자 구분없이 사용하려면 넣을 항상 대문자나 소문자로 바꿔 넣어야 한다.
Sorted​Dictionary<TKey,TValue> 타입은 요소를 사전순으로 정렬하여 이진 검색을 사용한다. 내부 알고리즘이 달라 열거시 요소의 순서가 다르다. 대용량에서 속도나 메모리 사용량의 차이 정도만 있을 Dictionary 동작은 거의 같다.
 



개발자의 천국 SoEn

목록보기 삭제 수정 신고 스크랩


로그인하셔야 댓글을 달 수 있습니다.