C#

[C#] 배열

sunlight-dby 2025. 5. 2. 13:58

 

'이것이 C#이다 개정판'의 chapter 10을 정리한 글입니다.

 


 

배열

배열은 필요한 용량을 가지고, 데이터를 넣어 사용됩니다.

주로, 같은 성격을 띤 다수의 데이터를 한번에 다룰때 사용됩니다.

데이터형식[ ] 배열이름 = NEW 데이터형식[ 용량 ];

 

 

[System.Index & ^ 연산자]

배열의 마지막 요소에 접근하려면, 배열의 길이에서 1을 빼는 식으로 마지막 인덱스에 접근해야 합니다.

배열이름[배열이름.Length - 1]

 

C# 8.0부터는 더 간편한 방법으로 마지막 인덱스에 접근할 수 있게 'System.Index 형식' '^ 연산자'를 지원합니다.

 

^ 연산자는 컬렉션의 마지막부터 역순으로 인덱스를 지정하는 기능을 갖고 있습니다.

^1은 컬렉션의 마지막 요소를 나타내는 인덱스, ^2는 컬렉션의 마지막에서 두 번째 요소를 나타내는 인덱스입니다.

 

^ 연산자의 연산 결과는 System.Index 형식의 인스턴스로 나타납니다.

 

[예제]

// System.Index 형식의 인스턴스로 나타내는 방식
System.Index last = ^1;
scores[last] = 34;  // scores[scores.Length - 1] = 34;와 동일

// System.Index의 인스턴스 없이 나타내는 방식
scores[^1] = 34;    // scores[scores.Length - 1] = 34;와 동일

[배열의 초기화]

배열을 초기화하는 방법은 세 가지의 방법이 존재합니다.

 

1. 첫 번째 방법

  • 배열의 원소 개수를 명시하고, 중괄호로 둘러싸인 블록을 생성한 후, 블록 사이에 배열의 각 원소에 입력될 데이터를 입력합니다.
  • 배열 객체를 초기화하는 { } 블록을 일컬어 '컬렉션 초기자 (Collection Initializer)'라고 부릅니다.
string[] array = new string[3] { "안녕", "Hello", "Halo" };

 

2. 두 번째 방법

  • 첫 번째 방법에서 배열의 용량을 생략합니다.
  • 배열의 용량을 생략하여 초기화해도 컴파일러는 용량을 생략하지 않았을 때와 동일한 실행 파일을 만듭니다.
string[] array = new string[] { "안녕", "Hello", "Halo" };

 

3. 세 번째 방법

  • new 연산자, 형식과 대괄호, 배열의 용량을 모두 생략한 채 블록 사이에 배열의 각 원소에 할당할 데이터를 넣어줍니다.
  • 문법은 훨씬 간편해졌지만, 그 결과는 첫 번째와 두 번째 방법과 동일합니다.
string[] array = { "안녕", "Hello", "Halo" };

[System.Array]

C#에서는 모든 것이 객체입니다. 배열도 객체이며, .NET의 CTS(Common Type System)에서 배열은 System.Array 클래스에 대응됩니다.

다음 예제 프로그램은 int 기반의 배열이 System.Array 형식에서 파생되었음을 보여줍니다.

 

[예제]

using System;

namespace DerivedFromArray
{
    class MainApp
    {
        static void Main(string[] args)
        {
            int[] array = new int[] { 10, 30, 20, 7, 1 };
            Console.WriteLine($"Type Of array : {array.GetType()}");
            Console.WriteLine($"Base type Of array : {array.GetType().BaseType}");
        }
    }
}

실행 결과

 

해당 예제 프로그램을 보면, int 기반의 배열이 System.Array 형식에서 파생되었음을 보여줍니다.

 

[Array 클래스의 주요 메서드와 프로퍼티]

분류 이름 설명
정적 메서드   Sort( )   배열을 정렬합니다. 오름차순 정렬이 기본값입니다.
   BinarySearch<T>( )   이진 탐색을 수행합니다.
   IndexOf( )   배열에서 찾고자 하는 특정 데이터의 인덱스를 반환합니다.
   TrueForAll<T>( )   배열의 모든 요소가 지정한 조건에 부합하는지의 여부를 반환합니다.
   FindIndex<T>( )   배열에서 지정한 조건에 부합하는 첫 번째 요소의 인덱스를 반환합니다.
  IndexOf( ) 메서드가 특정 값을 찾는데 비해,
  FindIndex<T>( )메서드는 지정한 조건에 바탕하여 값을 찾습니다.
   Resize<T>( )   배열의 크기를 재조정합니다.
   Clear( )   배열의 모든 요소를 초기화합니다. 배열이 숫자 형식 기반이면 0으로,
  논리 형식 기반이면 false로, 참조 형식 기반이면 null로 초기화합니다.
   ForEach<T>( )   배열의 모든 요소에 대해 동일한 작업을 수행하게 합니다.
   Copy<T>( )   배열의 일부를 다른 배열에 복사합니다.
인스턴스
메서드
   GetLength( )   배열에서 지정한 차원의 길이를 반환합니다.
  이 메서드는 다차원 배열에서 유용하게 사용됩니다.
프로퍼티    Length   배열의 길이를 반환합니다.
   Rank   배열의 차원을 반환합니다.

 

[예제]

using System;

namespace MoreOnArray
{
    class MainApp
    { 
        private static bool CheckPassed(int score)
        {
            return score >= 60;
        }

        private static void  Print(int value)
        {
            Console.Write($"{value} ");
        }

        static void Main(string[] args)
        {
            int[] scores = new int[] { 80, 74, 81, 90, 34 };

            foreach (int score in scores)
                Console.Write($"{score} ");
            Console.WriteLine();

            Array.Sort(scores);
            Array.ForEach<int>(scores, new Action<int>(Print));
            Console.WriteLine();


            Console.WriteLine($"Number of dimensions : {scores.Rank}");

            Console.WriteLine($"Binary Search : 81 is at "
                + $"{Array.BinarySearch<int>(scores, 81)}");

            Console.WriteLine($"Linear Search : 90 is at "
                + $"{Array.IndexOf<int>(scores, 90)}");

            Console.WriteLine($"Everyone passed ? : "
                + $"{Array.TrueForAll<int>(scores, CheckPassed)}");
            // CheckPassed : TrueForAll 메서드는 배열과 함께 조건을 검사하는 메서드를 매개변수로 받습니다.

            int index = Array.FindIndex<int>(scores, (score) => score < 60);
            // FindIndex : FindIndex 메서드는 특정 조건에 부합하는 메서드를 매개변수로 받습니다.
            // 해당 구문에서는 람다식으로 구현해봤습니다.

            scores[index] = 61;
            Console.WriteLine($"Everyone passed ? : "
                + $"{Array.TrueForAll<int>(scores, CheckPassed)}");

            Console.WriteLine("Old length of scores : " +
                $"{scores.GetLength(0)}");

            Array.Resize<int>(ref scores, 10);  // 5였던 배열의 용량을 10으로 재조정합니다.
            Console.WriteLine($"New length of scores : {scores.Length}");

            Array.ForEach<int>(scores, new Action<int>(Print)); // Action 대리자 사용
            Console.WriteLine();

            Array.Clear(scores, 3, 7);
            Array.ForEach<int>(scores, new Action<int>(Print));
            Console.WriteLine();

            int[] sliced = new int[3];
            Array.Copy(scores, 0, sliced, 0, 3);
            Array.ForEach<int>(sliced, new Action<int>(Print));
            Console.WriteLine();
        }
    }
}

실행 결과

 

[개념]

  • 이진 탐색 (Binary Search)
    • 이진 탐색은 정렬된 데이터(배열이나 리스트)에서 특정 값을 찾는 효율적인 탐색 알고리즘입니다.
    • 데이터의 가운데 요소로부터 찾으려는 값을 비교하면서, 찾으려는 값이 포함된 구간을 계속해서 가운데 요소로부터 비교하는 것을 반복하여 탐색합니다.
    • 시간 복잡도는 O(log n)입니다.
  • 선형 탐색 (Linear Search)
    • 선형 탐색은 데이터(배열이나 리스트)에서 원하는 값을 찾기 위해 처음부터 끝까지 순서대로 하나씩 비교하면서 찾는 기본적인 탐색 방법입니다.
    • 구현은 간단하지만, 이진 탐색과 비교하여 비효율적입니다.
    • 시간 복잡도는 O(n)입니다.
  • Array.TrueForAll<T>( ) 메서드
    • 역할
      • Array.TureForAll 메서드 배열의 모든 요소가 지정된 조건(predicate)을 만족하는지 확인합니다.
      • 만약 배열의 모든 요소가 조건을 만족하면 true를 반환하고, 하나라도 조건을 만족하지 않으면 즉시 false를 반환합니다.
    • 문법 (시그니처)
      •  public static bool TrueForAll<T>(T[ ] array, Predicate<T> predicate)
    • Predicate<T> predicate
      • 각 배열 요소에 대해 조건을 검사하는 메서드를 나타내는 대리자(Delegate)입니다.
      • 해당 대리자는 T 타입의 인자를 하나 받고 bool을 반환하는 메서드를 참조해야 합니다.
      • 예제에서는 CheckPassed 메서드를 매개변수로 받아 조건을 검사하였습니다.
    • 동작 방식
      • 배열의 첫 번째 요소부터 시작하여, 각 요소를 predicate 대리자가 가리키는 메서드의 인자로 전달하여 호출합니다.
        만약 predicate 메서드가 false를 반환하면, 메서드는 즉시 실행을 멈추고 false를 반환합니다.
        배열의 모든 요소에 대해 predicate 메서드가 true를 반환하면, 메서드는 최종적으로 true를 반환합니다.
  • 람다식 (Lambda Expression)
    • 람다식은 메서드를 간결하게 작성하는 방법이며, 특히 이름이 없는 익명 메서드를 만들 때 사용됩니다.
    • 기본 형태는 아래와 같습니다.
      • (매개변수_목록) => 메서드_본문;
      • => 연산자는 매개변수 목록과 메서드 본문을 구분하기 위한 연산자로,
        람다 연산자(Lambda operator) 또는 표현식 본문 연산자(Expression-body operator)라고 불립니다.
  • Action 대리자 (Action Delegate)
    • 역할
      • Action 대리자는 반환 값이 없는 메서드를 참조하는 데 사용되는 미리 정의된 대리자입니다.
      • 특정 작업을 수행하지만 겨로가 값을 돌려주지 않는 메서드들을 Action 타입의 변수에 담아서 전달하거나 호출할 수 있게 해줍니다.
    • 문법 (시그니처)
      • public delegate void Action<T>(매개변수_목록);
      • 매개변수는 0개부터 최대 16개까지 받을 수 있게 정의되어 있습니다.
    • 동작 방식
      • Action 대리자는 메서드를 객체처럼 다룰 수 있게 해주는 참조 타입이며, 메서드의 주소와 필요한 객체 인스턴스 정보를 캡슐화하여 대리자 인스턴스를 통해 언제든지 해당 메서드를 호출할 수 있게 합니다.
        여러 메서드를 등록하여 순차적으로 실행하는 멀티캐스트 기능도 지원합니다.
        • 정적 메서드의 경우는 인스턴스 정보가 필요 없습니다.
      • 예제에서의 Print 메서드는 int 매개변수 하나를 받고 반환값이 없으므로 Action<int> 대리자로 사용될 수 있습니다.

[배열의 분할]

위의 예제에서는 Array.Copy() 메서드를 통해 배열을 분할하였습니다.

하지만, C# 8.0부터 도입된 'System.Range', 그리고 '..연산자'를 통해 더욱 간편하게 배열을 분할할 수 있습니다.

 

[예제]

int[] scores = new int[] { 80, 74, 81, 90, 34 };

// 첫 번째 요소(0의 인덱스 값을 갖는 요소)부터 세 번째(2의 인덱스 값을 갖는 요소) 요소까지
System.Range r1 = 0..3;
int[] sliced = scores[r1];             // 80, 74, 81

// 첫 번재 요소(0"")부터 세 번째 요소(2"")까지
int[] sliced2 = scores[0..3];          // 80, 74, 81

// 첫 번째 요소(0"")부터 세 번째 요소(2"")까지
int[] sliced3 = scores[..3];           // 80, 74, 81

// 두 번째 요소(1"")부터 마지막 요소까지
int[] sliced4 = scores[1..];           // 74, 81, 90, 34

// 전체
int[] sliced5 = scores[..];            // 80, 74, 81, 90, 34

// 전체 : Index 객체 사용
Index last = ^0;
Range range_0_last = 0..last;
int[] sliced6 = scores[range_0_last];  // 80, 74, 81, 90, 34

// 전체 : Index 객체 사용
int[] sliced7 = socres[^5..^0];        // 80, 74, 81, 90, 34

 

※ 예제를 보면 아시겠지만, ..연산자의 마지막 인덱스는 배열 분할 결과에서 제외됩니다.

    예제에서 sliced6과 sliced7 배열에서 ^0이 사용된 이유 또한


2차원 배열

2차원 배열은 2개의 차원으로 원소를 배치하는 배열입니다.

데이터 형식[ , ] 배열이름 = new 데이터형식[ 2차원길이, 1차원길이 ];

 

[2차원 배열 선언 및 초기화]

2차원 배열을 선언할 때 동시에 초기화하고 싶다면 , 1차원 배열의 세 가지 초기화 방법과 동일한 형태로 초기화할 수 있습니다.

// 배열의 형식과 길이를 명시
int[,] arr = new int[2, 3] { {1, 2, 3}, {4, 5, 6} };

// 배열의 길이를 생략
int[,] arr = new int[,] { {1, 2, 3}, {4, 5, 6} };

// 형식과 길이를 모두 생략
int[,] arr = { {1, 2, 3}, {4, 5, 6} };

 

[예제]

using System;

namespace _2DArray
{
    class MainApp
    { 
        static void Main(string[] args)
        {
            int[,] arr = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } };

            for (int i = 0; i < arr.GetLength(0); ++i)
            {
                for (int j = 0; j < arr.GetLength(1); ++j)
                {
                    Console.Write($"[{i}, {j}] : {arr[i, j]}");
                }
                Console.WriteLine();
            }
            Console.WriteLine();


            foreach(int i in arr)
            {
                Console.Write($"{i} ");
            }
        }
    }
}

