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.
단계별 내부 동작
a == b: 둘 다Point(1,2)→ 자동Equals가 X,Y 비교 → True.ReferenceEquals(a,b): 서로 다른new→ False.GetHashCode: 값 기반이라 같은 값이면 같은 해시 → True. (Equals 가 같으면 해시도 같아야 한다는 계약을 record 가 지켜 줌)a with { Y = 9 }: a 복제 후 Y=9 → c=Point(1,9). a 는 그대로.ToString자동 생성으로Point { X = 1, Y = 2 },Point { X = 1, Y = 9 }.HashSet<Point>{a,b}: a,b 가Equals/GetHashCode상 동등 → 둘째 추가는 중복으로 무시 → Count = 1.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(배열은 참조 동등)를 쓰므로, 내용 비교가 자동으로 되진 않는다.
면접 포인트
- record 가 자동 생성하는 멤버(값 기반 Equals/GetHashCode/==, ToString, Deconstruct, with용 clone)와 그 결과(
==True,ReferenceEqualsFalse). with= 복제 + 일부 변경, 원본 불변. 값 동등이라 HashSet/Dictionary 에서 중복 제거가 자연스럽다.- record class vs record struct(참조형 vs 값형), 가변 record 를 해시 키로 쓰는 위험, 상속 시 EqualityContract 동등성.