Quick Reference
struct는 값 타입입니다. 대입·기본 인자 전달·반환에서 인스턴스가 복사되므로, 작은 데이터 중심 값을 표현할 때 적합합니다. 가변 struct는 복사본을 바꾸는 실수가 쉬우므로 보통 readonly struct 또는 immutable member로 설계합니다.
public readonly struct Money(decimal amount)
{
public decimal Amount { get; } = amount;
public Money Add(Money other) => new(Amount + other.Amount);
}
Money subtotal = new(10m);
Money total = subtotal.Add(new Money(5m));readonly struct는 struct 자신의 필드·프로퍼티를 바꾸지 못하게 하지만 참조 멤버가 가리키는 객체까지 불변으로 만들지는 않습니다.ref struct는Span<T>처럼 managed reference를 안전하게 다루는 제한된 타입입니다. heap field, boxing, 일반 delegate capture 같은 escape가 금지됩니다.default(T)와 array 요소 초기화는 struct의 명시적 parameterless constructor를 호출하지 않습니다.- object/interface 변환은 boxing이 될 수 있습니다. 필요할 때만
ref/in전달을 사용하고 실제 복사 비용을 측정합니다.
복사와 readonly
public readonly struct Point(double x, double y)
{
public double X { get; } = x;
public double Y { get; } = y;
public double DistanceTo(Point other) => Math.Hypot(X - other.X, Y - other.Y);
}
Point first = new(0, 0);
Point second = first;
Console.WriteLine(second.DistanceTo(new Point(3, 4))); // 5값 복사는 원본과 복사본의 struct 필드를 분리합니다. 큰 struct를 자주 전달·반환하면 복사량이 커질 수 있어, 읽기만 하는 API는 in T 또는 ref readonly를 검토할 수 있습니다. 그러나 in은 호출 규칙과 alias를 복잡하게 만들 수 있으므로 작은 값에 습관적으로 붙이지 않습니다.
readonly struct의 instance member는 상태를 변경하지 않는다는 계약입니다. 이 조건이 없는데 readonly receiver에서 non-readonly member를 호출하면 compiler가 방어 복사본을 만들 수 있습니다. 참조 멤버 자체는 여전히 가변일 수 있습니다.
public readonly struct TagSet(List<string> tags)
{
public List<string> Tags { get; } = tags;
}
var tagSet = new TagSet(new List<string>());
tagSet.Tags.Add("mutable"); // readonly struct라도 List 자체는 변경 가능초기화와 ref struct
struct에는 항상 default value가 있습니다. default(Measurement)와 new Measurement[1]의 요소는 모든 필드가 default인 값이며, 명시한 parameterless constructor의 검증·초기화는 실행하지 않습니다.
public readonly struct Measurement
{
public Measurement()
{
Value = double.NaN;
Unit = "unknown";
}
public double Value { get; }
public string? Unit { get; }
}
Measurement created = new(); // constructor 실행
Measurement defaultValue = default; // Value = 0, Unit = nullref struct는 stack allocation 기능이 아니라 escape를 막는 type safety 규칙입니다. Span<T>처럼 수명이 짧은 view가 GC heap에 남거나 async/closure 경계를 넘어가지 않도록 제한합니다.
static int Sum(ReadOnlySpan<int> values)
{
int total = 0;
foreach (int value in values) total += value;
return total;
}선택과 변환
| 필요 | 선택 | 주의점 |
|---|---|---|
| 작은 데이터 중심 값·독립 복사 | immutable struct | 참조 멤버는 얕게 공유 |
| 값 기반 equality와 데이터 문법 | record struct | 기본 mutable 여부와 복사량 확인 |
| identity·공유 상태·상속 | class | 참조 alias와 lifecycle 관리 |
| 짧은 수명의 memory view | ref struct/Span<T> | escape·async·capture 제한 |
struct를 object 또는 구현한 interface로 변환하면 boxing conversion이 존재합니다. 제네릭은 많은 API에서 이를 피할 수 있지만, 모든 제네릭 호출이 boxing-free라는 보장은 아닙니다. 값·참조 타입의 더 넓은 선택은 값 타입과 참조 타입에서 확인합니다.
자주 틀리는 부분
"struct는 50바이트 이하" 같은 고정 크기 규칙은 언어 기준이 아닙니다. copy frequency, generic/interface conversion, collection 사용, runtime 측정을 함께 보고 결정하세요.
record struct도 record class처럼 자동으로 완전 불변이 되지 않습니다. positional record struct의 프로퍼티는 기본적으로 set 가능하므로, 필요한 경우 readonly record struct 또는 명시적인 읽기 전용 API를 선택합니다.
참고 링크
2 sources