반응형

[PART9.컬렉션 기본 사용법(6/8)] 컬렉션 초기화자 — { 1, 2, 3 }은 컴파일러가 어떻게 풀어쓰는가

컬렉션 초기화자는 Add() 호출로, 인덱서 초기화자는 set_Item() 호출로 변환된다. IL을 보면 차이가 명확하다.


1. 문제 제기 — 왜 { 1, 2, 3 }이 그냥 동작할까

Unity에서 인스펙터에 노출하지 않는 룩업 테이블을 코드로 직접 박아 넣고 싶을 때가 있습니다. 몬스터 ID와 능력치, 아이템 키와 가격 같은 데이터입니다. 만약 Awake()에서 한 줄씩 채워야 한다면 코드는 이렇게 됩니다.

C#
// 매번 보는 풍경 — Awake가 Add 호출로 가득 차는 케이스
var monsterHp = new Dictionary<string, int>();
monsterHp.Add("Slime", 10);
monsterHp.Add("Goblin", 30);
monsterHp.Add("Orc", 80);
monsterHp.Add("Dragon", 500);
// ... 50줄 더 ...

현업 코드에서 이 패턴은 두 가지 통증을 만듭니다. 첫째, 데이터가 어떤 모습인지 한눈에 보이지 않습니다. 둘째, 항목을 추가·삭제할 때 어디에 끼워야 할지 손이 머뭇거립니다.

C# 3.0에 도입된 컬렉션 초기화자(Collection Initializer) 와 C# 6.0의 인덱서 초기화자(Indexer Initializer) 는 이 통증을 해결합니다.

C#
// 한눈에 들어오는 형태
var monsterHp = new Dictionary<string, int>
{
    ["Slime"]  = 10,
    ["Goblin"] = 30,
    ["Orc"]    = 80,
    ["Dragon"] = 500,
};

그런데 이 문법은 단순한 "예쁘게 쓰기"가 아닙니다. 컴파일러가 풀어쓰는 방식이 두 문법에서 서로 다르고, 그 차이가 중복 키 처리·예외 발생 여부 같은 실전 동작 차이로 직결됩니다. 이 글에서는 IL 레벨에서 두 문법이 어떻게 변환되는지 확인하고, 신입 개발자가 헷갈리기 쉬운 함정을 정리합니다.


2. 개념 정의 — 컬렉션 초기화자는 Add() 호출의 단축어다

2.1 비유: 택배 상자에 물건 담기

택배를 보낼 때 빈 상자를 먼저 책상 위에 놓고, 물건을 하나씩 집어넣은 뒤 마지막에 라벨을 붙여 들고 나가는 모습을 떠올려봅니다. 컬렉션 초기화자는 정확히 이 과정의 속기(速記)입니다.

컬렉션 초기화자가 풀어쓰는 단계

2.2 가장 단순한 예시 — List<int>

C#
public static List<int> ListWithInitializer()
{
    return new List<int> { 1, 2, 3 };
}

겉보기에 한 줄이지만, 컴파일러는 이 코드를 "빈 List<int> 생성 → Add(1)Add(2)Add(3) → 반환" 의 네 단계로 풀어씁니다. 컬렉션 초기화자는 어디까지나 컴파일 타임의 문법 설탕(Syntactic Sugar) 이며, 런타임에서는 결국 Add 메서드 호출로 동작합니다.

{ ... } (컬렉션 초기화자) — Collection Initializer 객체 생성 표현식 뒤에 중괄호를 두고 그 안에 요소를 나열하는 문법. 컴파일러는 각 요소를 대상 타입의 Add() 메서드 호출로 풀어쓴다. C# 3.0에서 도입.
예시: var list = new List<int> { 1, 2, 3 }; → 빈 List<int> 생성 후 Add(1), Add(2), Add(3) 호출과 동일

2.3 IL 레벨로 한 번 검증하기

/il-analysis 로 위 메서드를 디컴파일한 결과입니다.

