11. Equals/GetHashCode 계약과 Dictionary/HashSet 키
난이도 중내 리뷰 · C#
해설 · C#
해설 — Equals/GetHashCode 계약과 Dictionary/HashSet 키
난이도: 중상
요약
해시 기반 컬렉션은 키를 GetHashCode() 로 버킷에 넣고 같은 버킷 안에서
Equals() 로 일치를 확인한다. 그래서 (1) 재정의 없는 참조 형식은 참조
동일성으로만 비교돼 값이 같은 다른 인스턴스로는 못 찾고(found1=False),
(2) 사전에 든 키의 필드를 바꾸면 해시가 변해 그 항목을 영영 못 찾으며
(found2=False), (3) 값 형식은 재정의가 없어도 기본 ValueType 비교로 값
조회가 되지만(found3=True) 그 기본 구현은 느리다.
예측 출력
False // found1
False // found2
True // found3
각 경우 설명
(A) NoOverride — 참조 형식, 재정의 없음 → False
object기본GetHashCode/Equals는 참조 동일성(인스턴스 신원)이다.new NoOverride(1)두 개는 서로 다른 객체 → 해시도 다르고 Equals 도 false →ContainsKey실패.- 함정: "Id 가 같으니 같은 키"라는 의도가 전혀 반영되지 않는다. 값으로
찾으려면
Equals+GetHashCode를 값 기반으로 재정의해야 한다.
(B) ValueKey — 값 기반 재정의 O, 그러나 가변 → False
- 삽입 시
GetHashCode()==2로 버킷이 정해진다. k.Id = 999로 바꾸면 이제k.GetHashCode()==999.ContainsKey(k)는 999 버킷을 뒤지지만 항목은 2 버킷에 들어 있다 → 못 찾음.- 항목은 사라지지 않았지만 어떤 조회로도 닿을 수 없는 유령이 된다(원래 값 2 로도, 새 값 999 로도 못 찾음).
- 핵심 규칙: 해시 컬렉션의 키로 쓰는 객체는 넣은 뒤 변경하지 말 것(불변 키 사용). 값 형식이나 readonly 필드로 만들면 구조적으로 예방된다.
(C) Coord — 값 형식, 기본 해시 → True (단, 비용 주의)
- 값 형식은
ValueType.Equals/GetHashCode기본 구현이 필드 값을 비교/해시 하므로 값 조회가 된다. - 비용: 기본
ValueType.GetHashCode는 리플렉션으로 필드를 훑고, 경로에 따라 박싱이 끼어들어 느리다. 또 구현이 "대표 필드 위주"로 해시하는 경우가 있어 분포가 나빠질 수 있다. - 권장: 키로 쓸 구조체는
IEquatable<T>+Equals/GetHashCode를 직접 재정의(HashCode.Combine(X, Y))해 빠르고 고른 해시를 보장한다.
Equals/GetHashCode 계약
a.Equals(b)가 true 이면a.GetHashCode() == b.GetHashCode()여야 한다. (역은 불필요 — 다른 값이 같은 해시일 수 있다=충돌은 정상.)GetHashCode는 객체가 "동등 판단에 쓰는 상태"가 그대로면 호출 때마다 같은 값을 줘야 한다(안정성).- 둘을 함께 재정의해야 한다.
Equals만 바꾸고GetHashCode를 두면 "같은데 해시가 달라" 컬렉션에서 못 찾고, 반대도 충돌 폭증을 부른다.
흔한 오해·함정
- "
Equals만 재정의하면 사전에서 찾아진다" — 아니다. 해시가 먼저라 둘 다 필요. - "구조체는 키로 그냥 써도 빠르다" — 기본 해시는 리플렉션/박싱으로 느릴 수 있다.
- 가변 객체를 키로 쓰고 나중에 변경 → 조회 불가(누수성 유령 항목).
record는 값 기반Equals/GetHashCode를 자동 생성해 키로 안전(불변이면 더욱).
면접 포인트
- 해시 컬렉션의 조회 절차(해시→버킷→Equals)와 계약을 설명할 수 있는지.
- "가변 키" 버그의 메커니즘과 예방(불변 키/값 형식/readonly).
- 서버에서 좌표·복합 키 구조체를 사전 키로 쓸 때
IEquatable<T>+HashCode.Combine으로 박싱·리플렉션을 없애는 실무 감각.