숏컷 코드
// positional record — 한 줄 선언
public record User(string Name, int Level);
User mina = new("Mina", 5);
User mina2 = new("Mina", 5);
bool equal = mina == mina2; // true — 값 비교
User senior = mina with { Level = 6 }; // 일부만 바꾼 복사본
Console.WriteLine(mina); // User { Name = Mina, Level = 5 }문법
어떤 record 형태가 있나
record는 보통 아래 세 형태를 먼저 구분하면 됩니다.
public record User(string Name, int Level); // positional record class
public record class PlayerProfile(string Name, int Hp); // record class 명시
public readonly record struct Size(int Width, int Height); // 값 타입 recordrecord: 기본은 record classrecord class: 참조 타입을 명시record struct: 값 타입 record
컴파일러가 자동으로 생성하는 것들
public record User(string Name, int Level); 한 줄은 컴파일러가 다음을 자동으로 만들어냅니다.
public string Name { get; init; }— init-only 프로퍼티Equals()/==— 모든 프로퍼티를 비교하는 value equalityGetHashCode()— 프로퍼티 값 기반 해시ToString()—User { Name = Mina, Level = 5 }형태 출력Deconstruct()—var (name, level) = mina;구조 분해 지원<Clone>$()—with표현식이 사용하는 내부 복사 메서드
// 컴파일러가 생성하는 코드와 유사한 모습
public class User : IEquatable<User>
{
public string Name { get; init; }
public int Level { get; init; }
public User(string Name, int Level) { this.Name = Name; this.Level = Level; }
public virtual bool Equals(User? other)
=> other is not null && Name == other.Name && Level == other.Level;
public override int GetHashCode() => HashCode.Combine(Name, Level);
public override string ToString() => $"User {{ Name = {Name}, Level = {Level} }}";
public void Deconstruct(out string Name, out int Level) { ... }
protected virtual User <Clone>$() => new(Name, Level);
}init — 생성 시에만 쓸 수 있는 setter
positional record의 프로퍼티는 기본적으로 init setter를 가집니다. 생성자와 object initializer에서만 값을 설정할 수 있고, 이후에는 읽기 전용이 됩니다.
var user = new User("Mina", 5);
user.Name = "Rina"; // ❌ 컴파일 오류 — init-only property
// object initializer는 허용 (생성 시점이므로)
var user2 = new User { Name = "Rina", Level = 3 }; // ✅
// with 표현식 — 내부에서 Clone + init을 사용
var user3 = user with { Level = 6 }; // ✅record class vs record struct
C# 10부터 record struct도 선언할 수 있습니다.
// record class (참조 타입) — 기본
public record class Point(int X, int Y);
// record struct (값 타입) — 명시적으로 struct 지정
public record struct Size(int Width, int Height);record struct는 스택에 할당되고 boxing 없이 사용할 수 있습니다. 단, 기본적으로 mutable합니다. 불변으로 만들려면 readonly record struct를 사용합니다.
public readonly record struct Coordinate(double Lat, double Lon);var first = new User("Mina", 5);
var second = first with { Level = 6 };
// ❌ 내부에 List<T> 같은 가변 참조가 있으면 with는 얕은 복사class vs record vs struct 선택 기준
| class | record | struct | |
|---|---|---|---|
| 타입 종류 | 참조 | 참조 (기본) | 값 |
| 기본 비교 | 참조 동일성 | 값 동일성 | 값 동일성 |
| 불변성 | 수동 구현 | init으로 자동 | readonly 키워드 |
| 잘 맞는 곳 | 서비스, 엔티티, 수명 관리 | DTO, 이벤트, 설정, 도메인 값 | 소형 값 객체, 성능 중요 |
체크포인트
| 문법 | 의미 |
|---|---|
record User(string Name, int Age) | positional record — 프로퍼티 자동 생성 |
with { Level = 6 } | 일부 값만 바꾼 새 인스턴스 |
init | 생성 시에만 할당 가능한 setter |
record struct | 값 타입 record |
readonly record struct | 불변 값 타입 record |
주의할 점
record는 "내용이 같으면 같다"는 값 동일성을 기본으로 제공하지만, 내부에 List<T> 같은 변경 가능한 참조 타입이 들어 있으면 완전한 불변성은 보장되지 않습니다. with 표현식은 얕은 복사를 수행하기 때문에 중첩된 가변 객체는 원본과 공유됩니다.
equality와 with 표현식의 내부 동작에 대한 깊은 내용은 record equality와 with expression 카드를 참고하세요.
참고 링크
2 sources