IL
.method public hidebysig static
    class List`1<int32> ListWithInitializer () cil managed
{
    .locals init (
        [0] class List`1<int32>
    )

    IL_0001: newobj instance void List`1<int32>::.ctor()  // 빈 List 생성
    IL_0006: dup                                          // 스택 중복(같은 참조 두 번 사용)
    IL_0007: ldc.i4.1                                     // 정수 1 푸시
    IL_0008: callvirt instance void List`1<int32>::Add(!0)  // Add(1) 호출
    IL_000e: dup
    IL_000f: ldc.i4.2
    IL_0010: callvirt instance void List`1<int32>::Add(!0)  // Add(2)
    IL_0016: dup
    IL_0017: ldc.i4.3
    IL_0018: callvirt instance void List`1<int32>::Add(!0)  // Add(3)
    IL_001e: stloc.0
    IL_0022: ret
}
callvirt — 가상 호출 명령 메서드를 호출하면서 대상 객체가 null인지 검사하고, 가상 메서드라면 실제 타입의 구현을 디스패치한다. C# 컴파일러는 인스턴스 메서드 호출에 거의 대부분 callvirt를 사용한다(가상이 아닌 메서드라도).

핵심 관찰은 두 가지입니다.

  1. newobj 한 번 + callvirt Add 세 번 — 우리가 손으로 풀어쓴 형태와 정확히 같은 IL이 나옵니다.
  2. dup 명령 — 새로 만든 List 참조를 스택에 한 번 복사해 두고, Add 호출이 끝나도 스택 위에 같은 참조가 남아 다음 Add 를 받을 수 있게 합니다. 임시 변수를 거치지 않는 컴파일러 최적화입니다.

3. 내부 동작 — 초기화자 vs 수동 Add는 IL이 거의 같다

3.1 두 형태의 IL 비교

같은 결과를 두 가지로 작성해 두면, 컴파일러가 정말로 같은 일을 하는지 한 번에 비교할 수 있습니다.

C#
// Before — 수동으로 빈 List 만들고 Add 반복
public static List<int> ListWithManualAdd()
{
    var list = new List<int>();
    list.Add(1);
    list.Add(2);
    list.Add(3);
    return list;
}

// After — 컬렉션 초기화자
public static List<int> ListWithInitializer()
{
    return new List<int> { 1, 2, 3 };
}

두 메서드의 IL을 나란히 보면 차이가 매우 작습니다.

IL
// 수동 Add 버전
IL_0001: newobj  List`1<int32>::.ctor()
IL_0006: stloc.0                                  // 로컬 변수 list에 저장
IL_0007: ldloc.0                                  // 로컬 변수 다시 꺼냄
IL_0008: ldc.i4.1
IL_0009: callvirt List`1<int32>::Add(!0)
IL_000f: ldloc.0
IL_0010: ldc.i4.2
IL_0011: callvirt List`1<int32>::Add(!0)
IL_0017: ldloc.0
IL_0018: ldc.i4.3
IL_0019: callvirt List`1<int32>::Add(!0)
IL_001f: ldloc.0
IL_0020: stloc.1
IL_0024: ret

// 초기화자 버전
IL_0001: newobj  List`1<int32>::.ctor()
IL_0006: dup                                      // 스택 위에 그대로 복사
IL_0007: ldc.i4.1
IL_0008: callvirt List`1<int32>::Add(!0)
IL_000e: dup
IL_000f: ldc.i4.2
IL_0010: callvirt List`1<int32>::Add(!0)
IL_0016: dup
IL_0017: ldc.i4.3
IL_0018: callvirt List`1<int32>::Add(!0)
IL_001e: stloc.0
IL_0022: ret
dup / stloc / ldloc — 스택 명령 dup는 평가 스택의 맨 위 값을 그대로 한 번 더 복사한다. stloc.N은 스택 맨 위 값을 N번 로컬 변수에 저장하고 스택에서 빼며, ldloc.N은 N번 로컬 변수의 값을 스택에 다시 푸시한다.

차이는 단 하나입니다. 수동 버전은 로컬 변수에 저장한 뒤 매번 다시 꺼내고(stlocldloc × 3), 초기화자 버전은 스택 위의 참조를 dup 으로 복사해서 그대로 사용합니다. JIT(Just-In-Time, IL을 실행 시점에 네이티브 코드로 변환하는 런타임 컴파일러) 컴파일러는 두 형태를 거의 동일한 네이티브 코드로 옮기므로 런타임 성능 차이는 사실상 없습니다.

핵심: 컬렉션 초기화자는 "빠른 코드"를 만들어주는 게 아니라 "읽기 좋은 코드" 를 만들어줍니다.