실행 결과

 

※ 다차원 배열에 foreach를 사용하면, 컴파일러가 내부적으로 각 차원의 인덱스를 순회하며 모든 요소에 순차적으로 접근합니다.


다차원 배열

다차원 배열은 차원이 둘 이상인 배열을 말합니다. 같은 맥락으로 2차원 배열도 다차원 배열에 해당합니다.

다차원 배열을 선언하는 문법은 2차원 배열의 문법과 같습니다.

 

[예제]

int[ , , ] array = new int[4, 3, 2]
                   {
                       { {1, 2}, {3, 4}, {5, 6} },
                       { {1, 4}, {2, 5}, {3, 6} },
                       { {6, 5}, {4, 3}, {4, 1} },
                       { {6, 3}, {5, 2}, {4, 1} },
                   };

 

[주의할 점]

3차원 이상의 배열부터는 사용할 때 주의해야 합니다. 그리고 3차원 이상의 배열부터는 사용을 권하지 않습니다.

개발하면서 3차원 이상의 배열은, 개발하는 개발자가 이해하기 쉽지 않기 때문입니다.

본인이 이해하기 쉽지 않은 배열을 사용하게 되면 그만큼 많은 버그를 초래할 수 있습니다.

 

그러니 3차원 이상의 다차원 배열을 사용할 때는 유의하며 사용해야 합니다.


