← 문제로

7. record 의 값 의미론(value semantics)과 with 식

난이도 중
내 리뷰 · C#
해설 · C#

해설 — record 의 값 의미론(value semantics)과 with 식

난이도: 중상

요약

record(참조형)와 record struct(값형)는 컴파일러가 값 기반 동등성을 위한 멤버들(Equals/GetHashCode/==,!=/ToString/Deconstruct/복제용 clone)을 자동 생성해 준다. 그래서 같은 값이면 ==true(단 ReferenceEquals 는 여전히 인스턴스 비교라 false). with 식은 원본을 복제한 뒤 일부 속성만 바꾼 새 인스턴스를 만든다(원본 불변). 일반 class 는 이런 게 자동 생성되지 않아 참조 동등성만 갖는다.

출력 예측

True                       // (1) a == b : 값(X,Y) 동등
False                      // (2) ReferenceEquals(a, b) : 서로 다른 인스턴스
True                       // (3) GetHashCode 도 값 기반이라 같음
Point { X = 1, Y = 2 }     // (4) a 는 with 로 안 바뀜(불변)
Point { X = 1, Y = 9 }     // (5) c 는 복제 + Y 변경
1                          // (6) a,b 가 값 동등이라 HashSet 이 하나로 합침
False                      // (참고) 일반 class 는 참조 동등성 기본 → 다른 인스턴스면 false

핵심 개념

  • 컴파일러 자동 생성 멤버(record):
    • bool Equals(Point) / override bool Equals(object) — 타입 + 모든 멤버 값 비교.
    • override int GetHashCode() — 멤버들로부터 계산(값 같으면 해시 같음).
    • operator == / operator !=Equals 기반.
    • override string ToString()Point { X = 1, Y = 2 } 형태.
    • Deconstruct(out int X, out int Y) — 위치 record 의 분해.
    • 복제 메서드(protected <Clone>$) — with 가 사용하는 멤버별 복사.
  • with 식: a with { Y = 9 } 는 "a 를 멤버별로 복제 → 초기화자에 적은 속성만 덮어쓰기" 다. a 는 절대 바뀌지 않는다(새 객체 c 가 생긴다).
  • 값 동등 vs 참조 동등: == 는 record 에선 값 비교지만, ReferenceEquals 는 항상 같은 인스턴스인지만 본다 → (1) True, (2) False.

단계별 내부 동작

  1. a == b: 둘 다 Point(1,2) → 자동 Equals 가 X,Y 비교 → True.
  2. ReferenceEquals(a,b): 서로 다른 newFalse.
  3. GetHashCode: 값 기반이라 같은 값이면 같은 해시 → True. (Equals 가 같으면 해시도 같아야 한다는 계약을 record 가 지켜 줌)
  4. a with { Y = 9 }: a 복제 후 Y=9 → c=Point(1,9). a 는 그대로.
  5. ToString 자동 생성으로 Point { X = 1, Y = 2 }, Point { X = 1, Y = 9 }.
  6. HashSet<Point>{a,b}: a,b 가 Equals/GetHashCode 상 동등 → 둘째 추가는 중복으로 무시 → Count = 1.
  7. PlainPoint: Equals/GetHashCode 를 오버라이드하지 않은 일반 class → object 기본(참조 동등) → p.Equals(q) False.

record class vs record struct

  • record class(기본 record): 참조형. == 는 값 비교지만 ReferenceEquals 는 인스턴스 비교. 대입/전달은 참조 복사. with 는 힙에 새 객체.
  • record struct: 값형. 대입/전달이 값 복사라 애초에 복사본이 독립적. 동등성도 값 기반(자동 생성). 기본 record struct 는 가변(mutable)이라 readonly record struct 로 불변 권장. with 는 스택/값 복사 후 일부 변경.
  • 공통점: 둘 다 값 기반 Equals/GetHashCode/ToString 자동 생성. 차이: 참조형 vs 값형(복사/전달/박싱/힙 여부).

흔한 오해·함정

  • record 는 자동으로 불변이 아니다: 위치 매개변수(record Point(int X,int Y))는 init 전용이라 불변이지만, public int X { get; set; } 처럼 쓰면 가변이 된다. 가변 속성은 GetHashCode 가 변해 딕셔너리/HashSet 키로 쓰면 위험.
  • 상속 동등성: record 동등성은 런타임 타입(EqualityContract)까지 본다 — 기반 record 와 파생 record 는 값이 같아도 타입이 다르면 == 가 false. 일반 class 의 Equals 오버라이드와 미묘하게 다르다.
  • record 의 Equals멤버(프로퍼티/필드) 단위 비교다. 참조형 멤버(예: 배열)는 그 멤버의 Equals(배열은 참조 동등)를 쓰므로, 내용 비교가 자동으로 되진 않는다.

면접 포인트

  1. record 가 자동 생성하는 멤버(값 기반 Equals/GetHashCode/==, ToString, Deconstruct, with용 clone)와 그 결과(== True, ReferenceEquals False).
  2. with = 복제 + 일부 변경, 원본 불변. 값 동등이라 HashSet/Dictionary 에서 중복 제거가 자연스럽다.
  3. record class vs record struct(참조형 vs 값형), 가변 record 를 해시 키로 쓰는 위험, 상속 시 EqualityContract 동등성.