3.2 컴파일러 요구 조건 — IEnumerable + Add(...)

컬렉션 초기화자가 동작하려면 대상 타입이 두 조건을 모두 만족해야 합니다.

컬렉션 초기화자 적용 조건

여기서 미묘한 부분이 있습니다. Add 메서드는 어떤 인터페이스에도 강제되지 않습니다. 컴파일러는 단순히 "이름이 Add이고, 인자 타입이 호환되는 메서드"를 이름 기반으로 찾습니다. 그래서 ICollection<T> 같은 인터페이스를 구현하지 않아도, 그냥 IEnumerable 구현 + Add 메서드만 있으면 됩니다.

3.3 사용자 정의 컬렉션 IL 확인

직접 만든 Team 클래스로 컬렉션 초기화자가 정말로 동작하는지, 그리고 IL이 어떻게 나오는지 확인해 봅니다.

C#
public class Team : IEnumerable<string>
{
    private readonly List<string> _players = new List<string>();
    public void Add(string player) => _players.Add(player);
    public IEnumerator<string> GetEnumerator() => _players.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public static Team CreateTeam()
{
    return new Team { "박지성", "손흥민", "이강인" };
}
IL
.method public hidebysig static
    class Program/Team CreateTeam () cil managed
{
    IL_0001: newobj instance void Program/Team::.ctor()
    IL_0006: dup
    IL_0007: ldstr "박지성"
    IL_000c: callvirt instance void Program/Team::Add(string)   // 우리가 만든 Add 호출
    IL_0012: dup
    IL_0013: ldstr "손흥민"
    IL_0018: callvirt instance void Program/Team::Add(string)
    IL_001e: dup
    IL_001f: ldstr "이강인"
    IL_0024: callvirt instance void Program/Team::Add(string)
    IL_002a: stloc.0
    IL_002e: ret
}

List<int> 의 경우와 IL 패턴이 완전히 같습니다. 컴파일러는 사용자 정의 클래스든 BCL(Base Class Library, .NET 표준 클래스 라이브러리) 컬렉션이든 동일하게 newobj + dup + callvirt Add 패턴으로 풀어씁니다. 즉 컬렉션 초기화자는 특정 타입에 묶인 기능이 아니라 "조건을 만족하는 모든 타입에 자동으로 따라붙는" 컴파일러 기능입니다.


4. 실전 적용 — 인덱서 초기화자가 더 나은 자리

4.1 두 방식이 만드는 IL이 어떻게 다른가

C# 6.0부터는 딕셔너리류에 대해 인덱서 초기화자가 추가됐습니다. 똑같은 결과를 두 가지로 쓸 수 있습니다.

C#
// Before — 컬렉션 초기화자 ({ "key", value } 쌍 형식)
public static Dictionary<string, int> DictWithCollectionInit()
{
    return new Dictionary<string, int>
    {
        { "a", 1 },
        { "b", 2 }
    };
}

// After — 인덱서 초기화자 ([key] = value 형식)
public static Dictionary<string, int> DictWithIndexerInit()
{
    return new Dictionary<string, int>
    {
        ["a"] = 1,
        ["b"] = 2
    };
}
["key"] = value (인덱서 초기화자) — Indexer Initializer 객체 초기화자 안에서 인덱서 setter를 직접 호출하는 문법. 컴파일러는 각 항목을 obj[key] = value (즉 set_Item(key, value) 호출)로 풀어쓴다. C# 6.0에서 도입.
예시: var dict = new Dictionary<string, int> { ["x"] = 1 }; → 빈 Dictionary 생성 후 dict["x"] = 1 과 동일한 인덱서 setter 호출

4.2 IL을 비교하면 결정적 차이가 보인다

IL
// 컬렉션 초기화자 — Add 호출
IL_0001: newobj instance void Dictionary`2<string, int32>::.ctor()
IL_0006: dup
IL_0007: ldstr "a"
IL_000c: ldc.i4.1
IL_000d: callvirt instance void Dictionary`2<string, int32>::Add(!0, !1)
IL_0013: dup
IL_0014: ldstr "b"
IL_0019: ldc.i4.2
IL_001a: callvirt instance void Dictionary`2<string, int32>::Add(!0, !1)
IL_001f: stloc.0
IL_0024: ret

// 인덱서 초기화자 — set_Item 호출
IL_0001: newobj instance void Dictionary`2<string, int32>::.ctor()
IL_0006: dup
IL_0007: ldstr "a"
IL_000c: ldc.i4.1
IL_000d: callvirt instance void Dictionary`2<string, int32>::set_Item(!0, !1)
IL_0013: dup
IL_0014: ldstr "b"
IL_0019: ldc.i4.2
IL_001a: callvirt instance void Dictionary`2<string, int32>::set_Item(!0, !1)
IL_001f: stloc.0
IL_0024: ret
set_Item — 인덱서 setter의 IL 메서드 이름 C#의 this[T key] { set; } 인덱서는 IL 단계에서 set_Item(T key, V value) 메서드로 변환된다. obj[k] = v 코드는 항상 이 메서드 호출로 컴파일된다.

같은 자리에서 똑같은 모양으로 보이던 두 코드가 IL에서는 Add(!0, !1) vs set_Item(!0, !1) 으로 명확히 갈라집니다. 호출하는 메서드가 다르기 때문에 동작 의미도 달라집니다.

두 초기화자가 호출하는 메서드

4.3 Unity 실전: 룩업 테이블을 깔끔하게

Unity에서 가장 자주 쓰이는 패턴은 enum 또는 ID를 키로 하는 룩업 테이블입니다.

C#
// Before — Awake가 Add 호출로 가득
public class MonsterDB : MonoBehaviour
{
    private Dictionary<MonsterType, int> _hpTable;

