[PART6.배열과 문자열 기본(1/14)] 1차원 배열 — 가장 단순하지만 가장 깊은 데이터 구조
선언·초기화 4가지 / System.Array 위에 올라간 메모리 구조 / 인덱스 0 시작이 빨라지는 이유 / Length 고정과 IL 명령어(newarr·ldelem·stelem·ldlen) / Unity 핫패스에서 캐싱 배열로 GC alloc 없애기
목차
1. 왜 1차원 배열을 다시 봐야 하는가
신입 시절 Unity 코드에서 자주 마주치는 한 줄이 있습니다.
RaycastHit[] hits = Physics.RaycastAll(origin, dir);
문법은 익숙합니다. 배열을 받았고, hits.Length만큼 for로 돌면 됩니다. 그런데 모바일 빌드에서 프로파일러를 돌리면 이 한 줄이 매 프레임 GC(Garbage Collector — 더 이상 쓰지 않는 객체를 자동으로 회수해 메모리를 돌려주는 런타임 구성요소) Alloc을 만들고 있고, 60FPS 게임에서 GC가 한 번 돌 때마다 5~10밀리초씩 프레임이 끊긴다는 것을 알게 됩니다.
같은 문제를 푸는 두 줄짜리 차이는 이렇게 생겼습니다.
// ❌ 매 호출 새 배열 할당
RaycastHit[] hits = Physics.RaycastAll(origin, dir);
// ✅ 미리 할당해 둔 배열을 재사용
private static readonly RaycastHit[] _hits = new RaycastHit[16];
int n = Physics.RaycastNonAlloc(origin, dir, _hits);
두 코드의 차이를 설명할 수 있으려면 1차원 배열이 메모리에서 어떤 모양으로 살아 있는지, 왜 매 호출 새 배열을 만드는 게 비싸고 한 번 만들어 둔 것을 다시 쓰는 게 싼지를 알아야 합니다. "배열은 그냥 데이터 통이다"로는 위 두 줄의 차이를 설명할 수 없습니다.
이 글에서 풀고자 하는 질문은 세 가지입니다.
- 1차원 배열은 메모리에 어떻게 놓여 있는가?
- 인덱스가 왜 0부터 시작하고,
for와foreach중 무엇을 쓰는 게 빠른가? - Unity 핫패스에서 배열을 어떻게 다뤄야 GC가 한가해지는가?
2. 1차원 배열이란 무엇인가
비유 — 우편함이 일렬로 붙은 한 동짜리 아파트
1차원 배열은 길이가 정해진 우편함 한 줄이라고 보면 직관적입니다. 같은 크기의 칸이 0번부터 차례로 붙어 있고, 한 칸의 위치만 알면 다른 어떤 칸도 한 번의 계산으로 찾아갈 수 있습니다. 칸 수는 입주 시점에 박힌 채 절대 늘지 않습니다 — 더 많은 우편함이 필요하면 새 동(새 배열)을 짓고 기존 내용을 옮겨야 합니다.
메모리 레이아웃 — 객체 헤더 + 길이 + 요소 영역
배열 변수는 스택 위의 작은 슬롯(보통 8바이트)일 뿐이고, 실제 데이터는 모두 힙(heap — 가비지 컬렉터가 관리하는 동적 메모리 영역)에 살고 있습니다. 변수는 그 힙 객체를 가리키는 참조입니다.

