기본 패턴
// 동일한 문법으로 다양한 컬렉션 타입 초기화
int[] arr = [1, 2, 3];
List<int> list = [1, 2, 3];
Span<int> span = [1, 2, 3];
IEnumerable<int> seq = [1, 2, 3];
// spread 연산자 .. 로 컬렉션 병합
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] merged = [..first, ..second, 7]; // [1, 2, 3, 4, 5, 6, 7]
// 빈 컬렉션
List<string> empty = [];설명
통일된 문법이 필요한 이유 — 타입별로 달랐던 초기화 방식
이전까지는 컬렉션 타입마다 초기화 문법이 달랐습니다. 배열은 new int[] { }, 리스트는 new List<int> { }, 빈 컬렉션은 Array.Empty<T>() 또는 Enumerable.Empty<T>(). 코드 리뷰어는 맥락 없이는 의도를 추론해야 했고, 타입을 바꿀 때마다 초기화 코드도 함께 수정해야 했습니다.
컬렉션 식은 [...] 리터럴 하나로 이 모든 경우를 통일합니다. 컴파일러는 대상 타입을 보고 가장 효율적인 구현을 선택하므로 성능 손실도 없습니다.
// 이전 방식 — 타입마다 다른 문법
int[] old1 = new int[] { 1, 2, 3 };
int[] old2 = new[] { 1, 2, 3 }; // 타입 추론 버전
List<int> old3 = new List<int> { 1, 2, 3 };
List<int> old4 = [1, 2, 3]; // ✅ C# 12 컬렉션 식
// 빈 컬렉션 — 이전 방식
int[] emptyArr = Array.Empty<int>();
List<int> emptyList = new List<int>();
// ✅ C# 12 — 통일된 표기
int[] emptyArr2 = [];
List<int> emptyList2 = [];spread 연산자 .. — 컬렉션 병합을 한 줄로
.. 연산자는 다른 컬렉션의 모든 요소를 현재 위치에 펼쳐 넣습니다. 여러 컬렉션을 합치거나 기존 컬렉션에 요소를 추가한 새 컬렉션을 만들 때 Concat, AddRange보다 훨씬 읽기 쉬운 코드를 만들 수 있습니다.
string[] animals = ["cat", "dog"];
string[] plants = ["rose", "tulip"];
string[] extra = ["sun"];
// 이전 방식
var merged1 = animals.Concat(plants).Concat(extra).ToArray();
var merged2 = new List<string>(animals);
merged2.AddRange(plants);
merged2.Add("sun");
// ✅ spread 연산자 — 순서와 내용이 한눈에 보임
string[] merged3 = [..animals, ..plants, ..extra];
// ["cat", "dog", "rose", "tulip", "sun"]
// 앞뒤에 고정 요소 추가도 자연스럽게
string[] all = ["start", ..animals, "middle", ..plants, "end"];타입 추론 규칙 — 컴파일러가 최적 구현을 선택하는 원리
컬렉션 식은 **대상 타입(target type)**을 기준으로 동작합니다. 변수 선언부의 타입, 메서드 파라미터 타입, 반환 타입 등을 보고 컴파일러가 내부적으로 어떤 컬렉션을 생성할지 결정합니다.
int[]→new int[] { 1, 2, 3 }과 동등한 IL 생성List<int>→new List<int> { 1, 2, 3 }과 동등Span<int>→ 스택 할당 가능한 경우 힙 할당 없이 처리ImmutableArray<int>→ImmutableArray.Create(1, 2, 3)과 동등
// Span은 스택 기반 — 힙 할당 최소화
void ProcessNumbers(Span<int> numbers)
{
foreach (var n in numbers) Console.WriteLine(n);
}
// 호출 측에서 컬렉션 식을 그대로 전달
ProcessNumbers([10, 20, 30]); // 스택 할당으로 최적화 가능
// ImmutableArray와도 동작
ImmutableArray<string> tags = ["csharp", "dotnet", "linq"];var로는 타입 추론 불가 — 명시적 타입이 필요한 이유
컬렉션 식은 대상 타입이 명확해야 컴파일러가 구현을 선택할 수 있습니다. var는 타입을 추론할 대상이 없으므로 컴파일 오류가 발생합니다. 이는 컬렉션 식이 "리터럴"이 아니라 "대상 타입 의존 표현식"이기 때문입니다.
// ❌ var로는 타입 추론 불가 — 컴파일 오류
var items = [1, 2, 3]; // error CS9176: 컬렉션 식에 대상 형식이 없습니다
// ✅ 명시적 타입 선언
int[] items1 = [1, 2, 3];
List<int> items2 = [1, 2, 3];
// ✅ 타입이 추론되는 컨텍스트에서는 사용 가능
int[] result = GetNumbers();
static int[] GetNumbers() => [1, 2, 3]; // 반환 타입이 명확하므로 OK빠른 정리
| 상황 | 적합한 선택 |
|---|---|
| 배열, List, Span 초기화 | Type name = [elem1, elem2, ...] |
| 여러 컬렉션 병합 | [..a, ..b, extraElem] spread 연산자 |
| 빈 컬렉션 생성 | Type name = [] (Array.Empty<T>() 대체) |
| 메서드에서 컬렉션 반환 | return [elem1, elem2]; (반환 타입 명시) |
var와 함께 사용 | 불가 — 명시적 타입 필요 |
주의할 점
var로는 컬렉션 식을 사용할 수 없습니다. 컴파일러가 어떤 타입의 컬렉션을 만들어야 할지 알 수 없기 때문입니다. 반드시 대상 타입을 명시해야 합니다.
// ❌ 컴파일 오류
var nums = [1, 2, 3];
// ✅ 타입 명시
int[] nums1 = [1, 2, 3];
List<int> nums2 = [1, 2, 3];또한 spread 연산자 ..는 IEnumerable<T>를 구현한 컬렉션이라면 모두 펼칠 수 있지만, 대상 타입이 Span<T>인 경우 피연산자도 Span<T> 또는 배열이어야 합니다. 임의의 IEnumerable을 Span 컨텍스트에서 spread하면 컴파일 오류가 발생합니다.