    void Awake()
    {
        _hpTable = new Dictionary<MonsterType, int>();
        _hpTable.Add(MonsterType.Slime, 10);
        _hpTable.Add(MonsterType.Goblin, 30);
        _hpTable.Add(MonsterType.Orc, 80);
        _hpTable.Add(MonsterType.Dragon, 500);
    }
}

// After — 인덱서 초기화자로 한눈에
public class MonsterDB : MonoBehaviour
{
    private static readonly Dictionary<MonsterType, int> HpTable = new()
    {
        [MonsterType.Slime]  = 10,
        [MonsterType.Goblin] = 30,
        [MonsterType.Orc]    = 80,
        [MonsterType.Dragon] = 500,
    };
}

After 패턴에서 두 가지가 추가로 좋아졌습니다.

  1. static readonly 로 한 번만 초기화 — 매 인스턴스 Awake() 마다 새 딕셔너리를 만들 필요가 없습니다. MonoBehaviour 가 여러 개 생기는 경우 GC(Garbage Collector, 사용하지 않는 객체를 자동 회수하는 런타임 구성요소) 부담이 줄어듭니다.
  2. 표 형태의 시각적 구조 — 기획자가 코드 리뷰를 같이 본다 해도 어떤 몬스터의 HP가 얼마인지 한 줄에 한 항목으로 읽을 수 있습니다.

4.4 Unity 실전: 인스펙터 직렬화의 한계

한 가지 못 박아둘 점이 있습니다. Dictionary<TKey, TValue> 자체는 Unity 인스펙터에 노출되지 않습니다. Unity의 직렬화기는 딕셔너리를 지원하지 않기 때문입니다. 그래서 실무 패턴은 다음과 같이 두 단계로 나눕니다.

C#
public class MonsterDB : MonoBehaviour
{
    [System.Serializable]
    public struct Entry
    {
        public MonsterType type;
        public int hp;
    }

    [SerializeField] private List<Entry> entries;  // 인스펙터에 노출되는 것

    private Dictionary<MonsterType, int> _hpTable;  // 런타임 룩업용