배열 객체는 어떤 요소 타입이든 항상 힙에 자리 잡습니다. 머리에는 다른 .NET 객체와 똑같은 객체 헤더(타입 정보 + 동기화 블록)가 있고, 그다음에 변하지 않는 Length가 박혀 있고, 이어서 실제 요소들이 한 줄로 누워 있습니다. 이 "한 줄로 누워 있다"는 것이 배열의 모든 성능 특성을 결정합니다 — CPU 캐시가 잘 듣고, 인덱스 접근이 한 번의 곱셈+덧셈으로 끝나고, 길이가 박혀 있어 매 접근마다 범위 검사를 할 수 있다는 것이 모두 이 레이아웃에서 나옵니다.
네 가지 선언 방식
// (a) 크기만 지정 — 모든 칸이 기본값(int=0, bool=false, 참조타입=null)
int[] a = new int[3];
// (b) 길이 + 값 동시
int[] b = new int[] { 10, 20, 30 };
// (c) 변수 선언 한정 단축형 — (b)와 동일
int[] c = { 10, 20, 30 };
// (d) 컴파일러가 요소 타입 추론
int[] d = new[] { 10, 20, 30 };
(b)·(c)·(d)는 어디까지나 표기법 차이일 뿐 컴파일 결과가 같습니다. (c)는 변수를 처음 선언하는 자리에서만 쓸 수 있고(메서드 인자나 return 자리에서는 불가), (d)는 요소 타입을 적지 않아도 되니 var와 함께 자주 등장합니다.
이 셋이 정말 같은지 확인하려면 IL(Intermediate Language — C# 컴파일러가 만드는 중간 코드, JIT가 실행 직전에 기계어로 다시 번역한다)을 들여다보면 됩니다.
// FixedSize() — new int[3]
IL_0000: ldc.i4.3 // 길이 3을 스택에 푸시
IL_0001: newarr [System.Runtime]System.Int32 // int 배열을 힙에 새로 할당 (모든 칸 0으로 초기화)
IL_0006: ret
// WithValues() — new int[] { 10, 20, 30 }
IL_0000: ldc.i4.3 // 길이 3
IL_0001: newarr [System.Runtime]System.Int32 // 빈 배열 할당
IL_0006: dup // 스택의 배열 참조 복제
IL_0007: ldtoken field ... '__StaticArrayInitTypeSize=12' ... '97CA15...' // 사전 정의된 12바이트 데이터 블록 핸들
IL_000c: call void [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers
::InitializeArray(...) // 데이터 블록을 배열에 통째로 복사
IL_0011: ret
newarr 한 줄이 곧 GC 부담입니다. 길이만 적은 (a) 형태는 그 자리에서 0으로 채운 새 객체를 힙에 던지고, 초기값이 있는 (b)·(c)·(d) 형태는 빈 배열을 만든 뒤 컴파일러가 어셈블리 어딘가에 박아 둔 정적 바이트 블록(__StaticArrayInitTypeSize=12)을 RuntimeHelpers.InitializeArray로 한 번에 복사합니다. 한 칸씩 stelem.i4로 채우는 것보다 빠르지만 — 새 객체를 만든다는 사실은 변하지 않습니다. 핫패스에서 새 배열을 만드는 비용이 비싼 이유는 바로 이 newarr가 매번 호출되기 때문입니다.
3. 내부 동작 — 인덱스 접근, Length, 그리고 IL이 보여주는 진실
인덱스 0부터 시작하는 진짜 이유
"왜 1이 아니고 0인가?"
답은 메모리 주소 계산식입니다. CPU가 arr[i]를 만났을 때 실행하는 식은 단순합니다.
요소 주소 = 배열 시작 주소 + i × 요소 크기
i = 0이면 곱셈 결과가 0이라 그냥 시작 주소가 첫 요소 주소가 됩니다. 만약 1부터 셌다면 매 접근마다 (i - 1) 빼기 한 번이 추가로 들어갑니다. 한 번의 뺄셈이라 별것 아니라고 느낄 수 있지만, 1프레임에 수만 번 인덱싱이 일어나는 게임 루프에서는 의미 있는 차이가 됩니다. 그리고 무엇보다 — 0 시작은 C·C++부터 이어진 관례라 다른 언어와 데이터를 주고받을 때 인덱스 변환을 하지 않아도 됩니다.
for vs foreach — 의외로 IL이 거의 같다
신입 때 자주 듣는 말 중 하나가 "foreach는 IEnumerator를 만들기 때문에 느리다"입니다. 일반 컬렉션에서는 사실이지만, 배열에서는 컴파일러가 foreach를 사실상 for로 다시 써 줍니다.
public static int SumFor(int[] xs)
{
int sum = 0;
for (int i = 0; i < xs.Length; i++) sum += xs[i];
return sum;
}
public static int SumForeach(int[] xs)
{
int sum = 0;
foreach (int x in xs) sum += x;
return sum;
}
// SumFor 핵심 부분
IL_0007: ldloc.0 // sum
IL_0008: ldarg.0 // 배열 xs
IL_0009: ldloc.1 // 인덱스 i
IL_000a: ldelem.i4 // xs[i] (int) 로드 — 내부적으로 범위 검사 포함
IL_000b: add // sum + xs[i]
IL_000c: stloc.0 // sum 갱신
...
IL_0011: ldloc.1 // i
IL_0012: ldarg.0 // xs
IL_0013: ldlen // xs.Length (메서드 호출 아님 — 배열 전용 한 명령)
IL_0014: conv.i4
IL_0015: clt // i < Length 비교
// SumForeach 핵심 부분
IL_000a: ldloc.1 // 임시 변수에 담긴 배열
IL_000b: ldloc.2 // 인덱스
IL_000c: ldelem.i4 // xs[i]
...
IL_0017: ldloc.1
IL_0018: ldlen // ← foreach인데도 ldlen 사용!
IL_0019: conv.i4
IL_001a: blt.s IL_000a // i < Length이면 점프
두 메서드 모두 IEnumerator를 만들지 않고, ldlen(Length 가져오기)·ldelem.i4(int 요소 로드)·인덱스 증가의 같은 패턴을 돕니다. 차이가 있다면 foreach가 배열을 임시 변수에 한 번 담는다는 점뿐입니다. 배열 한정으로는 어느 쪽을 써도 성능이 같다고 봐도 됩니다 — 그러니 가독성이 더 좋은 쪽을 쓰면 됩니다.
ldlen이 메서드가 아니라 단일 IL 명령어라는 점도 짚어 둘 만합니다. 그래서 arr.Length는 호출 비용이 사실상 0이고, "루프 조건에서 arr.Length를 매번 평가하면 느리다"는 통념도 배열에는 해당하지 않습니다.
범위 검사와 IndexOutOfRangeException
배열 접근(ldelem·stelem)은 IL 한 명령어처럼 보이지만, JIT(Just-In-Time — 실행 직전에 IL을 기계어로 변환하는 컴파일러)는 그 자리에 보이지 않는 범위 검사를 박아 둡니다. 인덱스가 0 <= i < Length를 벗어나면 그 자리에서 IndexOutOfRangeException이 던져집니다. 이 검사 덕분에 C/C++ 같은 버퍼 오버런(범위를 넘어선 메모리에 침범) 사고를 막을 수 있지만, 매 접근마다 비교가 들어가면 성능이 깎입니다.
좋은 소식은 JIT가 for (int i = 0; i < arr.Length; i++) 같은 정형 패턴을 알아보고 범위 검사를 통째로 들어내는 BCE(Bounds Check Elimination — 범위 검사 제거) 최적화를 한다는 점입니다. JIT가 "이 루프 안에서 i는 절대 Length 이상이 될 수 없다"는 사실을 증명할 수 있으면, 매 반복의 검사를 빼 버립니다. 그래서 위 정형 패턴이 가장 빠릅니다.
반면 arr[someComputedIndex]처럼 인덱스 증명이 어려운 경우엔 검사가 그대로 남습니다. 신입이 자주 만드는 안티패턴 — 인덱스를 따로 계산해서 넘기는 헬퍼 함수, for (int i = 0; i <= arr.Length; i++) 같은 한 칸 넘는 루프 — 는 BCE 혜택을 못 받거나 런타임 예외로 떨어집니다.
<와<=— 한 글자 차이지만 결과가 다르다 길이 N인 배열의 유효 인덱스는0 ~ N-1이다.for (int i = 0; i < arr.Length; i++)은 마지막 인덱스N-1에서 끝나지만,<=로 적으면N까지 가서 즉시IndexOutOfRangeException이 발생한다.
4. 실전 적용 — Unity 핫패스에서 GC를 굶기는 법
Before/After: 매 호출 할당 vs 캐시 배열 재사용
// ❌ Before — 매 호출 새 배열을 힙에 던짐
public static int[] FindAlloc(int n)
{
int[] result = new int[n];
for (int i = 0; i < n; i++) result[i] = i;
return result;
}
// ✅ After — 미리 할당해 둔 캐시 배열에 결과를 채워 길이만 반환
private static readonly int[] _cache = new int[64];
public static int FindNonAlloc(int n)
{
int count = Math.Min(n, _cache.Length);
for (int i = 0; i < count; i++) _cache[i] = i;
return count;
}
// FindAlloc — 호출마다 newarr가 본문에 박혀 있음
IL_0001: ldarg.0
IL_0002: newarr [System.Runtime]System.Int32 // ← 매번 힙 할당
IL_0007: stloc.0
...
IL_000f: stelem.i4 // result[i] = i
// FindNonAlloc — 본문에는 newarr가 없다
IL_0002: ldsfld int32[] ArrayDemo.HotPath::_cache // ← 정적 캐시 로드
IL_0007: ldlen
...
IL_001a: stelem.i4 // _cache[i] = i
// .cctor — _cache는 클래스 처음 쓰일 때 단 한 번 만들어진다
IL_0000: ldc.i4.s 64
IL_0002: newarr [System.Runtime]System.Int32 // ← 평생 한 번만 newarr
IL_0007: stsfld int32[] ArrayDemo.HotPath::_cache
핵심은 두 메서드의 본문 IL을 비교하는 것입니다. FindAlloc은 본문 안에 newarr 한 줄이 박혀 있어 호출할 때마다 힙에 새 배열을 만듭니다. FindNonAlloc은 본문에 newarr가 없습니다 — ldsfld로 정적 필드를 가져올 뿐입니다. _cache 자체는 클래스 정적 생성자(.cctor)에서 한 번 만들어지고, 그 뒤로는 같은 객체를 모든 호출이 공유합니다. 호출 N번에 대한 힙 할당이 N번에서 1번으로 줄어듭니다.
Unity API에는 이 패턴을 그대로 따라가는 짝이 많습니다.
| 매번 새 배열 (피해야 할 쪽) | 캐시 배열 재사용 (선호) |
|---|---|
Physics.RaycastAll |
Physics.RaycastNonAlloc |
Physics.OverlapSphere |
Physics.OverlapSphereNonAlloc |
GetComponentsInChildren<T>() (배열 반환) |
GetComponentsInChildren<T>(List<T>) (오버로드) |
string.Split(...) |
미리 할당한 Span<char> 사용 |
핫패스(Update·FixedUpdate·OnTriggerStay 같이 매 프레임 호출되는 경로)에서는 거의 항상 NonAlloc 또는 캐시 패턴을 골라야 합니다. 60FPS로 10초 동안 돌리면 매 프레임 16개짜리 배열이 9600번 만들어지고, 그게 GC 입장에서는 정리해야 할 9600개의 단명 객체입니다.
특히 Unity 모바일 빌드는 IL2CPP(Intermediate Language to C++ — IL을 C++로 변환한 뒤 네이티브로 컴파일하는 빌드 백엔드)와 Boehm GC(보수적·정지형 가비지 컬렉터로, .NET CoreCLR이 쓰는 세대별 GC와 달리 한 번 돌면 모든 매니지드 스레드를 멈춘다)의 조합이라, 같은 코드라도 데스크톱·서버 .NET보다 GC 한 번에 끊기는 시간이 더 두드러집니다. Unity Profiler의 GC.Alloc 컬럼이 매 프레임 0이 되도록 핫패스를 설계하는 것이 모바일 60FPS의 출발점입니다.
빈 배열은 Array.Empty<T>()
길이가 0인 배열을 반환할 일이 자주 있다면(예: "결과가 없을 때 빈 배열을 돌려줌") new int[0] 대신 Array.Empty<int>()를 씁니다. 호출할 때마다 새 빈 배열 객체를 만드는 것이 아니라 타입별로 미리 만들어진 단 하나의 빈 배열 인스턴스를 돌려주기 때문에 GC alloc이 0입니다.
// ❌ 호출마다 작은 빈 배열 객체가 생긴다
public int[] EmptyResult() => new int[0];
// ✅ 어떤 호출이든 같은 객체 — 할당 0
public int[] EmptyResult() => Array.Empty<int>();
List<T>보다 미세하게 빠른 이유
List<T>[i]는 메서드 호출이라(내부적으로 _items[index]로 위임), 배열 직접 접근(arr[i] → IL ldelem.i4)보다 한 단계 더 거칩니다. 보통은 차이가 무의미하지만, 수만 번 반복되는 매 프레임 루프에서는 누적이 됩니다. 길이가 변하지 않고 인덱스 접근만 하는 자료라면 배열을 그대로 쓰는 편이 단순하고 약간 빠릅니다 — List<T>는 "런타임에 길이가 변하는 컨테이너"라는 정체성을 위해 도입하는 추가 비용이 있다는 사실만 기억하면 됩니다.
5. 함정과 주의사항
함정 1 — 배열 변수 대입은 복사가 아니라 별명
배열은 참조 타입입니다. 변수에 대입하면 새 배열이 생기는 게 아니라 같은 객체를 가리키는 참조가 하나 더 생길 뿐입니다. 신입이 가장 자주 깨지는 지점입니다.
![int[] original](https://blog.kakaocdn.net/dna/cChYSV/dJMcacCYFAF/AAAAAAAAAAAAAAAAAAAAABALcTBUSSP9oAa7qoHlM4VHAnLfFG3VqUxk0Hbcdu65/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1780239599&allow_ip=&allow_referer=&signature=6FWjavbRdbD5tgcSpI1x1ilxUrY%3D)
public static (int orig, int alias, int clone) ShallowVsClone()
{
int[] original = { 1, 2, 3 };
int[] aliased = original; // 참조 복사 — 같은 객체
int[] cloned = (int[])original.Clone(); // 새 객체
aliased[0] = 99;
cloned[0] = 7;
return (original[0], aliased[0], cloned[0]);
// → (99, 99, 7)
}
// 핵심 부분
IL_0012: stloc.0 // original 변수에 새 배열 저장
IL_0013: ldloc.0
IL_0014: stloc.1 // aliased = original (같은 참조)
IL_0015: ldloc.0
IL_0016: callvirt instance object [System.Runtime]System.Array::Clone() // 새 배열 객체 생성
IL_001b: castclass int32[]
IL_0020: stloc.2 // cloned에 새 객체 참조
IL_0021: ldloc.1 // aliased
IL_0022: ldc.i4.0
IL_0023: ldc.i4.s 99
IL_0025: stelem.i4 // aliased[0] = 99
// (aliased와 original이 같은 객체이므로
// original[0]도 99로 바뀐다)
aliased = original은 한 줄짜리 참조 복사이고, Clone()은 callvirt로 호출되어 진짜 새 배열을 만듭니다. 요소 값을 독립적으로 두고 싶으면 반드시 Clone()이나 Array.Copy로 복사해야 합니다. 메서드 인자로 배열을 넘길 때도 마찬가지로 — 받은 메서드가 그 배열을 수정하면 호출자의 배열도 같이 바뀝니다.
(int[])original.Clone()— 왜 캐스팅이 필요한가Array.Clone()은object를 반환한다. 배열이라는 사실은 런타임에는 알지만 컴파일러 입장에서는 일단object이므로, 다시int[]로 형변환해 줘야 인덱싱이 가능하다.
함정 2 — Length는 프로퍼티지 메서드가 아니다
arr.Length // ✅ 프로퍼티 — 단일 IL 명령(ldlen)으로 처리
arr.Length() // ❌ 컴파일 오류
List<T>는 Count(역시 프로퍼티), 문자열은 Length(프로퍼티), IEnumerable<T>에는 Count()(메서드, 전수 카운트)가 있어 헷갈리기 쉽습니다. 배열에서는 항상 괄호 없는 arr.Length입니다.
함정 3 — 공변성과 ArrayTypeMismatchException
C# 배열은 공변성(covariance — 서브타입 배열을 상위타입 배열로 다룰 수 있음)을 지원해, string[]을 object[] 자리에 넘길 수 있습니다. 컴파일은 통과하지만 런타임에 깨질 수 있다는 것이 함정입니다.
string[] strings = { "a", "b" };
object[] objs = strings; // 컴파일 OK (공변성)
objs[0] = 123; // 런타임: ArrayTypeMismatchException
// string 자리에 int를 넣으려 하므로 CLR이 막아 준다
CLR(Common Language Runtime — .NET 코드를 실행하는 가상머신)이 매 stelem 때마다 실제 요소 타입과 저장하려는 값의 타입을 비교해 막습니다. 이 비교 자체도 작은 비용이라, 성능이 중요한 자리에서는 IList<T>나 IReadOnlyList<T>처럼 제네릭 인터페이스로 전달하는 편이 안전하고 빠릅니다.
함정 4 — 참조 타입 배열은 모든 칸이 null
new string[5]는 string 다섯 개를 만들지 않습니다. 다섯 개의 빈 슬롯을 만들고 모두 null로 채울 뿐입니다.
string[] names = new string[3];
Console.WriteLine(names[0].Length); // NullReferenceException
값 타입 배열은 자동으로 0/false로 초기화되지만, 참조 타입 배열은 각 칸을 직접 new로 채워 넣기 전까지는 모두 비어 있다고 기억해야 합니다.
6. C# 버전별 변화
배열 자체는 C# 1.0부터 거의 그대로지만, 주변 문법은 꾸준히 발전했습니다.
| 버전 | 변화 | 비고 |
|---|---|---|
| 1.0 | int[] x = new int[] { 1, 2, 3 } |
기본 |
| 3.0 | int[] x = new[] { 1, 2, 3 } |
요소 타입 추론 |
| 12 | int[] x = [1, 2, 3] |
컬렉션 식 — List<T>·Span<T> 등에도 같은 문법 |
C# 12에서 등장한 컬렉션 식은 형태가 새롭지만, 단순 배열에 한해서는 컴파일 결과가 기존과 똑같습니다.
public static int[] OldStyle() => new int[] { 1, 2, 3 };
public static int[] NewStyle() => [1, 2, 3];
// OldStyle()
IL_0000: ldc.i4.3
IL_0001: newarr [System.Runtime]System.Int32
IL_0006: dup
IL_0007: ldtoken field ... '4636993D...' // 사전 정의된 12바이트 데이터 블록
IL_000c: call void ... RuntimeHelpers::InitializeArray(...)
IL_0011: ret
// NewStyle() — 완전히 동일!
IL_0000: ldc.i4.3
IL_0001: newarr [System.Runtime]System.Int32
IL_0006: dup
IL_0007: ldtoken field ... '4636993D...' // 같은 hash, 같은 데이터 블록
IL_000c: call void ... RuntimeHelpers::InitializeArray(...)
IL_0011: ret
두 메서드의 IL이 한 글자도 다르지 않습니다. 컬렉션 식이 진가를 발휘하는 곳은 List<T>·Span<T>·HashSet<T> 같은 다른 컬렉션 타입과 짝을 이룰 때입니다 — [1, 2, 3] 한 표기로 어떤 타입의 컬렉션이든 만들 수 있게 된 것이 핵심이고, 1차원 배열에서는 "더 짧게 쓸 수 있다" 정도로 받아들이면 충분합니다.
C# 12 컬렉션 식의 다른 활용(전개 연산자 .., Span<T>와의 조합 등)은 PART 6의 5번 주제 "컬렉션 식"에서 별도로 다룹니다.
7. 정리
이 글에서 다룬 내용을 한 손에 잡히는 체크리스트로 압축합니다.
- [ ] 배열은 참조 타입이고, 변수는 힙 객체를 가리키는 포인터다.
- [ ] 메모리 레이아웃: 객체 헤더 + Length(읽기 전용) + 요소 영역(연속).
- [ ] 인덱스 0 시작은
요소 주소 = 시작 + i × 크기식을 단순하게 유지하기 위해서다. - [ ]
Length는 프로퍼티(괄호 없음). IL에서는ldlen단일 명령으로 처리되어 호출 비용이 사실상 0이다. - [ ] 배열에 한해
for와foreach는 IL 패턴이 거의 같다 — 가독성으로 고른다. - [ ]
ldelem·stelem마다 JIT가 범위 검사를 박는다.for (int i = 0; i < arr.Length; i++)같은 정형 패턴은 BCE(Bounds Check Elimination)로 검사가 사라진다. - [ ]
new int[N]은 본문에newarr를 박는다 → 매 호출 GC alloc. 핫패스에서는 캐시 배열·*NonAllocAPI를 써서 본문에서newarr를 없앤다. - [ ] 빈 배열을 자주 돌려준다면
Array.Empty<T>()(타입당 인스턴스 1개). - [ ]
int[] b = a;는 별명이지 복사가 아니다. 독립이 필요하면(int[])a.Clone()또는Array.Copy. - [ ] 참조 타입 배열의 모든 칸은
null로 초기화된다. 값을 직접 넣어 줘야 안전하게 접근할 수 있다. - [ ] C# 12 컬렉션 식
[1, 2, 3]은 단순 배열에서 기존 표기와 IL이 같다. 진가는 다른 컬렉션 타입과 함께 쓸 때 드러난다 — 이는 PART 6-5에서 다룬다.
'C# 기초' 카테고리의 다른 글
| [PART6.배열과 문자열 기본(3/14)] 배열 초기화·순회·Length — 가장 많이 쓰는 세 가지 작업 (0) | 2026.05.01 |
|---|---|
| [PART6.배열과 문자열 기본(2/14)] 다차원 배열과 들쭉날쭉 배열 — `int[,]`와 `int[][]`는 다른 동물이다 (0) | 2026.05.01 |
| [PART5.메서드와 매개변수(18/18)] 메서드를 매개변수로 넘기기 — 콜백의 첫 소개 (0) | 2026.04.28 |
| [PART5.메서드와 매개변수(17/18)] async Main — 진입점도 비동기로 (C# 7.1) (0) | 2026.04.28 |
| [PART5.메서드와 매개변수(16/18)] 던지기 표현식 — throw as expression (C# 7) (0) | 2026.04.28 |