가변 배열 (Jagged Array)

가변 배열은 배열들을 담고 있는 또 다른 배열로, 각 내부 배열(행)의 길이가 서로 다를 수 있는 배열입니다.

 

[선언 방식]

데이터형식[ ][ ] 배열이름 = new 데이터 형식[가변_배열의_용량][ ];

 

 

[작동 방식]

[다차원 배열]

다차원 배열은 하나의 큰 직사각형 형태의 메모리 블록처럼 생각할 수 있습니다.

예를 들어 int[2, 3] 배열은 2행 3열의 직사각형 모양으로, 모든 칸(요소)이 연속된 메모리에 저장됩니다.

1 2 3
4 5 6

 

[가변 배열]

가변 배열은 다차원 배열처럼 하나의 큰 직사각형이 아니라, 배열들을 담고 있는 또 다른 배열입니다.

예를 들어 int[3][ ] 가변 배열이 존재한다 했을 때, 가변 배열은 3개의 공간을 가진 1차원 배열을 먼저 만듭니다.

그리고 이 3개의 각 공간에 별도의 1차원 배열들을 할당하여 연결하는 구조입니다.

 

[가변 배열 자체]

참조 1 참조 2 참조 3

 

[참조 1이 가리키는 별도의 배열] : 첫 번째 내부 배열 - 길이 5

         

 

