반응형

[PART7.클래스와 객체 입문(13/21)] 구조체의 매개변수 없는 생성자와 필드 이니셜라이저 (C# 10) / 자동 기본 구조체 (C# 11)

new T()·default(T)·new T[10]이 같은 결과를 줄 거라는 직관은 C# 10부터 깨진다. 그 이유와 안전하게 쓰는 법을 IL로 증명한다.


1. 문제 제기 — 같은 타입인데 결과가 셋 다 다르다

Unity에서 인벤토리 슬롯·플레이어 스탯·이펙트 옵션 같은 작은 데이터 묶음을 struct로 만들 때, 흔히 이런 코드를 작성합니다.

C#
// Unity에서 인벤토리 슬롯을 "기본값=빈 슬롯, 체력=100"으로 시작하고 싶다
public struct Slot
{
    public int Hp = 100;          // C# 10부터 가능
    public string Name = "Empty"; // C# 10부터 가능
    public Slot() { }              // C# 10부터 가능
}

public class Inventory
{
    public Slot Current = new Slot();    // (1) 명시적 생성
    public Slot Empty  = default;        // (2) default 키워드
    public Slot[] All  = new Slot[2];    // (3) 배열 할당
}

위 세 줄은 같은 Slot 타입을 만드는 코드입니다. 신입이 보면 셋 다 Hp=100, Name="Empty"로 채워질 거라고 자연스럽게 기대합니다.

실제 실행 결과는 다음과 같습니다.

arr0.Hp=0, arr0.Name=<null>, a.Hp=100
  • new Slot()Hp=100, Name="Empty" ✅ 생성자 호출됨
  • default(Slot)Hp=0, Name=null ❌ 생성자 호출 안 됨
  • new Slot[2]Hp=0, Name=null ❌ 생성자 호출 안 됨

이 차이를 모르면 다음과 같은 버그가 조용히 들어옵니다.

  • 인벤토리 배열을 new Slot[100]으로 미리 잡았는데, 모든 슬롯의 Hp100이 아니라 0. 게임 시작과 동시에 모든 슬롯이 "사망 상태"로 보입니다.
  • Slot.Name을 그대로 text.SetText(slot.Name)에 넘겼는데 NullReferenceException. 분명히 "Empty"로 초기화했다고 생각했습니다.

struct 초기화는 단순한 문법 차이가 아니라 CLR의 메모리 할당 규칙과 직접 묶여 있습니다. 이 글에서는 C# 10에서 풀린 제약, 그래도 남아 있는 함정, C# 11에서 더해진 편의 기능을 IL 레벨까지 따라가며 정리합니다.


2. 개념 정의 — new T()는 호출, default(T)는 메모리 0

비유: 공장 출고 vs 폐창고에서 꺼낸 박스

struct는 부품 박스라고 생각해보겠습니다.

  • new T()공장 라인에서 막 출고된 박스. 라벨이 붙고, 기본 부품이 들어 있고, 도장도 찍혀 있습니다(생성자가 실행).
  • default(T)창고 바닥에 빈 박스를 그냥 던진 것. 박스 모양만 있고 안은 텅 비어 있습니다(메모리만 0으로 초기화).
  • new T[10]빈 박스 10개를 한꺼번에 쌓아둔 더미. 도장 찍을 시간 없으니 전부 텅 빈 채로 둡니다(생성자 호출 없음).
struct — 구조체 (Value Type, 값 타입) 값 자체를 직접 담는 작은 데이터 컨테이너. 변수에 대입하면 내용 전체가 복사되며(복사 의미론), 일반적으로 스택이나 다른 객체 안에 인라인으로 배치된다. 참조 타입(class)과 달리 GC(Garbage Collector, 메모리를 자동으로 회수하는 런타임 구성요소)가 추적하지 않는다.
default 키워드 — 기본값 표현식 타입의 "비트가 모두 0인 상태"를 만들어내는 키워드. 값 타입은 모든 필드가 0/false/null로 채워지고, 참조 타입은 null이 된다. 생성자를 호출하지 않는다.
예시: Slot s = default; Slot의 모든 필드를 0/null로 채움. Slot s = new Slot();과 다름.

시각화 — 세 갈래의 결과

struct Slot { int Hp = 100; string Name =

기본 코드 — 세 가지 경로 한눈에 보기

C#
public struct Counter
{
    public int Value = 1;        // 필드 이니셜라이저 (C# 10+)
    public Counter() { }          // 매개변수 없는 생성자 (C# 10+)
}

public class Program
{
    public static void Main()
    {
        var a   = new Counter();      // (1) 생성자 호출
        var b   = default(Counter);   // (2) 메모리 0
        var arr = new Counter[3];     // (3) 메모리 0 (요소 개수만큼)

        System.Console.WriteLine($"new={a.Value}, default={b.Value}, arr0={arr[0].Value}");
        // 출력: new=1, default=0, arr0=0
    }
}

new·default·new T[] 세 구문은 C# 9 이전엔 모두 같은 결과를 냈습니다. 어차피 매개변수 없는 생성자를 만들 수 없었으니까요. C# 10에서 생성자가 허용되면서, 이 세 경로가 서로 다른 IL 명령어로 컴파일되는 사실이 외부로 드러나게 됐습니다.

IL 분석 — call .ctor vs initobj vs newarr

Main을 컴파일하면 다음 IL이 나옵니다.

IL
.locals init (
    [0] valuetype Counter,
    [1] valuetype Counter,
    [2] valuetype Counter[]
)

// (1) var a = new Counter();
IL_0000: ldloca.s 0
IL_0002: call instance void Counter::.ctor()   // ← 생성자 호출

// (2) var b = default(Counter);
IL_0007: ldloca.s 1
IL_0009: initobj Counter                       // ← 메모리 0 (생성자 우회)

// (3) var arr = new Counter[3];
IL_000f: ldc.i4.3
IL_0010: newarr Counter                        // ← 배열 할당, 요소별 .ctor 없음
IL_0015: stloc.2

핵심은 세 IL 명령어가 완전히 다른 일을 한다는 점입니다.

  • call instance void Counter::.ctor() — 우리가 정의한 Counter() 생성자를 호출. 필드 이니셜라이저(Value = 1)가 실행됨.
  • initobj Counter — 해당 메모리 영역의 모든 비트를 0으로 덮어씀. 메서드 호출이 아니라 단일 명령어다. 생성자가 있어도 호출하지 않음.
  • newarr Counter — 길이만큼의 연속된 메모리 블록을 할당하고 전체를 0으로 초기화. 요소마다 .ctor을 부르지 않음.
initobj — IL의 값 타입 0 초기화 명령어 값 타입의 메모리 주소(ldloca로 얻음)를 받아 그 영역을 비트 0으로 채운다. 어떤 메서드도 호출하지 않는, 사실상 memset(ptr, 0, sizeof(T))에 해당하는 한 줄짜리 명령어다.

이 IL 차이가 바로 "신입이 같은 결과를 기대했는데 다른 결과를 받는" 이유입니다.


3. 내부 동작 — 왜 CLR은 default를 "모두 0"으로 강제하는가

"default(T)는 항상 모두 0"이라는 CLR의 헌법

C# 10 이전에는 struct에 매개변수 없는 생성자도, 필드 이니셜라이저도 모두 금지였습니다. 이건 단순한 언어 설계 취향이 아니라 CLR(Common Language Runtime, .NET의 실행 엔진)의 핵심 규약과 맞물려 있었습니다.

CLR의 규약: 값 타입의 기본값(default(T))은 모든 비트가 0인 메모리 패턴과 동일해야 한다.

이 규약이 왜 필요할까요? 성능 때문입니다. CLR은 다음 상황에서 값 타입의 메모리를 memset(0)만으로 처리합니다.

  • new T[1_000_000] — 거대한 배열을 만들 때 요소마다 생성자를 부르면 끔찍하게 느립니다. 한 번에 0으로 밀어버리는 게 압도적으로 빠릅니다.
  • class Foo { T field; } — 참조 타입 안에 값 타입 필드가 있을 때, 객체 전체 메모리를 0으로 초기화하면 모든 값 타입 필드도 자동으로 "유효한 기본값"이 됩니다.
  • T local; — 로컬 변수 슬롯도 마찬가지로 0으로 초기화된 채로 시작합니다.

만약 매개변수 없는 생성자가 자유롭게 허용되면 다음 모순이 생깁니다.

C#
struct Hp { public int Value; public Hp() { Value = 100; } }

Hp a = new Hp();        // Value = 100  (생성자 호출 시)
Hp b = default;         // Value = 0    (CLR이 메모리만 0으로)
Hp[] arr = new Hp[10];  // Value = 0    (배열 할당 시)

같은 타입을 만드는 세 구문이 결과가 다릅니다. 이 모순을 막기 위해 C# 9 이전에는 언어 차원에서 매개변수 없는 생성자 자체를 금지해버렸습니다. 그러면 어떤 경로로 만들든 항상 모든 필드가 0인 상태가 보장됩니다.

C# 10이 푼 것 — "모순을 인정하고 명시적으로 분리"

C# 10 팀의 결정은 이렇습니다.

"모순을 없애려고 표현력을 막지 말고, 모순을 명시적으로 보이게 해서 개발자가 알고 쓰게 하자."

그래서 C# 10부터:

  • struct에 매개변수 없는 생성자와 필드 이니셜라이저 허용.
  • 단, default(T)new T[N]은 여전히 생성자를 호출하지 않음.
  • new T()는 명시적으로 생성자를 호출.

CLR 규약은 그대로 유지됩니다. 컴파일러가 IL 레벨에서 call .ctorinitobj를 명확히 구분해서 내보낼 뿐입니다.

시각화 — 세 경로의 의사 결정 흐름

컴파일러는 코드 형태에 따라 다른 IL을 내보낸다

코드와 IL — defaultnew T[N].ctor을 부르지 않는다

C#
public struct Counter
{
    public int Value = 1;
    public Counter() { Value = 999; }   // 안에서 더 큰 값 대입
}

Counter()이 호출되면 Value999가 돼야 하지만, 다음 코드는 모두 0을 출력합니다.

C#
Counter b = default;
Counter[] arr = new Counter[1];
System.Console.WriteLine($"{b.Value}, {arr[0].Value}");
// 출력: 0, 0

IL을 보면 두 줄 다 .ctor을 호출하지 않습니다.

IL
// Counter b = default;
IL_0000: ldloca.s 0
IL_0002: initobj Counter           // ← 메모리 0, .ctor 무시

// Counter[] arr = new Counter[1];
IL_0008: ldc.i4.1
IL_0009: newarr Counter            // ← 배열 할당, 요소별 .ctor 무시

Counter::.ctor이 어셈블리에 분명히 존재하는데도 호출되지 않는 이유는, 위에서 본 CLR 규약 때문입니다. 컴파일러가 일부러 무시하는 게 아니라 언어 명세가 그렇게 정의돼 있습니다. 이 동작은 C# 10·11·12·13에서도 변하지 않으며, 앞으로도 변하지 않을 것입니다.


4. 실전 적용 — Unity에서 안전하게 쓰는 패턴

Unity 핫패스에서 자주 마주치는 상황

Unity에서 struct는 다음 상황에 자주 등장합니다.

  1. 인벤토리/스킬 슬롯 — 작은 데이터를 한 번에 100~1000개 단위로 관리. 보통 배열로.
  2. 이펙트/사운드 옵션 — 함수 인자로 옵션 묶음을 넘길 때. 종종 default로 "기본값"을 넘김.
  3. 게임 메시지/이벤트IComponentData(DOTS) 같은 곳에서 대량으로 0으로 채워진 채 시작.

이 모든 경우에 default(T) 또는 new T[N] 경로로 인스턴스가 만들어집니다. 즉, 우리가 정의한 매개변수 없는 생성자는 호출되지 않습니다.

Before — 매개변수 없는 생성자에 의존한 인벤토리

C#
// 신입 개발자가 흔히 쓰는 잘못된 패턴
public struct Slot
{
    public int Hp = 100;
    public string Name = "Empty";
    public Slot() { }
}

public class Inventory : MonoBehaviour
{
    private Slot[] _slots;

    void Start()
    {
        _slots = new Slot[10];                  // ← 여기서 .ctor 호출 안 됨
        Debug.Log(_slots[0].Hp);                 // 0 (기대: 100)
        Debug.Log(_slots[0].Name ?? "<null>");   // <null> (기대: "Empty")

        if (_slots[0].Name.Length > 0) { /* … */ }   // 💥 NullReferenceException
    }
}

여기서 new Slot[10]newarr 한 줄로 끝납니다. 결과적으로 모든 슬롯의 Hp0, Namenull입니다. 게임 로직이 "Hp=100을 시작값으로" 가정하고 있다면 모든 슬롯이 사망 처리됩니다.

해당 IL은 다음과 같습니다.

IL
// _slots = new Slot[10];
IL_0000: ldarg.0
IL_0001: ldc.i4.s 10
IL_0003: newarr Slot         // ← 단일 명령어. .ctor 호출 없음.
IL_0008: stfld Slot[] Inventory::_slots

After — "모두 0인 상태가 유효하도록" 설계

올바른 해법은 default 상태가 곧 의미 있는 기본 상태가 되도록 타입을 설계하는 것입니다.

C#
public struct Slot
{
    public int Hp;       // 0 = 빈 슬롯
    public int ItemId;   // 0 = "아이템 없음"을 의미하도록 ID 0번을 비움
    public bool IsEmpty => ItemId == 0;
}

public class Inventory : MonoBehaviour
{
    private Slot[] _slots;

    void Start()
    {
        _slots = new Slot[10];
        // 모든 슬롯이 IsEmpty == true. 의도와 일치.
    }

    public void Equip(int index, int itemId, int hp)
    {
        _slots[index] = new Slot { Hp = hp, ItemId = itemId };
    }
}
  • 필드 이니셜라이저·매개변수 없는 생성자를 쓰지 않습니다.
  • 모든 필드가 0인 상태(new T[]이든 default이든 class 안의 필드든)가 곧 "빈 슬롯"이라는 유효한 의미를 가집니다.
  • 0을 일반 ID로 쓰지 않고 "없음"의 의미로 비워두는 작은 규약을 둡니다.

이 패턴의 IL은 단순합니다.

IL
// _slots = new Slot[10];
IL_0000: ldarg.0
IL_0001: ldc.i4.s 10
IL_0003: newarr Slot         // ← 한 줄로 10개 슬롯 0으로 채움
IL_0008: stfld Slot[] Inventory::_slots

// _slots[index] = new Slot { Hp = hp, ItemId = itemId };
IL_0014: ldelema Slot         // ← 배열 요소 주소 직접 가져오기 (복사 없음)
IL_0019: initobj Slot         // ← 슬롯을 0으로 리셋
IL_001f: ldarg.3
IL_0020: stfld int32 Slot::Hp
IL_0025: ldarg.2
IL_0026: stfld int32 Slot::ItemId

배열 요소를 ldelema로 직접 주소를 잡고 그 자리에서 필드를 채우기 때문에, 임시 복사도 박싱도 없습니다. Unity 핫패스에서 GC 압력을 만들지 않는 깔끔한 패턴입니다.

"기본값이 의미 있는 상태"가 되지 않는 경우

만약 타입 특성상 0이 유효한 상태가 될 수 없다면 — 예: Damage 구조체에서 Multiplier = 0이면 모든 데미지가 0이 되어 버그가 됨 — struct보다는 class가 적합합니다. classnull 또는 명시적 new로만 만들 수 있어서 "초기화하지 않으면 컴파일러가 막아주거나 NRE로 즉시 알려줍니다".

상태 struct class
"모두 0" 상태가 유효 ✅ 적합 가능
"모두 0" 상태가 불유효 ⚠️ 위험 — 배열·default 경로에서 침묵하는 버그 ✅ 적합 — null이거나 명시 생성

5. 함정과 주의사항

함정 1 — 컬렉션 미리 잡기(new T[N], List<T>(N))

C#
// ❌ 신입의 직관: "new로 만들었으니 생성자가 호출됐겠지"
public struct Slot
{
    public int Hp = 100;
    public Slot() { }
}

var arr = new Slot[5];
Debug.Log(arr[0].Hp);   // 0  ← 100이 아니다

대응되는 IL:

IL
IL_0000: ldc.i4.5
IL_0001: newarr Slot          // ← .ctor 호출 없음
IL_0006: stloc.0
C#
// ✅ 정말로 생성자 결과가 필요하면 명시적 루프
var arr = new Slot[5];
for (int i = 0; i < arr.Length; i++) arr[i] = new Slot();   // ← 여기서 .ctor 호출

대응되는 IL의 핵심 줄:

IL
IL_0010: ldelema Slot                        // ← 배열 요소 주소
IL_0015: call instance void Slot::.ctor()    // ← 여기서 비로소 .ctor 호출

List<T>(capacity)도 마찬가지로 capacity는 내부 배열 크기를 미리 잡을 뿐, 요소를 만들지 않습니다. 요소를 채우려면 Add 또는 명시적 루프가 필요합니다.

함정 2 — 제네릭 new T() 제약

C#
// ❌ 제네릭에서 new T()를 쓰면, T가 struct여도 .ctor 호출이 보장되지 않는 경우가 있다
public static T MakeDefault<T>() where T : struct
{
    return new T();   // C# 10 이전: default(T)와 같음
                       // C# 10 이후: T에 매개변수 없는 .ctor이 정의돼 있어도 호출 보장 X
}

C# 10 이후에도 제네릭의 new T()default(T)와 동등하게 동작합니다(T : new() 제약을 붙여도 마찬가지). 이는 JIT가 제네릭 코드를 공유하기 위한 설계 결정입니다.

IL
// public static T MakeDefault<T>() where T : struct  ⇒  return new T();
IL_0000: ldloca.s 0
IL_0002: initobj !!T          // ← .ctor 호출이 아니라 initobj가 나온다
IL_0008: ldloc.0
IL_0009: ret

그래서 다음 두 호출은 같은 결과를 줍니다.

C#
Slot a = MakeDefault<Slot>();   // Hp = 0
Slot b = default(Slot);          // Hp = 0
Slot c = new Slot();             // Hp = 100  ← 직접 호출만 .ctor이 실행됨

올바른 패턴: 제네릭 팩토리에서 사용자 정의 초기화가 필요하면 Func<T> 등 명시적 팩토리를 받습니다.

C#
public static T MakeWith<T>(System.Func<T> factory) where T : struct
    => factory();

Slot s = MakeWith(() => new Slot());   // 여기서는 람다 안에서 .ctor 호출됨

함정 3 — class 안에 struct 필드

C#
// ❌ class 필드로 둔 struct는 객체 생성 시 .ctor 호출 안 됨
public class Player
{
    public Slot MainSlot;   // Slot에 매개변수 없는 .ctor이 있어도 호출 안 됨
}

var p = new Player();
Debug.Log(p.MainSlot.Hp);   // 0  ← 100이 아니다

new Player()Player 객체 메모리 전체를 0으로 초기화하고 Player::.ctor을 호출합니다. 이때 Slot::.ctor은 별도로 호출되지 않습니다 — Player.ctor이 직접 부르지 않는 한.

IL
// new Player()
IL_0000: newobj instance void Player::.ctor()   // Player의 .ctor만 호출
// Player의 .ctor은 Object::.ctor만 부르고 끝, MainSlot은 이미 0 상태

올바른 패턴: "필드의 0 상태가 유효한 의미"를 갖도록 설계하거나, Player 생성자에서 명시적으로 채웁니다.

C#
public class Player
{
    public Slot MainSlot = new Slot();   // 필드 이니셜라이저로 명시 (class의 필드 이니셜라이저는 호출됨)
}

class의 필드 이니셜라이저는 class.ctor이 시작될 때 자동으로 실행되므로 안전합니다.

함정 4 — init/required와의 상호작용

C# 11의 required가 붙은 필드도 default·new T[] 경로에서는 검사가 우회됩니다.

C#
public struct Config
{
    public required int Port;
}

Config a = new Config { Port = 80 };   // ✅ 컴파일러가 강제
Config b = default;                     // ⚠️ 컴파일은 통과하지만 Port = 0

default 경로는 객체 초기화자({ ... })를 거치지 않으므로 required 검사가 효력을 잃습니다. structrequired를 붙이는 건 "직접 new 호출 경로에서만" 작동한다는 점을 기억해야 합니다.


6. C# 버전별 변화

C# 9 이전 — 둘 다 금지

C#
// ❌ 컴파일 오류
public struct Counter
{
    public int Value = 1;       // CS0573: 구조체에서 인스턴스 필드 이니셜라이저는 사용할 수 없습니다
    public Counter() { }         // CS0568: 구조체는 명시적 매개변수 없는 생성자를 포함할 수 없습니다
}

매개변수 있는 생성자는 허용됐지만, 그 안에서 모든 필드를 명시적으로 초기화해야 했습니다.

C#
public struct Point
{
    public int X;
    public int Y;
    public int Z;

    // C# 9 이하: Y, Z를 빠뜨리면 CS0171 오류
    public Point(int x)
    {
        X = x;
        Y = 0;   // ← 명시적으로 채워야 함
        Z = 0;   // ← 명시적으로 채워야 함
    }
}

C# 10 — 매개변수 없는 생성자·필드 이니셜라이저 허용

C#
public struct Counter
{
    public int Value = 1;        // ✅ 허용
    public Counter() { }          // ✅ 허용
}

단, 매개변수가 있는 생성자에서는 여전히 모든 필드를 명시적으로 채워야 했습니다.

C#
public struct Point
{
    public int X;
    public int Y;

    public Point(int x)
    {
        X = x;
        // ❌ C# 10: CS0171 — Y를 초기화하지 않았다
    }
}

C# 11 — 자동 기본 구조체 (Auto-default Struct)

C# 11에서는 명시적으로 채우지 않은 필드를 컴파일러가 자동으로 default로 채워주는 기능이 추가됐습니다.

C#
public struct Measurement
{
    public int Value;
    public string Unit;
    public System.DateTime Timestamp;

    public Measurement(int value)
    {
        Value = value;
        // ✅ C# 11: Unit, Timestamp는 컴파일러가 자동으로 default로 채워줌
    }
}

이건 단순한 컴파일러 편의(syntactic sugar)이고, IL을 보면 컴파일러가 우리 대신 초기화 코드를 삽입한 흔적이 그대로 남습니다.

IL
.method public hidebysig specialname rtspecialname
    instance void .ctor (int32 'value') cil managed
{
    // 컴파일러가 자동 삽입: Unit = null
    IL_0000: ldarg.0
    IL_0001: ldnull
    IL_0002: stfld string Measurement::Unit

    // 컴파일러가 자동 삽입: Timestamp = default
    IL_0007: ldarg.0
    IL_0008: ldflda valuetype DateTime Measurement::Timestamp
    IL_000d: initobj DateTime

    // 우리가 작성한 코드: Value = value
    IL_0013: ldarg.0
    IL_0014: ldarg.1
    IL_0015: stfld int32 Measurement::Value
    IL_001a: ret
}

요점은 두 가지입니다.

  • C# 10까지는 개발자가 직접 빠진 필드를 0으로 채워야 했고, 안 채우면 CS0171 오류가 났습니다.
  • C# 11부터는 컴파일러가 빠진 필드의 초기화 IL을 자동으로 끼워 넣어 줍니다. 동작은 똑같지만 코드가 깔끔해집니다.

이 기능은 매개변수 있는 생성자에 한정되며, default(T)new T[N] 동작은 여전히 그대로입니다. C# 11로 올렸다고 해서 배열 원소가 갑자기 매개변수 없는 생성자 결과로 채워지는 게 아닙니다.

한눈에 보기

버전 매개변수 없는 .ctor 필드 이니셜라이저 매개변수 있는 .ctor에서 일부 필드 누락 default·new T[N].ctor 호출
C# 9 이하 ❌ 금지 ❌ 금지 ❌ CS0171 (해당 사항 없음)
C# 10 ✅ 허용 ✅ 허용 ❌ CS0171 ❌ 안 함
C# 11+ ✅ 허용 ✅ 허용 ✅ 자동 default 채움 ❌ 안 함

7. 정리

이 글의 핵심은 한 줄로 요약됩니다.

new T()는 생성자 호출이고, default(T)new T[N]은 메모리 0이다. C# 10·11이 새로 풀어준 문법은 이 차이를 없애지 않는다.

기억할 체크리스트:

  • C# 10: struct도 매개변수 없는 생성자와 필드 이니셜라이저를 가질 수 있음.
  • C# 11: 매개변수 있는 생성자에서 일부 필드를 빠뜨려도 컴파일러가 자동으로 default로 채움(자동 기본 구조체).
  • ⚠️ CLR 규약은 그대로: default(T)는 항상 모든 비트가 0. new T[N]도 마찬가지.
  • ⚠️ 세 IL 명령어 구분: call .ctor / initobj / newarr. 이들이 같은 결과를 보장하지 않음.
  • ⚠️ 제네릭 new T(): T : struct / T : new() 제약이 있어도 default(T)로 컴파일됨. 제네릭 팩토리에서 매개변수 없는 생성자 효과를 기대하지 말 것.
  • ⚠️ class 안의 struct 필드: Player.ctor이 끝나도 Slot::.ctor은 호출되지 않음.
  • Unity 베스트 프랙티스: "모두 0인 상태가 유효한 의미를 갖도록" struct를 설계한다. 매개변수 없는 생성자에 의존하지 않는다.
  • 타입 선택: "0 상태가 불유효"하면 class로 바꾸거나, ID 0번을 비워 두는 등 작은 규약을 둔다.

한 줄로: 구조체의 매개변수 없는 생성자는 "보너스 편의 기능"이지 "초기화 보장"이 아닙니다. CLR이 메모리를 0으로 미는 모든 경로(default·new T[]·class 필드·미할당 로컬)를 그대로 통과하므로, 안전한 설계는 여전히 "기본값이 의미 있는 상태가 되도록" 만드는 데에 있습니다.

반응형

+ Recent posts