    void Awake()
    {
        // 인스펙터 데이터를 인덱서 초기화자 형태로는 못 쓰지만
        // ToDictionary 한 줄로 변환 가능
        _hpTable = entries.ToDictionary(e => e.type, e => e.hp);
    }
}

규칙: 기획 데이터는 인스펙터로 입력 → Awake() 에서 딕셔너리로 변환, 하드코딩된 상수 데이터는 static readonly 인덱서 초기화자로 선언. 두 패턴을 섞지 않는 것이 깔끔합니다.


5. 함정과 주의사항

5.1 ❌ 같은 키를 두 번 쓰면 어떤 일이 일어나는가

이 글에서 가장 중요한 함정입니다. 두 초기화자는 중복 키에 대해 정반대로 동작합니다.

C#
// ❌ 컬렉션 초기화자 + 중복 키 → 런타임 예외!
try
{
    var dict = new Dictionary<string, int>
    {
        { "a", 1 },
        { "a", 2 }   // Add("a", 2) → ArgumentException
    };
}
catch (ArgumentException ex)
{
    Debug.LogError(ex.Message);
    // "An item with the same key has already been added. Key: a"
}

// ✅ 인덱서 초기화자 + 중복 키 → 마지막 값으로 덮어쓰기
var safe = new Dictionary<string, int>
{
    ["a"] = 1,
    ["a"] = 2   // set_Item("a", 2) → 조용히 덮어씀
};
Debug.Log(safe["a"]);  // 2

이유는 4.2에서 본 IL에 그대로 드러납니다. Add 메서드는 키가 이미 있으면 ArgumentException 을 던지도록 구현돼 있고, set_Item(인덱서 setter) 은 그냥 덮어쓰도록 구현돼 있기 때문입니다. 문법이 비슷해 보여도 호출하는 메서드가 다르면 동작이 달라집니다.

Unity 게임 빌드에서 이 실수가 무서운 이유는 에디터에서는 동작하다가 데이터가 늘어난 어느 시점부터 모바일에서만 크래시 하는 패턴이 흔하기 때문입니다. 키 충돌이 우연히 생기는 시점부터 갑자기 Awake 가 예외를 뱉습니다.

5.2 ❌ IEnumerable 만 구현하고 Add 가 없는 클래스

컴파일러가 컬렉션 초기화자를 적용하려면 Add 라는 이름의 메서드가 반드시 있어야 합니다. 한쪽만 있으면 컴파일 에러입니다.

C#
// ❌ Add 메서드가 없음 — 컴파일 에러
public class ReadOnlyTeam : IEnumerable<string>
{
    private readonly List<string> _players = new() { "박지성" };
    public IEnumerator<string> GetEnumerator() => _players.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// var team = new ReadOnlyTeam { "손흥민" };
// CS1061: 'ReadOnlyTeam' does not contain a definition for 'Add'

// ✅ Add 메서드를 추가
public class WritableTeam : IEnumerable<string>
{
    private readonly List<string> _players = new();
    public void Add(string player) => _players.Add(player);   // 이게 있어야 동작
    public IEnumerator<string> GetEnumerator() => _players.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

var team = new WritableTeam { "손흥민" };  // OK

핵심: 읽기 전용 컬렉션을 만들고 싶다면 컬렉션 초기화자도 비활성화 됩니다. 둘 중 하나입니다 — 초기화자를 쓰려면 Add 를 열어두거나, 아니면 생성자로만 채우거나.

5.3 ❌ 초기화자 안에서 같은 객체 참조를 두 번 호출

이건 미묘한 함정입니다. 컬렉션 초기화자는 dup 으로 같은 참조를 재사용하는데, 만약 Add 메서드 내부에서 다른 Add 를 호출하는 식의 재진입이 생기면 의도치 않은 결과가 생길 수 있습니다.

C#
// ❌ Add 안에서 자기 자신을 또 건드리는 패턴 (실제로 거의 안 짜지만)
public class WeirdList : List<int>
{
    public new void Add(int x)
    {
        base.Add(x);
        if (x == 1) base.Add(99);   // Add 안에서 다시 Add
    }
}

var w = new WeirdList { 1, 2, 3 };
// 결과: { 1, 99, 2, 3 } — 직관적이지 않음

// ✅ Add 메서드는 단일 책임만 — 추가만, 다른 Add 호출 금지
public class CleanList : List<int>
{
    public new void Add(int x) => base.Add(x);
}

규칙: 사용자 정의 컬렉션의 Add 메서드는 "한 항목 추가" 만 한다. 부수 효과를 넣으면 컬렉션 초기화자의 가독성이 무너집니다.

5.4 ❌ null 값 오해 — Add(null) 은 호출된다

C#
// ❌ "null은 알아서 걸러지겠지" 라고 기대하면 틀림
var list = new List<string> { "박지성", null, "손흥민" };
// list.Count == 3
// list[1] == null

// ✅ null 을 허용하지 않으려면 직접 가드
string a = "박지성", b = null, c = "손흥민";
var list = new List<string>();
foreach (var name in new[] { a, b, c })
    if (name != null) list.Add(name);

컬렉션 초기화자는 그저 Add(...) 의 단축어이므로, Add(null) 이 허용되는 컬렉션이라면 그대로 들어갑니다. 필터링은 자동으로 일어나지 않습니다.


6. C# 버전별 변화

6.1 C# 3.0 — 컬렉션 초기화자 도입

처음 도입된 형태는 Add(T) 호출 형식 한 가지였습니다. IEnumerable 구현 + Add 메서드만 있으면 어떤 타입에든 적용됐습니다.

C#
// C# 3.0
var list = new List<int> { 1, 2, 3 };
var dict = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } };  // Add 호출

6.2 C# 6.0 — 인덱서 초기화자 추가

C# 6.0에서 두 가지가 추가됐습니다.