[참조 2가 가리키는 별도의 배열]  : 두 번째 내부 배열 - 길이 3

     

 

[참조 3이 가리키는 별도의 배열]  : 세 번째 내부 배열 - 길이 2

   

 

 

[선언과 초기화]

// 선언 후 할당
int[][] jagged = new int[3][];
jagged[0] = new int[5]  { 1, 2, 3, 4, 5 };
jagged[1] = new int[] { 10, 20, 30};
jagged[2] = new int[] { 100, 200 };

// 선언과 동시에 초기화
int[][] jagged = new int[2][] {
                   new int[] { 1000, 2000 },
                   new int[4] { 6, 7, 8, 9 }
                 };

 

[예시]

using System;

namespace JaggedArray
{
    class MainApp
    {
        static void Main(string[] args)
        {
            int[][] jagged = new int[3][];
            jagged[0] = new int[5] { 1, 2, 3, 4, 5 };
            jagged[1] = new int[] { 10, 20, 30 };
            jagged[2] = new int[] { 100, 200 };

            foreach (int[] arr in jagged)
            {
                Console.Write($"Length : {arr.Length}, ");
                foreach (int e in arr)
                {
                    Console.Write($"{e} ");
                }
                Console.WriteLine();
            }

            Console.WriteLine();

            int[][] jagged2 = new int[2][]
            {
                new int[] { 1000, 2000 },
                new int[4] { 6, 7, 8, 9 },
            };

            foreach (int[] arr in jagged2)
            {
                Console.Write($"Length : {arr.Length}, ");
                foreach (int e in arr)
                {
                    Console.Write($"{e} ");
                }
                Console.WriteLine();
            }
        }
    }
}

실행 결과

'C#' 카테고리의 다른 글

[C#] 일반화 프로그래밍  (0) 2025.05.05
[C#] 컬렉션, 인덱서  (1) 2025.05.03
[C#] 프로퍼티, 레코드  (0) 2025.04.25
[C#] 인터페이스  (0) 2025.04.24
[C#] 구조체, 변경 불가능 객체, 튜플  (0) 2025.04.22