C# 배열 활용과 고급 기법

배열 기초와 다양한 초기화 방식

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로 열거자를 획득하고, MoveNexttrue를 반환하는 동안 Current 속성으로 현재 요소에 접근합니다.

커스텀 반복기 구축

yield return은 반복기를 간결하게 구현하는 문법적 설탕입니다. 각 yield return은 현재 요소를 반환하고 다음 위치로 상태를 보존합니다. yield break는 반복을 즉시 종료합니다.

이 구문이 포함된 메서드나 속성은 반복 블록이 되며, 컴파일러는 내적으로 상태 머신을 구현하는 중첩 클래스를 자동 생성합니다.

Tuple: 이질적 데이터의 결합

배열이 동일한 유형의 집합이라면, 튜플은 서로 다른 유형의 요소를 하나로 묶습니다. Tuple 클래스는 최대 8개 요소를 지원하며, 8번째 요소는 또 다른 튜플을 받아 확장이 가능합니다.

구조적 비교의 세계

배열과 튜플은 IStructuralEquatableIStructuralComparable을 구현하여 참조가 아닌 내용 기반의 비교를 제공합니다. 이들은 명시적 구현이므로 사용 시 인터페이스로 캐스팅이 필요합니다.

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 메서드는 제공된 비교자를 각 요소에 순차적으로 적용합니다.

태그: C# Array JaggedArray ArraySegment IEnumerable

5월 29일 23:01에 게시됨