  1. 인덱서 초기화자["key"] = value 형식. set_Item(key, value) 으로 풀어쓰기.
  2. 확장 메서드 Add 인정 — 클래스 본체에 Add 가 없어도, 확장 메서드로 정의된 Add 가 있으면 컬렉션 초기화자가 동작합니다.
C#
// C# 6.0
var dict = new Dictionary<string, int>
{
    ["a"] = 1,
    ["b"] = 2
};

// 확장 메서드로 Add 제공 — 외부 라이브러리 클래스에도 초기화자 적용 가능
public static class TeamExt
{
    public static void Add(this ReadOnlyTeam team, string player) { /* ... */ }
}

6.3 C# 12 — 컬렉션 식 (다음 주제 예고)

C# 12에서는 한 단계 더 짧은 컬렉션 식(Collection Expression) [1, 2, 3] 이 도입됐습니다. 차이를 미리 한 줄로 정리하면:

  컬렉션 초기화자 (C# 3.0+) 컬렉션 식 (C# 12+)
문법 new List<int> { 1, 2, 3 } [1, 2, 3]
타입 new 로 명시 필요 대상 타입 추론
메서드 항상 Add 호출 타입에 따라 최적 코드 (배열은 직접 할당)
전개 연산자 없음 [..a, ..b] 지원

다음 주제 에서는 이 컬렉션 식이 IL 레벨에서 어떻게 다른지(특히 배열을 만들 때 Add 호출이 사라지고 Span<T> 기반으로 직접 채워지는 점) 자세히 다룰 예정입니다. 컬렉션 초기화자가 구식이라기보다는, 두 문법이 공존하면서 상황에 맞춰 골라 쓰는 형태가 됩니다.

C#
// C# 12 — 미리 보기
List<int> list = [1, 2, 3];          // 타입 추론
int[] array = [1, 2, 3];             // 동일 문법으로 배열도 가능
List<int> combined = [..list, 4, 5]; // 전개 연산자

7. 정리

이 글에서 다룬 내용을 한 줄씩 정리합니다.

  • [ ] new List<int> { 1, 2, 3 } 은 컴파일러가 newobj + Add 반복 호출 로 풀어쓰는 문법 설탕이다.
  • [ ] 컬렉션 초기화자 동작 조건은 두 가지 — IEnumerable 구현 + 접근 가능한 Add 메서드. 인터페이스가 강제하는 게 아니라 이름 매칭 으로 찾는다.
  • [ ] 인덱서 초기화자 ["key"] = valueset_Item(key, value) 호출로 풀어쓴다 — 컬렉션 초기화자(Add) 와 호출 메서드가 다르다.
  • [ ] 중복 키: Add 호출은 ArgumentException 을 던지고, set_Item 은 조용히 덮어쓴다. Unity 모바일에서 늦게 터지는 크래시의 흔한 원인.
  • [ ] 사용자 정의 클래스도 IEnumerable + Add 만 구현하면 컬렉션 초기화자를 쓸 수 있다. C# 6 부터는 확장 메서드 Add 도 인정.
  • [ ] 초기화자 vs 수동 Add — IL은 거의 같다. 성능이 아니라 가독성을 위한 문법이다.
  • [ ] Unity 실전 — 하드코딩 룩업은 static readonly + 인덱서 초기화자, 인스펙터 데이터는 List<Entry> + ToDictionary 변환.
  • [ ] C# 12의 컬렉션 식 [1, 2, 3] 은 다음 주제. 타입 추론 + 전개 연산자 + 배열 최적화 가 추가된 진화형이다.
반응형

+ Recent posts