배열 기초와 다양한 초기화 방식
C#에서 동일한 데이터 유형의 여러 요소를 효율적으로 관리하려면 배열이나 컬렉션을 사용합니다. System.Array 클래스는 요소 정렬, 필터링 기능을 제공하며, 열거자를 통해 전체 요소를 순회할 수 있습니다.
배열 선언과 메모리 할당
배열은 참조 형태이므로 선언 시점에 실제 데이터를 담을 메모리 공간이 확보되지 않습니다. new 연산자를 통해 요소의 자료형과 개수를 명시하여 초기화해야 합니다.
double[] scores;
초기화 패턴
다양한 초기화 방식을 살펴봅시다:
// 1단계: 선언 후 별도 초기화
double[] scores;
scores = new double[5];
// 2단계: 크기 지정과 동시에 값 할당
double[] scores = new double[4] { 85.5, 92.0, 78.5, 90.0 };
// 3단계: 크기 생략 후 암묵적 추론
double[] scores = new double[] { 85.5, 92.0, 78.5, 90.0, 88.0 };
// 4단계: 가장 간결한 표현
double[] scores = { 85.5, 92.0, 78.5, 90.0, 88.0 };
인덱스를 통한 요소 접근
인덱스는 0부터 시작하며, Length 속성을 활용해 안전하게 마지막 요소에 접근할 수 있습니다.
// 직접 인덱스 접근
scores[0] // 첫 번째 요소
scores[scores.Length - 1] // 마지막 요소
// for 문 순회
for (int idx = 0; idx < scores.Length; idx++)
{
Console.WriteLine(scores[idx]);
}
// foreach 문 순회
foreach (var point in scores)
{
Console.WriteLine(point);
}
참조 형식 배열의 주의사항
배열 요소가 참조 형식일 경우, 각 요소별로 개별적으로 인스턴스를 생성해야 합니다. 초기화되지 않은 요소에 접근하면 NullReferenceException이 발생합니다.
Employee[] staff = new Employee[3];
staff[0] = new Employee { Name = "Kim", Department = "개발" };
staff[1] = new Employee { Name = "Lee", Department = "인사" };
staff[2] = new Employee { Name = "Park", Department = "기획" };
다차원 배열의 이해
단일 인덱스로 접근하는 것이 1차원 배열이라면, 여러 개의 인덱스를 동시에 사용하는 것이 다차원 배열입니다.
2차원 배열
int[,] matrix = new int[3, 3];
matrix[0, 0] = 1; matrix[0, 1] = 2; matrix[0, 2] = 3;
matrix[1, 0] = 4; matrix[1, 1] = 5; matrix[1, 2] = 6;
matrix[2, 0] = 7; matrix[2, 1] = 8; matrix[2, 2] = 9;
경계를 벗어나는 인덱스(예: matrix[3,3], matrix[2,3], matrix[3,2])는 런타임 예외를 유발합니다. 또한 값이 할당되지 않은 요소를 참조해도 예외가 발생할 수 있습니다.
초기화 구문을 사용하면 더 간결하게 표현 가능합니다:
int[,] matrix = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
중괄호 내의 각 중첩 집합이 행을 의미하며, 첫 번째 중첩 집합의 첫 번째 값이 matrix[0,0]에 해당합니다.
3차원 배열
int[,,] cube = {
{ { 1, 2 }, { 3, 4 } },
{ { 5, 6 }, { 7, 8 } },
{ { 9, 10 }, { 11, 12 } }
};
Console.WriteLine(cube[0, 1, 1]); // 출력: 4
Console.WriteLine(cube[2, 0, 1]); // 출력: 10
가변 배열(Jagged Array)
가변 배열은 각 행마다 독립적인 길이를 가질 수 있는 배열 구조입니다. 선언 시 대괄호를 연속으로 배치하며, 내부 배열은 개별적으로 생성합니다.
int[][] irregular = new int[3][];
irregular[0] = new int[] { 1, 2 };
irregular[1] = new int[] { 3, 4, 5, 6, 7, 8 };
irregular[2] = new int[] { 9, 10, 11 };
중첩 반복문을 통해 모든 요소를 순회합니다:
for (int r = 0; r < irregular.Length; r++)
{
for (int c = 0; c < irregular[r].Length; c++)
{
Console.WriteLine($"행: {r}, 열: {c}, 값: {irregular[r][c]}");
}
}
Array 클래스의 활용
동적 배열 생성
Array는 추상 클래스이므로 직접 인스턴스화할 수 없습니다. 대신 CreateInstance 정적 메서드를 활용합니다.
Array dynamicArray = Array.CreateInstance(typeof(int), 5);
for (int i = 0; i < dynamicArray.Length; i++)
{
dynamicArray.SetValue(i * 10, i);
}
for (int i = 0; i < dynamicArray.Length; i++)
{
Console.WriteLine(dynamicArray.GetValue(i));
}
// 명시적 형변환
int[] typedArray = (int[])dynamicArray;
배열 복제의 미묘한 차이
배열은 참조 형식이므로 단순 대입은 동일한 메모리 주소를 공유하게 됩니다. 독립적인 복사본이 필요하다면 Clone 메서드를 사용합니다.
int[] original = { 10, 20 };
int[] duplicate = (int[])original.Clone();
주의할 점은 Clone이 얕은 복사(Shallow Copy)를 수행한다는 것입니다. 참조 형식 요소의 경우 객체 자체가 아닌 참조만 복사됩니다.
Product[] inventory = {
new Product { Code = "A001", Price = 50000 },
new Product { Code = "B002", Price = 75000 }
};
Product[] inventoryCopy = (Product[])inventory.Clone();
// inventory[0].Price = 60000; // inventoryCopy[0]에도 영향
Copy 메서드도 동일하게 얕은 복사를 수행합니다.
정렬 메커니즘
Array.Sort는 퀵 정렬 알고리즘을 기반으로 하며, 정렬 대상은 IComparable<T> 인터페이스를 구현해야 합니다.
string[] artists = {
"Christina Aguilera",
"Shakira",
"Beyonce",
"Gwen Stefani"
};
Array.Sort(artists);
foreach (var artist in artists)
{
Console.WriteLine(artist);
}
사용자 정의 클래스에 대한 정렬을 구현하려면:
public class Member : IComparable<Member>
{
public string FamilyName { get; set; }
public string GivenName { get; set; }
public override string ToString() => $"{FamilyName} {GivenName}";
public int CompareTo(Member other)
{
if (other is null) throw new ArgumentNullException(nameof(other));
int comparison = this.FamilyName.CompareTo(other.FamilyName);
if (comparison == 0)
{
comparison = this.GivenName.CompareTo(other.GivenName);
}
return comparison;
}
}
배열의 매개변수 전달과 변성
배열 공변성(Array Covariance)
배열은 공변성을 지원하여, 기반 클래스 타입으로 선언된 배열에 파생 클래스 배열을 할당할 수 있습니다. 단, 이는 참조 형식에만 적용됩니다.
// Manager가 Employee를 상속받은 경우
Employee[] employees = new Manager[5]; // 유효한 코드
공변성은 "더 구체적인(파생된) 타입을 더 일반적인(기반) 타입으로 대체할 수 있음"을 의미합니다. 반대로 역변성은 "더 일반적인 타입을 더 구체적인 타입으로 대체할 수 있음"을 의미합니다.
ArraySegment<T>로 구간 처리
대규모 배열의 특정 구간만 별도 처리가 필요할 때 ArraySegment<T>를 활용합니다. 이는 원본 배열을 복사하지 않고 참조하므로 메모리를 절약합니다.
int[] dataset1 = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int[] dataset2 = { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };
var portion1 = new ArraySegment<int>(dataset1, 0, 3);
var portion2 = new ArraySegment<int>(dataset2, 3, 3);
int total = SumPortions(portion1, portion2);
중요한 점은 ArraySegment<T>가 원본 배열을 참조하므로, 세그먼트 내 요소를 수정하면 원본 배열도 변경된다는 사실입니다.
열거자 패턴과 yield 구문
foreach의 내부 동작
foreach 문은 컴파일 시 IEnumerator 인터페이스 기반의 코드로 변환됩니다. GetEnumerator로 열거자를 획득하고, MoveNext가 true를 반환하는 동안 Current 속성으로 현재 요소에 접근합니다.
커스텀 반복기 구축
yield return은 반복기를 간결하게 구현하는 문법적 설탕입니다. 각 yield return은 현재 요소를 반환하고 다음 위치로 상태를 보존합니다. yield break는 반복을 즉시 종료합니다.
이 구문이 포함된 메서드나 속성은 반복 블록이 되며, 컴파일러는 내적으로 상태 머신을 구현하는 중첩 클래스를 자동 생성합니다.
Tuple: 이질적 데이터의 결합
배열이 동일한 유형의 집합이라면, 튜플은 서로 다른 유형의 요소를 하나로 묶습니다. Tuple 클래스는 최대 8개 요소를 지원하며, 8번째 요소는 또 다른 튜플을 받아 확장이 가능합니다.
구조적 비교의 세계
배열과 튜플은 IStructuralEquatable와 IStructuralComparable을 구현하여 참조가 아닌 내용 기반의 비교를 제공합니다. 이들은 명시적 구현이므로 사용 시 인터페이스로 캐스팅이 필요합니다.
Person[] groupA = {
new Person { FirstName = "Min", LastName = "Ji" },
new Person { FirstName = "Hye", LastName = "Jin" }
};
Person[] groupB = {
new Person { FirstName = "Min", LastName = "Ji" },
new Person { FirstName = "Hye", LastName = "Jin" }
};
// 참조 비교: 서로 다른 객체
Console.WriteLine(groupA != groupB); // true
// 구조적 비교를 위한 캐스팅
var equatable = (IStructuralEquatable)groupA;
bool identical = equatable.Equals(groupB, EqualityComparer<Person>.Default);
사용자 정의 비교 로직은 IEqualityComparer<T>를 구현하여 전달할 수 있습니다.
public class PersonComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
if (x is null || y is null) return false;
return x.FirstName == y.FirstName && x.LastName == y.LastName;
}
public int GetHashCode(Person obj)
{
return HashCode.Combine(obj.FirstName, obj.LastName);
}
}
튜플에 대해서도 동일한 패턴이 적용되며, Tuple<T1, T2>의 Equals 메서드는 제공된 비교자를 각 요소에 순차적으로 적용합니다.