핵심 정리
public readonly struct Point
{
public double X { get; init; }
public double Y { get; init; }
public double DistanceTo(Point other)
{
double dx = X - other.X;
double dy = Y - other.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
}
Point a = new Point { X = 0, Y = 0 };
Point b = new Point { X = 3, Y = 4 };
double d = a.DistanceTo(b); // 5.0문법
struct는 값 타입 — 대입하면 복사된다
class를 대입하면 같은 객체를 가리키는 참조가 복사됩니다. struct는 값 자체가 복사되므로 원본과 독립적입니다.
Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1; // 값 복사 — 독립적인 두 Point
// p2.X를 바꿔도 p1에 영향 없음이 특성은 예측 가능한 동작을 만들지만, 크기가 큰 struct를 자주 복사하면 성능 비용이 커집니다.
readonly struct — 불변 보장 + 최적화
모든 멤버가 읽기 전용임을 컴파일러에 알립니다. 덕분에 in 매개변수로 전달할 때 방어 복사(defensive copy)가 줄어 성능이 좋아집니다.
// readonly가 없으면 in 매개변수 전달 시 컴파일러가 방어 복사를 만들 수 있음
void Process(in Point p) { ... }
// readonly struct는 방어 복사 없이 직접 전달ref struct — 스택 전용
ref struct는 힙에 올라갈 수 없으므로 필드, 박싱, 비동기 메서드, 람다에서 캡처가 불가능합니다. 대신 GC 압박 없이 스택에서만 빠르게 처리할 수 있습니다. Span<T>가 대표적인 예입니다.
Boxing / Unboxing
struct를 object나 인터페이스 타입으로 다루면 힙에 복사(boxing)가 일어납니다. 성능이 중요한 코드에서는 이를 피하기 위해 제네릭을 쓰거나 ref 전달을 씁니다.
int x = 42;
object boxed = x; // boxing — 힙 할당
int unboxed = (int)boxed; // unboxing — 힙에서 복사
// 제네릭으로 boxing 없이 처리
void Print<T>(T value) => Console.WriteLine(value); // boxing 없음선택 기준
struct | class | record | |
|---|---|---|---|
| 메모리 위치 | 스택(주로) | 힙 | 힙 |
| 대입 의미론 | 값 복사 | 참조 복사 | 참조 복사 |
| 기본 비교 | 멤버별 값 비교 | 참조 동일성 | 멤버별 값 비교 |
| 잘 맞는 상황 | 작은 불변 값 (좌표, 색상) | 수명 있는 서비스·엔티티 | DTO, 설정 객체 |
주의할 점
크기가 큰 struct를 자주 복사하거나 컬렉션에 담으면 성능이 나빠집니다. "가볍다"는 이미지와 달리, 50바이트를 넘는 struct는 class보다 불리한 경우가 많습니다.
struct는 null이 될 수 없습니다. nullable로 쓰려면 Point?(Nullable<Point>)를 써야 합니다. 또한 상속이 불가능하고, 인터페이스 구현만 가능합니다.
참고 링크
2 sources