← 문제로

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 계약

  1. a.Equals(b) 가 true 이면 a.GetHashCode() == b.GetHashCode() 여야 한다. (역은 불필요 — 다른 값이 같은 해시일 수 있다=충돌은 정상.)
  2. GetHashCode 는 객체가 "동등 판단에 쓰는 상태"가 그대로면 호출 때마다 같은 값을 줘야 한다(안정성).
  3. 둘을 함께 재정의해야 한다. Equals 만 바꾸고 GetHashCode 를 두면 "같은데 해시가 달라" 컬렉션에서 못 찾고, 반대도 충돌 폭증을 부른다.

흔한 오해·함정

  • "Equals 만 재정의하면 사전에서 찾아진다" — 아니다. 해시가 먼저라 둘 다 필요.
  • "구조체는 키로 그냥 써도 빠르다" — 기본 해시는 리플렉션/박싱으로 느릴 수 있다.
  • 가변 객체를 키로 쓰고 나중에 변경 → 조회 불가(누수성 유령 항목).
  • record 는 값 기반 Equals/GetHashCode 를 자동 생성해 키로 안전(불변이면 더욱).

면접 포인트

  • 해시 컬렉션의 조회 절차(해시→버킷→Equals)와 계약을 설명할 수 있는지.
  • "가변 키" 버그의 메커니즘과 예방(불변 키/값 형식/readonly).
  • 서버에서 좌표·복합 키 구조체를 사전 키로 쓸 때 IEquatable<T>+HashCode.Combine 으로 박싱·리플렉션을 없애는 실무 감각.