숏컷 코드
public record User(string Name, int Level);
User first = new("Mina", 5);
User second = new("Mina", 5);
User third = first with { Level = 6 };
bool equal = first == second; // true — 값 비교
bool diff = first == third; // false — Level이 다름
Console.WriteLine(first); // User { Name = Mina, Level = 5 }문법
어떤 record 기능을 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 것 |
|---|---|
| 값 동일성 비교 | record equality |
| 일부 값만 바꾼 복사 | with |
| 얕은 복사가 위험한지 확인 | 참조 타입 멤버 점검 |
| 값 타입 record가 필요함 | record struct |
컴파일러가 생성하는 equality
record는 컴파일러가 모든 프로퍼티를 비교하는 Equals와 == 를 자동 생성합니다. class의 기본 ==는 참조 동일성이지만, record는 값 동일성입니다.
// 컴파일러가 생성하는 코드 (개념)
public virtual bool Equals(User? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return EqualityContract == other.EqualityContract
&& Name == other.Name
&& Level == other.Level;
}
public override int GetHashCode()
=> HashCode.Combine(EqualityContract, Name, Level);
// == 연산자도 Equals를 사용하도록 오버로드됨
public static bool operator ==(User? left, User? right)
=> left?.Equals(right) ?? right is null;EqualityContract 는 record 상속 시 파생 타입도 포함해 비교하기 위한 숨겨진 프로퍼티입니다. User와 PremiumUser : User는 프로퍼티 값이 같아도 다른 타입이므로 다릅니다.
record User(string Name);
record PremiumUser(string Name, int Level) : User(Name);
User a = new("Mina");
User b = new PremiumUser("Mina", 5);
Console.WriteLine(a == b); // false — EqualityContract가 다름// ❌ class와 같은 참조 비교라고 생각하면 오해
var left = new User("Mina", 5);
var right = new User("Mina", 5);
Console.WriteLine(ReferenceEquals(left, right)); // false
Console.WriteLine(left == right); // truewith 표현식 — 얕은 복사
with 표현식은 내부적으로 <Clone>$() 메서드를 호출한 뒤 지정한 프로퍼티를 init setter로 수정합니다. 얕은 복사이므로 참조 타입 프로퍼티는 원본과 공유됩니다.
record Config(string Name, List<string> Tags);
var original = new Config("Dev", new List<string> { "c#", "dotnet" });
var copy = original with { Name = "Prod" };
copy.Tags.Add("azure"); // Tags는 공유됨!
Console.WriteLine(original.Tags.Count); // 3 — 원본도 변경됨깊은 복사가 필요하면 with로 컬렉션도 새로 만들어야 합니다.
var deepCopy = original with
{
Name = "Prod",
Tags = new List<string>(original.Tags) // 새 리스트 생성
};record 상속과 equality
record는 클래스처럼 상속할 수 있습니다. 파생 record는 기반 record의 프로퍼티와 equality를 자동으로 확장합니다.
record Animal(string Name);
record Dog(string Name, string Breed) : Animal(Name);
var d1 = new Dog("Rex", "Husky");
var d2 = new Dog("Rex", "Husky");
Console.WriteLine(d1 == d2); // true
// 기반 타입 변수로 비교
Animal a = d1;
Animal b = d2;
Console.WriteLine(a == b); // true — 런타임 타입(Dog) 기준으로 비교record struct의 equality
record struct는 readonly struct처럼 모든 필드를 비교합니다. boxing 없이 값 비교가 가능합니다.
readonly record struct Point(int X, int Y);
var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
Console.WriteLine(p1 == p2); // true — boxing 없이 값 비교체크포인트
| 항목 | 동작 |
|---|---|
== / Equals | 모든 프로퍼티 값 비교 |
EqualityContract | 타입 비교 포함 — 파생 타입 구분 |
with { Prop = val } | 얕은 복사 + init setter |
GetHashCode | 모든 프로퍼티 기반 해시 |
| 컬렉션 멤버 | with는 얕은 복사 — 수동 깊은 복사 필요 |
주의할 점
with는 얕은 복사입니다. List<T>, 배열, 다른 클래스 타입의 프로퍼티는 원본과 같은 참조를 공유합니다. "불변 record를 with로 안전하게 복사한다"는 가정은 참조 타입 멤버가 있을 때 깨집니다.
record를 Dictionary나 HashSet의 키로 쓸 때는 GetHashCode가 프로퍼티 값에 따라 변한다는 점에 주의하세요. 키로 사용 중인 record 인스턴스의 값을 바꾸면 컬렉션에서 찾을 수 없게 됩니다. record는 기본적으로 불변 사용이 원칙입니다.
참고 링크
2 sources